Replace stat names with icons

Replaces stat titles and user navigation with icons from https://boxicons.com

当前为 2024-03-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @match       https://archiveofourown.org/*
// @author      genusslicht
// @namespace   ao3-boxicons
// @name        Replace stat names with icons
// @description Replaces stat titles and user navigation with icons from https://boxicons.com
// @icon        https://archiveofourown.org/favicon.ico
// @supportURL  https://gist.github.com/genusslicht/2ba4be62a30f936e7cc9d8f2c33409f5
// @license     MIT
// @version     1.0.0
// @grant       none
// ==/UserScript==

// AO3 css selectors 
const WordsTotal = "dl.statistics dd.words";
const WordsWork = "dl.stats dd.words";
const ChaptersWork = "dl.stats dd.chapters";
const CollectionsWork = "dl.stats dd.collections";
const CommentsWork = "dl.stats dd.comments";
const KudosTotal = "dl.statistics dd.kudos";
const KudosWork = "dl.stats dd.kudos";
const BookmarksTotal = "dl.statistics dd.bookmarks";
const BookmarksWork = "dl.stats dd.bookmarks";
const HitsTotal = "dl.statistics dd.hits";
const HitsWork = "dl.stats dd.hits";
const SubscribersWork = "dl.stats dd.subscriptions";
const SubscribersTotal = "dl.statistics dd[class=subscriptions]";
const AuthorSubscribers = "dl.statistics dd.user.subscriptions";
const CommentThreads = "dl.statistics dd.comment.thread";
const FandomsCollection = "li.collection dl.stats dd a[href$=fandoms]";
const WorksCollection = "li.collection dl.stats dd a[href$=works]";
const BookmarksCollection = "li.collection dl.stats dd a[href$=bookmarks]";
const Kudos2HitsWork = "dl.stats dd.kudos-hits-ratio";
const ReadingTimeWork = "dl.stats dd.reading-time";
const DatePublishedWork = "dl.work dl.stats dd.published";
const DateStatusTitle = "dl.work dl.stats dt.status";
const DateStatusWork = "dl.work dl.stats dd.status";

const AccountUserNav = "#header a.dropdown-toggle[href*='/users/']";
const PostUserNav = "#header a.dropdown-toggle[href*='/works/new']";
const LogoutUserNav = "#header a[href*='/users/logout']";

/**
 * Initialises boxicons.com css and adds a small css to add some space between icon and stats count.
 */
function initBoxicons() {
  // load boxicon style
  const boxicons = document.createElement("link");
  boxicons.setAttribute("href", "https://unpkg.com/[email protected]/css/boxicons.min.css");
  boxicons.setAttribute("rel", "stylesheet");
  document.head.appendChild(boxicons);

  // css that adds margin for icons
  const boxiconsCSS = document.createElement("style");
  boxiconsCSS.setAttribute("type", "text/css");
  boxiconsCSS.innerHTML = `
    i.bx {
      margin-right: .3em;
    }`;
  document.head.appendChild(boxiconsCSS);
}

/**
 * Creates a new element with the icon class added to the classList.
 * 
 * @param {string} iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
 * @param {boolean} solid    Indicates if the icon should be of the "solid" variant. 
 *                           Will be ignored if iconClass has "bx(s)" prefix. 
 * @returns <i> Element with the necessary classes for a boxicons icon.
 */
function getNewIconElement(iconClass, solid = false) {
  const i = document.createElement("i");
  i.classList.add("bx");
  if (/^bxs?-/i.test(iconClass))
    i.classList.add(iconClass);
  else {
    i.classList.add(solid ? "bxs-"+iconClass : "bx-"+iconClass);
  }
  return i;
}

/**
 * Prepends the given boxicons class to the given element.
 * Note: If the element is an <i> tag, nothing will happen, as we assume that the <i> is already an icon.
 * 
 * @param {HTMLElement} element parent element that the icon class should be prepended to.
 * @param {string} iconClass    name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
 * @param {boolean} solid       Indicates if the icon should be of the "solid" variant. 
 *                              Will be ignored if iconClass has "bx(s)" prefix. 
 */
function setIcon(element, iconClass, solid = false) {
  if (element.tagName !== "I") element.prepend(getNewIconElement(iconClass, solid));
}

/**
 * Iterates through all elements that apply to the given querySelector and adds an element with the given icon class to it.
 * 
 * @param {string} querySelector CSS selector for the elements to find and iconify. 
 * @param {string} iconClass     name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
 * @param {boolean} solid        Indicates if the icon should be of the "solid" variant. 
 *                               Will be ignored if iconClass has "bx(s)" prefix. 
 */
function findElementsAndSetIcon(querySelector, iconClass, solid = false) {
  const els = document.querySelectorAll(querySelector);
  els.forEach(el => el.firstChild.nodeType === Node.ELEMENT_NODE ? setIcon(el.firstChild, iconClass, solid) : setIcon(el, iconClass, solid));
}

/**
 * Adds an CSS that will hide the stats titles and prepends an icon to all stats.
 */
function iconifyStats() {
  // css to hide stats titles
  const statsCSS = document.createElement("style");
  statsCSS.setAttribute("type", "text/css");
  statsCSS.innerHTML = `
    dl.stats dt {
      display: none !important;
    }`;
    document.head.appendChild(statsCSS);

  findElementsAndSetIcon(`${WordsTotal}, ${WordsWork}`, "pen", true);
  findElementsAndSetIcon(ChaptersWork, "food-menu");
  findElementsAndSetIcon(CollectionsWork, "collection", true);
  findElementsAndSetIcon(CommentsWork, "chat", true);
  findElementsAndSetIcon(`${KudosTotal}, ${KudosWork}`, "heart", true);
  findElementsAndSetIcon(`${BookmarksTotal}, ${BookmarksWork}, ${BookmarksCollection}`, "bookmarks", true);
  findElementsAndSetIcon(`${HitsTotal}, ${HitsWork}`, "show-alt");
  findElementsAndSetIcon(`${SubscribersTotal}, ${SubscribersWork}`, "bell", true);
  findElementsAndSetIcon(AuthorSubscribers, "bell-ring", true);
  findElementsAndSetIcon(CommentThreads, "conversation", true);
  findElementsAndSetIcon(FandomsCollection, "crown", true);
  findElementsAndSetIcon(WorksCollection, "library");

  // AO3E elements
  findElementsAndSetIcon(Kudos2HitsWork, "hot", true);
  findElementsAndSetIcon(ReadingTimeWork, "hourglass", true);

  // calendar icons at works page
  findElementsAndSetIcon(DatePublishedWork, "calendar-plus");
  const workStatus = document.querySelector(DateStatusTitle);
  if (workStatus && workStatus.innerHTML.startsWith("Updated")) {
    setIcon(document.querySelector(DateStatusWork), "calendar-edit");
  } else if (workStatus && workStatus.innerHTML.startsWith("Completed")) {
    setIcon(document.querySelector(DateStatusWork), "calendar-check");
  }
}

/**
 * Replaces the "Hi, {user}!", "Post" and "Log out" text at the top of the page with icons.
 */
function iconifyUserNav() {
  // add css for user navigation icons
  const userNavCss = document.createElement("style");
  userNavCss.setAttribute("type", "text/css");
  userNavCss.innerHTML = `
  ${LogoutUserNav},
  ${AccountUserNav},
  ${PostUserNav} {
    /* font size needs to be higher to make icons the right size */
    font-size: 1.25rem;
    /* left and right padding for a slightly bigger hover hitbox */
    padding: 0 .3rem;
  }

  ${LogoutUserNav} i.bx {
    /* overwrite the right margin for logout icon */
    margin-right: 0;
    /* add left margin instead to add more space to user actions */
    margin-left: .3em;
  }`;
  document.head.appendChild(userNavCss);

  // replace text with icons
  document.querySelector(AccountUserNav).replaceChildren(getNewIconElement("user-circle", true));
  document.querySelector(PostUserNav).replaceChildren(getNewIconElement("book-add", true));
  document.querySelector(LogoutUserNav).replaceChildren(getNewIconElement("log-out"));
}

(function() {
  initBoxicons();
  iconifyStats();
  iconifyUserNav();
})();