// ==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.0
// @match *://*.archiveofourown.org/tags/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license MIT
// ==/UserScript==
(function($) {
'use strict';
// 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: we're not on a tag landing page
// seriously, why do @excludes not work in Tampermonkey, it's annoying
const page_url = window.location.pathname;
if (page_url.endsWith('/search') || page_url.endsWith('/edit') || page_url.endsWith('/wrangle') || page_url.endsWith('/comments'))
{ console.log('Count Wranglers on Subfandoms script says: this is not a tag landing page'); return true; } // bow out gracefully if this isn't a tag landing page
// reasons not to run this script: the tag isn't a fandom metatag
// console output is mostly for debugging purposes tbh, since we're relying on funky CSS classes being present on the page to recognize if this is a fandom tag
const subfandoms = document.getElementsByClassName('sub')[0];
if ($('div.parent.fandom').length == 0) { console.log('Count Wranglers on Subfandoms script says: this is not a fandom tag'); return true; } // bow out gracefully if we're not looking at a fandom
if (subfandoms == null) { console.log('Count Wranglers on Subfandoms script says: fandom doesn\'t have subfandoms'); return true; } // bow out gracefully if the fandom has no subfandoms
// some code to add *) hidden span to track ao3jail *) span for progress bar/report global results, *) button to start the checking
$(subfandoms).children('h3.heading').before('<span id="countwranglers-jail" style="display: none;"></span>' +
'<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() {
// checking that the click event actually worked
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');
// immediately add a span placeholder next to each fandom for the # of wranglers, show TBD until the fandom was checked
// this is basically our progress bar while the script slowly works down the fandom tree
$(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 ( document.getElementById('countwranglers-jail').innerText == "true") {
console.log('previously received "Retry later" response, skipped check on subfandom');
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) {
// nothing to do here, all interactions are in done() and failed()
// do stuff on success (write out how many wranglers are on a fandom tag, add an Edit link when 0, and count global unwrangled's up)
}).done(function(response) {
// pick the correct field in the form 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) {
if (unwrangledCount == 0) {
$(progress).html('all subfandoms are wrangled \\o/');
}
else {
$(progress).html(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) {
// update user on the issue
$(progress).html('Wrangler Count has hit "Retry later", sorry!');
document.getElementById('countwranglers-jail').innerText = "true"; // set it in DOM so next delayed loops can skip
//This shows status code eg. 429
console.log("Wrangler Count has hit Retry later", data.status);
//This shows status message eg. Too Many Requests
//console.log("bin #"+i+" STATUS: "+xhr);
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'; }
}