Help find real crossovers when fandoms have overlapping fandom tags.
// ==UserScript==
// @name AO3: True Crossover Filter
// @author Quihi
// @version 1.0
// @namespace https://greasyfork.org/en/users/812553-quihi
// @icon https://icons.duckduckgo.com/ip2/archiveofourown.org.ico
// @description Help find real crossovers when fandoms have overlapping fandom tags.
// @license MIT
// @match https://archiveofourown.org/works?*
// @match https://archiveofourown.org/tags/*/works*
// @run-at document-start
// ==/UserScript==
// the goal of this script is to hide from the page, fics which AO3 may count as crossovers but are not actually
// crossovers, because the tags are overlapping, e.g. various media types like a manga and anime.
("use strict");
document.addEventListener("DOMContentLoaded", () => {
const mainFandomName = document.querySelector("#main > .heading > .tag").textContent;
const mainFandomNameKey = encodeURI(mainFandomName);
const scriptStorage = localStorage.getItem("crossoverScript") ? JSON.parse(localStorage.getItem("crossoverScript")) : {};
const storedSameFandoms = scriptStorage[mainFandomNameKey] ? scriptStorage[mainFandomNameKey] : [];
let hiddenFics = 0;
// Create button to open/close menu
function createOptionsButton() {
const navigationBar = document.querySelector("div.navigation.actions");
const optionsButton = document.createElement("ul");
optionsButton.id = "crossoverScript-options-button";
optionsButton.innerHTML = `<li><a href="#crossover-options">Crossover Helper Options</a></li>`;
navigationBar.prepend(optionsButton);
optionsButton.addEventListener("click", (event) => toggleMenu(event));
}
// Create an indicator for if the script is active and things are currently being hidden
function updateIndicator() {
let indicator = document.getElementById("crossoverScript-indicator");
if (indicator === null) {
indicator = document.createElement("span");
indicator.id = "crossoverScript-indicator";
document.getElementById("crossoverScript-options-button").after(indicator);
}
indicator.innerHTML = `${storedSameFandoms.length} 🔁 / ${hiddenFics} 🛑`;
indicator.title = `${storedSameFandoms.length} fandom${storedSameFandoms.length === 1 ? " is" : "s are"} the same; ${hiddenFics} work${hiddenFics === 1 ? " is" : "s are"} hidden from this page`;
}
// Create a menu to let people select which fandoms should not count as crossovers
// Buttons to select the top ten fandoms based on AO3 filters
// Text entry field so people can add their own fandoms
function createMenu() {
const crossoverMenuWrapper = document.createElement("div");
crossoverMenuWrapper.id = "crossoverScript-menu-wrapper";
crossoverMenuWrapper.addEventListener("click", (event) => {
// use if condition to avoid bubbling from child elements
if (event.target === event.currentTarget) {
toggleMenu(event);
}
});
document.body.append(crossoverMenuWrapper);
const colorAccent = window.getComputedStyle(document.querySelector('nav[aria-label="Site"] > ul'))["background-color"];
const colorBG = window.getComputedStyle(document.getElementsByTagName("fieldset")[1])["background-color"];
const colorFont = window.getComputedStyle(document.getElementsByTagName("fieldset")[1])["color"];
const optionsMenuStyle = document.createElement("style");
optionsMenuStyle.textContent = `
#crossoverScript-menu-wrapper {
--xover-script-color-accent: ${colorAccent};
--xover-script-color-bg: ${colorBG};
--xover-script-color-text: ${colorFont};
position: fixed;
top: 0px;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
background-color: color-mix(in srgb, var(--xover-script-color-bg, black) 65%, color(srgb 0 0 0 / 0));
z-index: 9999;
left: 0;
opacity: 1;
display: none;
}
#crossoverScript-menu-wrapper.active {
display: flex;
}
#crossoverScript-menu {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
background: var(--xover-script-color-bg, black);
color: var(--xover-script-color-text, grey);
padding: 1rem;
border: 2px solid color-mix(in srgb, var(--xover-script-color-accent, red) 65%, black);
border-radius: 0.3rem;
max-height: 90vh;
overflow: auto;
}
#crossoverScript-indicator {
padding-left: 1em;
}`;
const crossoverMenu = document.createElement("div");
crossoverMenuWrapper.append(optionsMenuStyle, crossoverMenu);
crossoverMenu.id = "crossoverScript-menu";
crossoverMenu.innerHTML = "<h1>Crossover Helper Settings</h1>";
crossoverMenu.innerHTML += `<p>You are on the page for: ${mainFandomName}</p>`;
crossoverMenu.innerHTML += "<p><strong>Which fandoms do you consider the same?</strong></p>";
const fandomChecklist = document.createElement("ul");
fandomChecklist.id = "crossoverScript-menu-checklist";
fandomChecklist.classList.add("filters");
crossoverMenu.append(fandomChecklist);
const topTenFandoms = document.querySelectorAll("#include_fandom_tags label > span:last-child");
topTenFandoms.forEach((fandom) => {
const fandomName = fandom.textContent.replace(/(.*) \(\d+\)/, "$1");
if (fandomName !== mainFandomName) {
const fandomChecklistItem = document.createElement("li");
fandomChecklistItem.innerHTML = `<label><input type="checkbox"><span class="indicator" aria-hidden="true"></span>${fandomName}</label>`;
fandomChecklist.append(fandomChecklistItem);
}
});
// fill in checkboxes with which are already checked from local storage
// add items to list which are in local storage but not the top ten
const pageProvidedFandomList = Array.from(document.querySelectorAll("#crossoverScript-menu-checklist input"));
storedSameFandoms.forEach((fandom) => {
const isFound = pageProvidedFandomList.some((providedFandom) => {
if (providedFandom.parentElement.textContent === fandom) {
providedFandom.checked = true;
return true;
}
return false;
});
if (!isFound) {
const newListItem = document.createElement("li");
newListItem.innerHTML = `<label><input type="checkbox" checked="true"><span class="indicator" aria-hidden="true"></span>${fandom}</label>`;
fandomChecklist.append(newListItem);
}
});
// add free text entry box
const customCrossoverEntryBox = document.createElement("div");
customCrossoverEntryBox.style.display = "block";
crossoverMenu.append(customCrossoverEntryBox);
const customCrossoverEntryInput = document.createElement("input");
customCrossoverEntryInput.type = "text";
customCrossoverEntryInput.style.width = "18em";
customCrossoverEntryInput.style.margin = "0.5em 0em 0em 0em";
customCrossoverEntryInput.id = "crossoverScript-custom-entry-input";
customCrossoverEntryBox.append(customCrossoverEntryInput);
const customCrossoverButton = document.createElement("span");
customCrossoverButton.classList.add("actions");
customCrossoverButton.style.marginLeft = "0.25em";
customCrossoverButton.style.float = "none";
customCrossoverButton.innerHTML = `<a href="#crossover-options">Add to List</a>`;
customCrossoverButton.addEventListener("click", (event) => addCustomCrossover(event));
customCrossoverEntryBox.append(customCrossoverButton);
// Buttons!
const crossoverMenuNav = document.createElement("ul");
crossoverMenuNav.classList.add("actions");
crossoverMenu.append(crossoverMenuNav);
// Add button to save filters
const applyAndSaveFiltersButton = document.createElement("li");
applyAndSaveFiltersButton.innerHTML = `<a href="#crossover-options">Apply & Save Filter</a>`;
applyAndSaveFiltersButton.addEventListener("click", (event) => saveFilters(event));
crossoverMenuNav.append(applyAndSaveFiltersButton);
// Add button to clear filters
const clearChecklistButton = document.createElement("li");
clearChecklistButton.innerHTML = `<a href="#crossover-options">Clear Checklist</a>`;
clearChecklistButton.addEventListener("click", (event) => clearChecklist(event));
crossoverMenuNav.append(clearChecklistButton);
// Add button to close without saving
const closeNoSaveButton = document.createElement("li");
closeNoSaveButton.innerHTML = `<a href="#crossover-options">Close Without Saving</a>`;
closeNoSaveButton.addEventListener("click", (event) => toggleMenu(event));
crossoverMenuNav.append(closeNoSaveButton);
const menuFooter = document.createElement("p");
menuFooter.textContent = 'This filter will be applied every time you open the page for this tag and filter "Show only crossovers".';
crossoverMenu.append(menuFooter);
}
function addCustomCrossover(event) {
event.preventDefault();
const typedFandom = document.getElementById("crossoverScript-custom-entry-input").value.trim();
if (typedFandom == "") {
return;
}
const newListElem = document.createElement("li");
newListElem.innerHTML = `<label><input type="checkbox" checked="true"><span class="indicator" aria-hidden="true"></span>${typedFandom}</label>`;
document.getElementById("crossoverScript-menu-checklist").append(newListElem);
document.getElementById("crossoverScript-custom-entry-input").value = "";
}
function clearChecklist(event) {
event.preventDefault();
const checkedFandoms = document.querySelectorAll("#crossoverScript-menu-checklist input:checked");
checkedFandoms.forEach((checkbox) => {
checkbox.checked = false;
});
}
function toggleMenu(event) {
if (event) {
event.preventDefault();
}
const menu = document.getElementById("crossoverScript-menu-wrapper");
menu.classList.toggle("active");
}
// For each fic, check if it is a fake crossover, and hide them.
function filterWorks() {
const displayedWorks = document.querySelectorAll("ol.work.group > li");
const excludedFandoms = [...storedSameFandoms, mainFandomName];
hiddenFics = 0;
// Check each work on the page
displayedWorks.forEach((work) => {
// Get its fandom tags
const workFandoms = Array.from(work.querySelectorAll("h5.fandoms > a.tag"));
// It is a true crossover if one fandom from its tags is not on the list of excluded fandoms.
const isTrueCrossover = workFandoms.some((fandom) => {
return !excludedFandoms.includes(fandom.textContent);
});
if (!isTrueCrossover) {
work.style.display = "none";
hiddenFics += 1;
}
});
updateIndicator();
}
// Store the information locally so it carries over to the next page
function saveFilters(event) {
event.preventDefault();
const checkedFandoms = Array.from(document.querySelectorAll("#crossoverScript-menu-checklist input:checked"));
storedSameFandoms.length = 0;
checkedFandoms.forEach((fandom) => {
storedSameFandoms.push(fandom.parentElement.textContent);
});
scriptStorage[mainFandomNameKey] = storedSameFandoms;
localStorage.setItem("crossoverScript", JSON.stringify(scriptStorage));
filterWorks();
toggleMenu();
}
function init() {
// Only run script if "Crossovers only" is selected
// Make sure the setup isn't already done (i.e. from going forward and back pages)
const isCrossoversOnly = document.querySelector("#work_search_crossover_t").checked;
if (isCrossoversOnly) {
createOptionsButton();
createMenu();
filterWorks();
}
}
init();
});