您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
my version of the floating comment box script by ScriptMouse
当前为
// ==UserScript== // @name AO3: Floating Comment Box - Redux // @namespace https://gf.qytechs.cn/en/users/906106-escctrl // @version 0.2 // @description my version of the floating comment box script by ScriptMouse // @author escctrl // @license MIT // @match *://archiveofourown.org/works/* // @exclude *://archiveofourown.org/works/*/new // @exclude *://archiveofourown.org/works/*/edit // @exclude *://archiveofourown.org/works/new* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js // @grant none // ==/UserScript== (function($) { 'use strict'; // done: get a floating comment box that is not modal (you can move it around) and resizable // GOAL: make it work on mobile XD // GOAL: make it work with my Comment Formatting script (that means adapting the other script) // done: make it submit the comment directly, no need to copy it elsewhere (like owl's comment from bins) // done: cache comment text // GOAL: choices of pseud and chapter (if viewing multiple chapters) // done: insert highlighted text directly in comment (in italics or blockquote) // done: character counter // done: open it from a nicely placed button // done: while submitting, show some sort of progress & load the page to the new comment if possible // done: make it open at the position where it was last closed $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">`) // button at top of work to open the modal let cmtButton = `<li id='float_cmt_button'><a href='#'>Floating Comment</a></li>`; $('#show_comments_link_top').after(cmtButton); // prepping the dialog (without opening it) var dlg = "#float_cmt_dlg"; createCommentBox(); // open or reopen the dialog when the button is clicked $('#float_cmt_button').on('click', (e) => { e.preventDefault(); openCommentBox(); }); // prepares the dialog and loads the cache into it function createCommentBox() { // designing the floating box $("body").append(`<div id="float_cmt_dlg"></div>`); // optimizing the size of the GUI in case it's a mobile device let dialogwidth = parseInt($("body").css("width")); // parseInt ignores letters (px) dialogwidth = dialogwidth > 500 ? 500 : dialogwidth * 0.9; $(dlg).dialog({ modal: false, autoOpen: false, resizable: true, width: dialogwidth, position: { my: "right bottom", at: "right bottom" }, title: "Comment", buttons: { CopyHighlight: grabHighlight, Discard: discardComment, Post: submitComment, Cancel: closeCommentBox }, // positioning stuff below is so that it SCROLLS WITH THE PAGE JFC https://stackoverflow.com/a/9242751/22187458 create: function(event, ui) { $(event.target).parent().css('position', 'fixed'); // and also to put the dialog where it was last left across pageloads let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt'))); if (cachemap.get('pos')) { let pos = JSON.parse(cachemap.get('pos')); pos.of = $(window); $(dlg).dialog('option','position', pos); } }, resizeStop: function(event, ui) { let position = [(Math.floor(ui.position.left) - $(window).scrollLeft()), (Math.floor(ui.position.top) - $(window).scrollTop())]; $(event.target).parent().css('position', 'fixed'); $(dlg).dialog('option','position',position); } }); $(dlg).html(`<div id="float_cmt_title" style="display: none;">Comment as [user] on [chapter]</div><div id="float_cmt_userinput"> <textarea style="min-height: 8em">${loadCache()}</textarea> <div id="float_cmt_counter" style="font-size: 80%; padding: 0.2em; margin: 0.2em 0;"><span>10000</span> characters left</div></div>`); $('#float_cmt_userinput textarea').on('input', function(e) { whenTextChanges(e.target); }); } // counter and cache: triggered by event and other functions when text in the commentbox changes function whenTextChanges(el) { // calculate remaining characters let cmt = $(el).val(); let rem = 10000 - (cmt.length + cmt.split("\n").length-1); // count like AO3 does: linebreak = 2 chars $('#float_cmt_counter span').text(rem); // warning if we've exceeded allowed characters if (rem<0) $('#float_cmt_counter').addClass('ui-state-error ui-corner-all'); else $('#float_cmt_counter').removeClass('ui-state-error ui-corner-all'); storeCache(); } // shows the dialog function openCommentBox() { // use the position of the dialog $(dlg).dialog('open'); } // hides the dialog function closeCommentBox() { // store the position of the dialog so we can reopen it there after page refresh let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt'))); let pos = $(dlg).dialog( "option", "position" ); pos = { my: pos.my, at: pos.at }; // need to keep only the pieces we need - it's a cyclic object! cachemap.set('pos', JSON.stringify(pos)); localStorage.setItem('floatcmt', JSON.stringify( Array.from(cachemap.entries()) )); $(dlg).dialog('close'); } // takes highlighted text and appends it to the comment function grabHighlight() { // copy highlighted text works only on summary, notes, and fic if ($(window.getSelection().anchorNode).parents(".userstuff").length > 0) { let area = $('#float_cmt_userinput textarea'); let highlighted = `<blockquote>${window.getSelection().toString().trim()}</blockquote>`; $(area).val($(area).val() + highlighted); // insert new text at the end whenTextChanges(area); // trigger an update for the counter } } // update the stored cache (called on any text change) function storeCache() { let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt'))); // cache is stored per page: path -> text, path-date -> last update date let path = new URL(window.location.href).pathname; // update current values in Map() and localStorage immediately cachemap.set(path, $('#float_cmt_userinput textarea').val()).set(path+"-date", Date.now()); localStorage.setItem('floatcmt', JSON.stringify( Array.from(cachemap.entries()) )); } // on page load, retrieve previously stored cached text function loadCache() { let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt'))); // cache is stored per page: path -> text, path-date -> last update date let path = new URL(window.location.href).pathname; // is cache outdated? we keep it for 1 month to avoid storage limit issues let cachedate = new Date(cachemap.get(path+"-date") || '1970'); let maxdate = createDate(0, -1, 0); if (cachedate < maxdate) deleteCache(path); let cache = cachemap.get(path); if (!cache) return ""; // if there's nothing stored yet for this path else return cache; } // clean up cache for this page function deleteCache() { let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt'))); // cache is stored per page: path -> text, path-date -> last update date let path = new URL(window.location.href).pathname; cachemap.delete(path); cachemap.delete(path+'-date'); localStorage.setItem('floatcmt', JSON.stringify( Array.from(cachemap.entries()) )); } // removes all traces of the comment for this page function discardComment() { $('#float_cmt_userinput textarea').val(""); // resets the textarea to blank whenTextChanges($('#float_cmt_userinput textarea')); // updates the counter accordingly deleteCache(); // deletes the cached data closeCommentBox(); // and hides the dialog } // assemble the form data needed to submit the comment function submitComment() { let pseud_id = $("#add_comment_placeholder input[name='comment[pseud_id]']").val(); // need to pick up the selected pseud let action = $("#add_comment_placeholder form").attr("action"); // already contains work ID // consolidating the fields we need for submitting a comment var fd = new FormData(); fd.set("authenticity_token", $("#add_comment_placeholder input[name='authenticity_token']").val()); fd.set("comment[pseud_id]", pseud_id); fd.set("comment[comment_content]", $(dlg).find('textarea').val()); fd.set("controller_name", "works"); console.log(action, fd); // turn buttons into a loading indicator $(dlg).dialog( "option", "buttons", [{ text: "Posting Comment...", click: function() { return false; } }]); // post the comment and reload the page to show it grabResponse(action, fd); } // actually submit the comment in a POST request async function grabResponse(action, fd) { // post the comment! this uses the Fetch API to POST the form data const response = await fetch(action, { method: "POST", body: fd }); // response might be not OK in case of retry later (427) if (!response.ok) { // show an error to the user $(dlg).dialog( "option", "buttons", [{ text: "Error saving comment!", click: function() { return false; } }]); return false; // stop all processing (comment is still cached) } // eff this, there's no way to get the original redirected location of the POST (which includes the new #comment_id at the end) // so all we can do is look at the response page with comments shown (per the redirected GET) // puzzling together the reponse stream until we have a full HTML page (to avoid another background pageload) let responseBody = ""; for await (const chunk of response.body) { let chunktext = new TextDecoder().decode(chunk); // turns it from uint8array to text responseBody += chunktext; } // find out if there's multiple pages of comments now, based on the comment pagination (pick the last page) let lastpage = $(responseBody).find('#comments_placeholder ol.pagination').first().children().eq(-2).find('a').attr('href'); // if there's no pagination, just use the redirect URL; either way scroll that to the footer lastpage = (lastpage > "") ? lastpage.slice(0, -9)+'#footer' : response.url+'#footer'; discardComment(); // clean up since it's now posted // redirect us to where we're hopefully seeing the comment we just posted window.location.href = lastpage; } })(jQuery); function createDate(days, months, years) { var date = new Date(); date.setFullYear(date.getFullYear() + years); date.setMonth(date.getMonth() + months); date.setDate(date.getDate() + days); return date; }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址