您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Easily move tags from one fandom to another via wrangulator!!
// ==UserScript== // @name AO3: [Wrangling] Boot tags to another fandom! // @description Easily move tags from one fandom to another via wrangulator!! // @version 1.0.1 // @author owlwinter // @namespace N/A // @license MIT license // @match *://*.archiveofourown.org/tags/*/wrangle?*status=unfilterable* // @grant none // ==/UserScript== (function() { 'use strict'; //Gets the edit url of the tag from the checkbox const get_url = function get_url(checkbox) { //If iconify is enabled, this will return the tag's link const a = checkbox.parentElement.parentElement.querySelector("ul.actions > li[title='Edit'] > a"); if (a) { return a.href; } //If iconify is not enabled, we'll use the default path const buttons = checkbox.parentElement.parentElement.querySelectorAll("ul.actions > li > a"); return array(buttons).filter(b => b.innerText == "Edit")[0].href; } //Hides the checked rows after we do our work const delete_tag_row = function delete_tag_row(checkbox) { const row = checkbox.parentElement.parentElement; row.parentElement.removeChild(row); } //Each time we finish a request, we'll add 1 to the "done" count //When it's equal to the number of tags we had to make requests for, we know we gone through all the checked rows! var done; //Holds any errors from form submit var errors; //All tags selected to boot var tags; // Called for each checked tag // Submits form data & passes on any resulting error messages // If succesfully done for every selected tag, will also display success banner :) // `url` is the URL that will process the form submission // `fullfandoms` is an array of all the new fandoms to be added // `tag` is the tag being edited // `fd` is the new formdata const submit_xhr = function submit_xhr(url, fullfandoms, tag, fd) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function xhr_onreadystatechange() { if (xhr.readyState == xhr.DONE ) { //After form submitted: if (xhr.status == 200) { // Check for errors const err = xhr.responseXML.documentElement.querySelector("#error"); if (err) { alert(err.innerText); errors.push(err) } done += 1; //If succesfully done for every selected tag, we want to display a success banner! if (done == tags.length) { // for some reason this seems to always be present on the page, even if there is no content in it var flash = document.getElementsByClassName("flash")[0] flash.innerHTML = ""; flash.classList.add("notice") if (errors.length != 0) { //In case the user wasn't bonked enough for their error, //puts the red box with the error message from the iframe into the flash action message area errors.forEach(e => { e.parentElement.removeChild(e) flash.appendChild(e); }); } else { // happy path :) flash.appendChild(document.createTextNode("The following tags were successfully booted to ")); flash.appendChild(document.createTextNode(fullfandoms.toString().replaceAll(',', ', '))) flash.appendChild(document.createTextNode(": ")); const as = tags.map(checkbox => { const a = document.createElement("a") a.href = get_url(checkbox); a.target = "_blank" a.innerText = checkbox.labels[0].innerText; return a; }); as.forEach((a, i) => { if (i != 0) { flash.appendChild(document.createTextNode(", ")) } flash.appendChild(a); }); tags.forEach(checkbox => delete_tag_row(checkbox)); } flash.appendChild(document.createElement("br")) flash.scrollIntoView(); //Unset explicit background to let it go back to the default CSS background button.style.background = "" button.disabled = false button.innerText = "Boot"; } } else if (xhr.status == 429) { // ~ao3jail alert("Rate limited. Sorry :(") } else { alert("Unexpected error, check the console :(") console.log(xhr) } } } xhr.open("POST", url) xhr.responseType = "document" xhr.send(fd) } // Called for each checked tag // Gets the edit form data about the tag and sets up request that will actually submit // `url` is the url of the edit tag page // `newfandoms` is an array of all the new fandoms to be added const boot_xhr = function boot_xhr(url, newfandoms) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function xhr_onreadystatechange() { if (xhr.readyState == xhr.DONE ) { if (xhr.status == 200) { //Fandoms the tag has pre-boot const oldfandoms = [] const inner_input = xhr.responseXML.documentElement.querySelector("input[name='tag[syn_string]']") const tag = inner_input.value; const form = xhr.responseXML.documentElement.querySelector("#edit_tag") //Removing all fandoms the tag has pre-boot const checks = array(xhr.responseXML.documentElement.querySelectorAll("label > input[name='tag[associations_to_remove][]']")).filter(foo => foo.id.indexOf("parent_Fandom_associations_to_remove") != -1); for (const check of checks) { const name = check.parentElement.nextElementSibling.innerText oldfandoms.push(name) //Edge case - if user boots a tag to a fandom currently on the tag if (!newfandoms.includes(name)) { check.checked = true; } } //Setting the new fandoms on the tag xhr.responseXML.documentElement.querySelector("#tag_fandom_string").value = newfandoms; console.log("Tag at url " + url + " booted from " + oldfandoms.toString() + " and booted to " + newfandoms) const fd = new FormData(form); //Second xhr call that will submit our edited data to the form submit_xhr(form.action, newfandoms, tag, fd); } else if (xhr.status == 429) { // ~ao3jail alert("Rate limited. Sorry :(") } else { alert("Unexpected error, check the console :(") console.log(xhr) } } } xhr.open("GET", url) xhr.responseType = "document" xhr.send() } //Runs when the boot button is clicked const boot_fandoms = function boot_fandoms(e) { e.preventDefault() //Gets all the rows with selected tags tags = array(document.querySelectorAll("input[name='selected_tags[]']")).filter(inp => inp.checked); //If no tags are selected, bonk user >:( if (tags.length == 0) { alert("You need to select at least one tag to boot!") return; } //The fandoms checked via the Fandom Assignment Shortcut script, if it is installed const fandomschecked = array(document.querySelectorAll("input[name='fandom_shortcut']:checked")).map(f => f.value) //The fandoms added via wrangulator textbox const fandomstext = document.getElementById("fandom_string").value.split(',') //Combine both fandom lists, then remove empty elements const fullfandoms = fandomschecked.concat(fandomstext).filter(n => n) //If no fandoms are selected, bonk user >:( if (fullfandoms.length == 0) { alert("You need to select at least one fandom to boot to!") return; } //Each time we finish a request, we'll add 1 to "done", and when it's equal to the number of tags we had to make requests for, we know we have finished them all done = 0; errors = [] button.disabled = "true"; button.innerText = "Processing..." // There is no special style in the default ao3 styles for disabled buttons, so even when disabled, the button still looks clickable. This sets the background to // "lightgrey" instead of the default light -> dark gradient, to make it look more disabled. We undo this later by setting this property back to an empty string // after all our requests are done. button.style.background = "lightgrey"; //Runs hard boot xhr call on each tag selected tags.forEach(checkbox => { const url = get_url(checkbox) boot_xhr(url, fullfandoms) }) } // 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) //Creates boot button const button = document.createElement("button"); button.type = "text"; button.style.textAlign = "center" button.style.marginRight = "5px" button.textContent = "Boot"; button.addEventListener("click", boot_fandoms) document.querySelector("dd.submit").prepend(button); // Your code here... })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址