AO3: [Wrangling] Count Wranglers on Subfandoms

On a fandom tag's Landing Page, writes out how many wranglers are currently assigned to the listed subfandoms

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AO3: [Wrangling] Count Wranglers on Subfandoms
// @namespace    https://greasyfork.org/en/users/906106-escctrl
// @description  On a fandom tag's Landing Page, writes out how many wranglers are currently assigned to the listed subfandoms
// @author       escctrl
// @version      1.1
// @match        *://*.archiveofourown.org/tags/*
// @exclude      *://*archiveofourown.org/tags/*/wrangle*
// @exclude      *://*archiveofourown.org/tags/*/edit
// @exclude      *://*archiveofourown.org/tags/*/comments*
// @exclude      *://*archiveofourown.org/tags/*/search*
// @exclude      *://*archiveofourown.org/tags/*/troubleshooting*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js
// @license      MIT
// ==/UserScript==

(function($) {
    'use strict';

    const DEBUG = true;

    // wait duration between checking individual bins - set this number higher if you often run into Retry Later
    //   this applies especially to huge fandom trees like DCU!! 3sec is NOT enough for the full list! make this number huge and let it run in the background
    //   defined in milliseconds, e.g. 3000 = 3 seconds
    const interval = 5000;

    // reasons not to run this script: the tag isn't a fandom metatag, or tag doesn't have subtags to check
    const subfandoms = document.getElementsByClassName('sub')[0];
    if ($('div.parent.fandom').length == 0) { if (DEBUG) console.log('Count Wranglers on Subfandoms script says: this is not a fandom tag'); return true; }
    if (subfandoms == null) { if (DEBUG) console.log('Count Wranglers on Subfandoms script says: fandom doesn\'t have subfandoms'); return true; }

    // resetting from any previous page loads so the fun can start
    localStorage.removeItem("ao3jail");
    localStorage.setItem("ao3jail", "false");

    // some code to add *) span for progress bar/report global results, *) button to start the checking
    $(subfandoms).children('h3.heading').before(`<span id="countwranglers-unwrangled" style="display: none;">0</span>
                                                 <span id="countwranglers-results" style="float:right;"><button id="countwranglers">Count Wranglers</button></span>`);

    // assign the function to the click event (will trigger only once user clicked the link)
    $(subfandoms).find('button#countwranglers').click(countWranglers);

    // Checks if using a dark mode by calculating the 'brightness' of the page background color
    const darkmode = lightOrDark(window.getComputedStyle(document.body).backgroundColor) == "dark" ? true : false;

    // a function that gets run on button push
    function countWranglers() {

        // replace button with progress message
        var progress = document.getElementById('countwranglers-results');
        $(progress).css("padding", "0.3em 0.25em");
        $(progress).html('Counting wranglers... grab a drink, this may take a while!');

        // find all the subfandoms
        var fandoms = $(subfandoms).find('ul.tree a');

        // placeholder next to each fandom showing TBD until the fandom was checked
        $(fandoms).after('<span style="font-size: smaller;"> <em>... wranglers TBD</em></span>');

        // loop through the unwrangled bins
        $(fandoms).each(function(i, fandom) {

            // set a delay between bin checks to reduce chances of jail
            setTimeout(function() {

                // if previous loops hit Ao3 Jail, don't try looking for wranglers anymore
                if ( localStorage.getItem("ao3jail") == "true") {
                    console.log('previously received "Retry later" response, skipping');
                    return false;
                }

                var assignedCount = 0;
                var unwrangledTotal = document.getElementById('countwranglers-unwrangled');

                // retrieve the data from the fandom tag's edit page
                $.get(fandom.href + '/edit', function(response) {

                }).done(function(response) {
                    // find the field containing the assigned wranglers
                    var assignedWranglers = $(response).find('#edit_tag fieldset:first dd:nth-of-type(4)').text();
                    var unwrangledCount = parseInt(unwrangledTotal.innerText);

                    // unwrangled fandoms have a Sign Up link here
                    if (assignedWranglers == 'Sign Up') {
                        // count up the global total of unwrangled subfandoms
                        unwrangledCount++;
                        unwrangledTotal.innerText = unwrangledCount.toString();

                        // update the fandom tag with that information and put in an Edit & Wrangle child tags button for convenience
                        $(fandom).next().html(' ... unwrangled [<a href="'+ fandom.href + '/edit">Edit</a>] [<a href="'+ fandom.href + '/wrangle">Wrangle</a>]');

                        // highlight the fandom tag to make it easily findable in a long list
                        if (darkmode) { $(fandom).parent().css("background-color", "rgb(160, 82, 45)"); }
                        else { $(fandom).parent().css("background-color", "rgb(255, 255, 133)"); }
                    }
                    // assigned wranglers are plaintext
                    else {
                        // split the a comma-seperated list into an array and count its length (that's the number of wranglers)
                        assignedCount = assignedWranglers.split(', ').length;

                        // update the fandom tag with that information
                        $(fandom).next().html(' ... '+ assignedCount +' wrangler' + ((assignedCount > 1) ? "s" : ""));
                    }

                    // if last loop also succeeded, update global progress with "all wrangled" or "# unwrangled"
                    if (fandoms.length == i+1)
                        $(progress).html((unwrangledCount == 0) ? 'all subfandoms are wrangled \\o/' : unwrangledCount + ' subfandoms unwrangled');

                // thanks to Przemysław Sienkiewicz on Stackoverflow for the code to catch error responses https://stackoverflow.com/a/40256829
                // do other stuff on fail (set the ao3jail marker)
                }).fail(function(data, textStatus, xhr) {
                    localStorage.setItem("ao3jail", "true"); // store it so next AJAX can skip
                    $(progress).html('Wrangler Count has hit "Retry later", sorry!');
                    //This shows status code eg. 429
                    console.log("Wrangler Count has hit Retry later", data.status);
                    return false;
                });
            // the each() loops immediately and creates all timeout calls (async) at once, so we need to stagger them
            // by multiplying the 3s delay by the loop number (bin #0 = 3000*0, bin #1 = 3000*1, bin #2 = 3000*2, etc)
            }, interval*i);
        }); // end each
    } // end function
})(jQuery);


// helper function to determine whether a color (the background in use) is light or dark
// https://awik.io/determine-color-bright-dark-using-javascript/
function lightOrDark(color) {

    // Variables for red, green, blue values
    var r, g, b, hsp;

    // Check the format of the color, HEX or RGB?
    if (color.match(/^rgb/)) {

        // If RGB --> store the red, green, blue values in separate variables
        color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);

        r = color[1];
        g = color[2];
        b = color[3];
    }
    else {

        // If hex --> Convert it to RGB: http://gist.github.com/983661
        color = +("0x" + color.slice(1).replace(
        color.length < 5 && /./g, '$&$&'));

        r = color >> 16;
        g = color >> 8 & 255;
        b = color & 255;
    }

    // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
    hsp = Math.sqrt(
    0.299 * (r * r) +
    0.587 * (g * g) +
    0.114 * (b * b)
    );

    // Using the HSP value, determine whether the color is light or dark
    if (hsp>127.5) { return 'light'; }
    else { return 'dark'; }
}