// ==UserScript==
// @name AO3: [Wrangling] Mark Co- and Solo-Wrangled Fandoms
// @author escctrl
// @description On your wrangling homepage, mark whether the fandoms are co- or solo-wrangled. Refreshes once a month.
// @namespace https://greasyfork.org/en/users/906106-escctrl
// @version 3.2
// @license MIT
// @match *://*.archiveofourown.org/tag_wranglers/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
// @grant none
// ==/UserScript==
/****************** CONFIGURATION ******************/
// this enables the script to change the CSS classes, which Redux and N-in-1 Filtering rely on
// sadly this works only partially for N-in-1, because it can't make filters appear which are created dynamically
// if setting this to true, make sure that this script runs AFTER the filtering script!
const FILTERING = false;
// set this to true if you don't want to see the icon indicating co/solo wrangled fandoms
// filtering would still work, even if the icon is hidden
const HIDE_MARKERS = false;
// supervisors: change these if you only want to use this script to help during the trainee checkins
const ENABLE_ON_OTHER_USERS = false; // true = enables a button to check cowrangling on other users' wrangling homepages (not stored)
const ENABLE_ON_MY_PAGE = true; // false = disables the script on your own wrangling homepage
// ****************** NOTE ON LOCAL STORAGE ******************
// For compatibility between userscript managers, this script uses local storage, which is visible from the Developer console.
// If you ever uninstall the script, unfortunately its data can't be automatically deleted.
// If you want to remove the data it sets, (1) visit your wrangling homepage, (2) go into the Developer console,
// (3) look for the Local Storage (4) and delete the entries for "aia_ref" and "aia_refdate".
// The script also removes its data if you switch DELETESTORAGE below to true, and visit your wrangling homepage.
const DELETESTORAGE = false;
(function($) {
'use strict';
// wait duration between checking individual bins - set this number higher if you often run into Retry Later
// defined in milliseconds, e.g. 3000 = 3 seconds
const interval = 3000;
const DEBUG = false;
if (DELETESTORAGE || !ENABLE_ON_MY_PAGE) {
// if you want to laugh: "aia" stands for "am i alone" wrangling this fandom?
localStorage.removeItem("aia_refdate");
localStorage.removeItem("aia_ref");
if (DELETESTORAGE) return;
}
// Am I looking at my own page?
var MYOWNPAGE =
window.location.pathname.match(/\/([A-Za-z0-9_]+)\/?$/i)[1] ==
$('#greeting>ul.user.navigation>li:first-of-type>a[href^="/users/"]')[0].href.match(/\/([A-Za-z0-9_]+)\/?$/i)[1]
? true : false;
if (DEBUG) console.log(MYOWNPAGE);
// as always, a list of available icons is here --> https://fontawesome.com/v4/icons
const icons = { 'co': "", 'solo': "", 'load': "", 'dunno': "" };
const title = { 'co': "co-wrangled fandom", 'solo': "solo-wrangled fandom", 'load': "fandom wranglers loading", 'dunno': "fandom wranglers not yet checked" };
$("head").append(`<style type="text/css"> .aia-check { font-family: FontAwesome, sans-serif; min-width: 1.2em;
display: inline-block; text-align: center; padding-right: 0.2em; } </style>`)
.prepend(`<script src="https://use.fontawesome.com/ed555db3cc.js" />`);
const fandomList = $('.assigned tbody tr th a'); // the full list of fandoms
var fandomRef = new Map(); // here we'll build/load the reference list of fandoms and co-wrangling status
if (DEBUG) console.log(fandomList);
// if this is wrangler's own page, immediately start the process as the page loads
if (MYOWNPAGE && ENABLE_ON_MY_PAGE) {
const button_text = 'Co/Solo: force reload';
$('.assigned h3').after('<p id="aia" style="font-size: 0.7em; display: inline-block;"><button id="aia-start">' + button_text + '</button></p>');
$('#aia-start').click(forceRefresh);
fullPageReload();
}
// on other people's pages, add a button to start the check
else if (!MYOWNPAGE && ENABLE_ON_OTHER_USERS) {
const button_text = 'Co/Solo: load';
$('.assigned h3').after('<p id="aia" style="font-size: 0.7em; display: inline-block;"><button id="aia-start">' + button_text + '</button></p>');
$('#aia-start').click(fullPageReload);
}
function forceRefresh() {
localStorage.removeItem("aia_refdate");
localStorage.removeItem("aia_ref");
fullPageReload();
}
function fullPageReload() {
// markers initiation
fandomList.before(`<span class="aia-check" title="${title.load}"` + (HIDE_MARKERS ? ' style="display: none;"' : "") + `>${icons.load}</span>`);
if (MYOWNPAGE) {
// check if data is still recent enough
var stored_date = new Date(localStorage.getItem("aia_refdate") || '1970'); // the date when the storage was last refreshed
var compare_date = createDate(0, -1, 0); // a month before
if (DEBUG) console.log(stored_date, compare_date);
if (stored_date > compare_date) fandomRef = new Map(JSON.parse(localStorage.getItem("aia_ref")));
if (DEBUG) console.log(fandomRef);
}
// if not on own page or data outdated, the fandomRef remains empty
// resetting from any previous page loads so the fun can start
localStorage.setItem("ao3jail", "false");
// keep track of fandoms to be checked
var todo = 0;
// run through all fandoms on page
$(fandomList).each( function(i, f) {
// if the given fandom's part of the stored data, update the status display
if (fandomRef.has(f.innerText)) {
if (DEBUG) console.log(f.innerText +' was found in storage, status '+ fandomRef.get(f.innerText));
writeMarker(f, fandomRef.get(f.innerText));
}
// if not in storage, create a background pageload for assigned wranglers
else {
todo++;
if (DEBUG) console.log(f.innerText +' was not found in storage, page load in '+ interval*todo +'ms');
setTimeout(function() {
loadFandom(f);
}, interval*todo);
}
});
}
function loadFandom(a) {
// if previous loops hit Ao3 Jail, don't try checking age anymore
if ( localStorage.getItem("ao3jail") == "true") {
console.log('previously received "Retry later" response, skipped '+ a.innerText +' cowrangler check');
return false;
}
if (DEBUG) console.log('Check started for '+ a.innerText);
// turn the url from a /wrangle into an /edit and load the page
var url = a.href;
if (url.endsWith("/")) url = url.slice(0, -1);
$.get(url.slice(0, -7) + 'edit', function(response) {
}).done(function(response) {
// count the wranglers:
// pick the correct field in the form containing the assigned wranglers
// split the a comma-seperated list into an array and count its length (that's the number of wranglers)
var assignedWranglers = $(response).find('#edit_tag fieldset:first dd:nth-of-type(4)').text().split(', ').length;
assignedWranglers = (assignedWranglers == 1) ? "solo" : "co";
fandomRef.set(a.innerText, assignedWranglers);
if (DEBUG) console.log(a.innerText + ' is '+ assignedWranglers + '-wrangled');
if (MYOWNPAGE) {
localStorage.setItem('aia_ref', JSON.stringify(Array.from(fandomRef.entries())));
localStorage.setItem('aia_refdate', new Date())
}
writeMarker(a, assignedWranglers);
}).fail(function(data, textStatus, xhr) {
//This shows status code eg. 429
console.log("error", data.status);
writeMarker(a, 'dunno'); // set ? as result
});
}
// update the marker with the appropriate icon
function writeMarker(f, status) {
if (FILTERING) {
// Redux uses "shared-", n-in-1 uses "co-" (but both use "solo-") as prefixes
var cofilter = ($('p#fandom-filter').length > 0) ? "co-" : "shared-";
var classes = $(f).parent().parent().prop("classList");
// replace the prefixes in class names for filtering
switch (status) {
case 'solo':
classes.forEach((v, i) => {
if (v.startsWith(cofilter)) classes.replace(v, v.replace(cofilter, 'solo-'));
});
break;
case 'co':
classes.forEach((v, i) => {
if (v.startsWith('solo-')) classes.replace(v, v.replace('solo-', cofilter));
});
break;
default:
status = 'dunno';
break;
}
}
$(f).prev().html(icons[status]).attr('title', title[status]);
}
})(jQuery);
// convenience function to be able to pass minus values into a Date, so JS will automatically shift correctly over month/year boundaries
// thanks to Phil on Stackoverflow for the code snippet https://stackoverflow.com/a/37003268
function createDate(days, months, years) {
var date = new Date();
date.setFullYear(date.getFullYear() + years);
date.setMonth(date.getMonth() + months);
date.setDate(date.getDate() + days);
return date;
}