Iconify Stats with Boxicons

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

目前为 2024-03-18 提交的版本。查看 最新版本

// ==UserScript==
// @name        Iconify Stats with Boxicons
// @match       https://archiveofourown.org/*
// @grant       none
// @author      genusslicht
// @description Replaces Stats and user navigation with icons from https://boxicons.com
// @license     MIT
// @namespace   ao3-boxicons
// @version     1.0.0
// @icon        https://archiveofourown.org/favicon.ico
// ==/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 BookmarksCollection = "li.collection dl.stats dd a[href$=bookmarks]";
const HitsTotal = "dl.statistics dd.hits";
const HitsWork = "dl.stats dd.hits";
const SubscribersTotal = "dl.statistics dd[class=subscriptions]";
const SubscribersWork = "dl.stats dd.subscriptions";
const FandomsCollection = "li.collection dl.stats dd a[href$=fandoms]";
const AuthorSubscribers = "dl.statistics dd.user.subscriptions";
const CommentThreads = "dl.statistics dd.comment.thread";
const WorksCollection = "li.collection dl.stats dd a[href$=works]";
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 neccessary 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();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址