AO3: True Crossover Filter

Help find real crossovers when fandoms have overlapping fandom tags.

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
});