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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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'; }
}