您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mass-select tags in the bin and set/unset the unwrangleable flag, replace fandoms, or remove 0-use syns
当前为
// ==UserScript== // @name AO3: [Wrangling] Bulk-Manage Tags for Clean-Up Projects // @namespace https://gf.qytechs.cn/en/users/906106-escctrl // @description Mass-select tags in the bin and set/unset the unwrangleable flag, replace fandoms, or remove 0-use syns // @author escctrl // @version 4.0 // @match *://*.archiveofourown.org/tags/*/wrangle?* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js // @license MIT // ==/UserScript== (function($) { 'use strict'; const binParams = new URLSearchParams(window.location.search); const binStatus = binParams.has('status') ? binParams.get('status') : ""; // bow out gracefully if we're not wrangling in a fandom bin (but on a char/rel/freeform tag) if ($('div#dashboard.tag.wrangling ul.navigation.actions:last-of-type li').length < 5) return false; // bow out gracefully if this is a mergers or subtags bin on a fandom, to avoid accidents (userscript @exclude isn't trustworthy enough in Tampermonkey) if (binParams.get('show') == "mergers" || binParams.get('show') == "sub_tags") return false; // build the action buttons (above and below the tag table) var labelManage = binStatus == "unfilterable" ? "Set Unwrangleable" : binStatus == "unwrangleable" ? "Remove Unwrangleable" : binStatus == "synonymous" ? "De-syn 0 use tags" : binStatus == "canonical" ? "Change Case/Diacritic" : ""; const labelYeet = "Replace Fandoms"; if (labelManage != "") { // only create the Manage button on UF/UW-able/Syn const buttonManage = `<input id="massManage" name="massManage" type="submit" style="padding-left: 0.75em;" value="${labelManage}"> `; $('#wrangulator p.submit.actions').prepend(buttonManage); } if (binStatus != "unwrangled") { // Yeeting is supported on all pages except UW (because it's pointless there) const buttonYeet = `<input id="massYeet" name="massYeet" type="submit" style="padding-left: 0.75em;" value="${labelYeet}"> `; $('#wrangulator p.submit.actions').prepend(buttonYeet); } // helper vars need to be available globally for my sanity var errorMsg = []; var completeTags = 0; var selectedTags = []; var selectedFandoms = []; var bgAction = ""; var oldText = ""; var newText = ""; // add the event listener to the buttons with function to load the Edit pages $('input#massManage,input#massYeet').click(function(e) { // stop button from automatically submitting the form it's in e.preventDefault(); // reset counters between button clicks completeTags = 0; errorMsg = []; // which button has been clicked? that decides what we're doing in the background bgAction = $(this).attr("id"); // disable both buttons so they can't be clicked again while a background process is running $('input#massManage,input#massYeet').attr("disabled",true); $('input#'+bgAction).attr("value", 'running...'); // grab the selected tags selectedTags = array($('input[name="selected_tags[]"]')).filter(ckbx => ckbx.checked); selectedFandoms = array($('#fandom_string').prev().find('li.added.tag')); // check that we have the necessary info to start working if (selectedTags.length < 1) { alert("Please select at least one tag!"); return false; } if (bgAction == "massYeet" && selectedFandoms.length < 1) { alert("Please select at least one fandom to yeet to!"); return false; } // now let's run through all the tags and update them $(selectedTags).each(function(i, tag) { // quick check before we create background page loads: removing synonyms should only work for 0-use tags if (binStatus == "synonymous" && bgAction == "massManage") { const tagUses = $(tag).closest('tr').find('td[title="taggings"]')[0].innerText; if (parseInt(tagUses) > 0) { errorMsg[i] = "Tag is not zero uses, skipped de-synning"; bgEditComplete(); return; } } // if we're replacing text parts in the tag name, asking for the text that needs to be replaced else if (binStatus == "canonical" && bgAction == "massManage") { oldText = prompt("Enter the old/incorrectly formatted text:"); newText = prompt("Remember: only cases and diacritics can be changed. Enter the properly formatted text:"); } // grab the URL for the Edit Page that we'll load in an iframe var url = $(tag).closest('tr').find('a[href$="/edit"]')[0]; // nifty jQuery, will work with icon action buttons and without url = url.href; // jQuery would return the relative URL, but we need the absolute // create the iframe, hide it, add it to the DOM, and attach an event listener to make the desired changes // we're routing through: the iFrame we're operating in, and the index of the tag that's being changed const tagFrame = document.createElement("iframe"); $(tagFrame).hide().appendTo('body').one('load', function() { bgEditLoad(tagFrame, i); }); // .one() runs only once and removes itself tagFrame.src = url; // at last, we let the edit page load }); }); // event listener function to do the actual Edit page changes in the iframe function bgEditLoad(tagFrame, iTag) { const frameContent = $(tagFrame).contents(); // need to pick up on ao3jail. since the load event always triggers on iFrames, and HTTP responses are impossible to get a hold of, we'll just look at the page content... if ($(frameContent).find('#edit_tag').length != 1) { errorMsg[iTag] = "Page could not load. Retry later"; bgEditComplete(tagFrame); return; } // if we're mass-managing Syns or Unwrangleable flags if (bgAction == "massManage") { switch (binStatus) { case "synonymous": // remove the synonymous tag $(frameContent).find('#tag_syn_string').prev().find("li.added span.delete a")[0].trigger("click"); break; case "unwrangleable": case "unfilterable": { // switch the state of the unwranglable checkbox in the iframe const fieldUnwrangleable = $(frameContent).find('#tag_unwrangleable')[0]; fieldUnwrangleable.checked = fieldUnwrangleable.checked == true ? false : true; break; } case "canonical": { // get the tag name let fieldTagname = $(frameContent).find("#tag_name")[0]; // replace the old text part (regex-excaped and case insensitive!) with the new text part oldText = oldText.replace(/[/.*+?^${}()|[\]\\]/g, '\\$&') fieldTagname.value = fieldTagname.value.replace(new RegExp(oldText, "ig"), newText); break; } default: break; } } // if we're resetting fandoms to move tags else { // if we check the checkbox & add the same fandom in the input field, it still gets removed // so we have to be smarter and check only boxes on fandoms we don't want anymore // first we create an array that only includes the the names of the wanted fandoms const workingFandoms = []; $(selectedFandoms).each((i, sel) => { workingFandoms.push(sel.childNodes[0].textContent.trim()); }); $(frameContent).find('#parent_Fandom_associations_to_remove_checkboxes a.tag').each((j, fandom) => { // first we find out if the existing fandom needs to be kept var keep = workingFandoms.indexOf(fandom.innerText.trim()); // then we're removing (ticking) anything that's on the tag but not wanted if (keep < 0) { var ckbx = $(fandom).prev().find('input')[0]; ckbx.checked = true; } // and kicking out any fandoms we want to keep, that are already set on the tag else workingFandoms.splice(keep, 1); }); // finally we're adding all fandoms that are left to be added const fieldFandom = $(frameContent).find('input#tag_fandom_string_autocomplete')[0]; fieldFandom.focus(); const ke = new KeyboardEvent('keydown', { keyCode: 13, key: "Enter" }); $(workingFandoms).each((j, fandom) => { fieldFandom.value = fandom; fieldFandom.dispatchEvent(ke); }); } // add another event listener to retrieve the fail/success message, and submit the iframe $(tagFrame).one('load', function() { bgEditSubmit(tagFrame, iTag); }); $(tagFrame).contents().find('#edit_tag')[0].submit(); } // event listener function to catch errors occurring in the iframe after submitting the form function bgEditSubmit(tagFrame, iTag) { // need to pick up on ao3jail. since the load event always triggers on iFrames, and HTTP responses are impossible to get a hold of, we'll just look at the page content... if ($(tagFrame).contents().find('#edit_tag').length != 1) { errorMsg[iTag] = "Page could not load. Retry later"; } // tracking any errors we've run into else { const frameContent = $(tagFrame).contents(); const err = $(frameContent).find('#error'); if (err.length > 0) { errorMsg[iTag] = err[0].innerHTML; } else if (binStatus == "synonymous" && bgAction == "massManage") { // also track if de-syn didn't result in tag being ready to rake (tagset tag) // retrieve the numbers showing in the sidebar. we only need the last one (total taggings) // we grab the text, match only the number in it, convert it from string to int const taggings = parseInt($(frameContent).find('#dashboard.tag.wrangling ul:nth-of-type(2) li:last-of-type span').text().match(/\d+/g)); if (taggings == 0 && $(frameContent).find('select#tag_type').length === 0) { errorMsg[iTag] = "Tagset tag, will not rake. Consider synning again."; } } } bgEditComplete(tagFrame); } function bgEditComplete(tagFrame = false) { if (tagFrame !== false) document.body.removeChild(tagFrame); // if this was the last tag, we can write out errors on the bin's page // note: there are no iframes open anymore completeTags++; if (completeTags == selectedTags.length) { printResults(); } } // write out all errors that occurred and all tags that were changed successfully function printResults() { const successMsg = []; const failMsg = []; $(selectedTags).each(function(i, tag) { var tagRow = $(tag).closest('tr'); var tagURL = $(tagRow).find('a[href$="/edit"]')[0]; tagURL = tagURL.href; var tagName = $(tagRow).find('th label')[0]; tagName = tagName.innerText; // errorMsg[] has the corresponding indices so we know which tag failed if (errorMsg[i] != undefined) { failMsg.push('<a href="' + tagURL + '">' + tagName + '</a>: ' + errorMsg[i]); } else { successMsg.push('<a href="' + tagURL + '">' + tagName + '</a>'); // remove the changed tags from the bin page if (binStatus == "unwrangleable" || binStatus == "unfilterable" || binStatus == "synonymous") $(tagRow).remove(); } }); // constructing the response message to the user and showing it const title = bgAction == "massYeet" ? "Fandom" : binStatus == "synonymous" ? "Synonym of" : binStatus == "canonical" ? "Case/Diacritics" : "Unwrangleable flag"; if (failMsg.length > 0) { const fail = '<div id="error" class="error">The following tags\' '+ title +' could not be updated or ran into other issues:<br />' + failMsg.join('<br />') + '</div>'; $(fail).prependTo('#main'); window.scrollTo(0,0); } if (successMsg.length > 0) { const success = '<div class="flash notice">The following tags\' '+ title +' was updated successfully:<br />' + successMsg.join(', ') + '</div>'; $(success).prependTo('#main'); window.scrollTo(0,0); } // reset both buttons so they can be used again $('input#massManage').attr("disabled",false).attr("value", labelManage); $('input#massYeet').attr("disabled",false).attr("value", labelYeet); } // the return value of document.querySelectorAll is technically a "NodeList", which can be indexed like an array, but // doesn't have helpful functions like .map() or .forEach(). So this is a simple helper function to turn a NodeList // (or any other array-like object (indexed by integers starting at zero)) into an array const array = a => Array.prototype.slice.call(a, 0); })(jQuery);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址