Skip to content

Instantly share code, notes, and snippets.

@genusslicht
Last active April 7, 2024 15:46
Show Gist options
  • Save genusslicht/2ba4be62a30f936e7cc9d8f2c33409f5 to your computer and use it in GitHub Desktop.
Save genusslicht/2ba4be62a30f936e7cc9d8f2c33409f5 to your computer and use it in GitHub Desktop.
Userscript for Violentmonkey or other browser add-ons. Literally just replaces the stats labels with icons. GreasyFork install location: https://greasyfork.org/de/scripts/490214-replace-stat-names-with-icons
// ==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/boxicons@2.1.4/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");
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();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment