AO3Boxicons

Reusable library that initialized the boxicons css and serves functions to turn stats and menus into icons

ეს სკრიპტი არ უნდა იყოს პირდაპირ დაინსტალირებული. ეს ბიბლიოთეკაა, სხვა სკრიპტებისთვის უნდა ჩართეთ მეტა-დირექტივაში // @require https://update.greasyfork.org/scripts/497064/1489249/AO3Boxicons.js.

// ==UserScript==
// @exclude      *
// @author       Yours Truly
// @version      1.2.1

// ==UserLibrary==
// @name         AO3Boxicons
// @description  Reusable library that initialized the boxicons css and serves functions to turn stats and menus into icons
// @license      MIT

// ==/UserScript==

// ==/UserLibrary==

/**
 *
 * @param {Object} settings
 * @param {String} settings.boxiconsVersion Used version of https://boxicons.com/
 * @param {Boolean} settings.iconifyStats   Flag that indicates if the AO3 work stat names should be turned into icons
 * @param {Object} settings.statsSettings   Individual settings for stat icons
 *                                          that typically consist of { iconClass: string, solid: boolean, tooltip: string }
 * @param {Object} settings.statsSettings.wordCountOptions
 * @param {Object} settings.statsSettings.chaptersOptions
 * @param {Object} settings.statsSettings.collectionsOptions
 * @param {Object} settings.statsSettings.commentsOptions
 * @param {Object} settings.statsSettings.kudosOptions
 * @param {Object} settings.statsSettings.bookmarksOptions
 * @param {Object} settings.statsSettings.hitsOptions
 * @param {Object} settings.statsSettings.workSubsOptions
 * @param {Object} settings.statsSettings.authorSubsOptions
 * @param {Object} settings.statsSettings.commentThreadsOptions
 * @param {Object} settings.statsSettings.challengesOptions
 * @param {Object} settings.statsSettings.fandomsOptions
 * @param {Object} settings.statsSettings.requestOptions
 * @param {Object} settings.statsSettings.workCountOptions
 * @param {Object} settings.statsSettings.seriesCompleteOptions
 * @param {Object} settings.statsSettings.kudos2HitsOptions
 * @param {Object} settings.statsSettings.timeToReadOptions
 * @param {Object} settings.statsSettings.dateWorkPublishedOptions
 * @param {Object} settings.statsSettings.dateWorkUpdateOptions
 * @param {Object} settings.statsSettings.dateWorkCompleteOptions
 * @param {Object} settings.iconifyUserNav  Flag that indicates if the AO3 user navigation should be turned into icons
 * @param {Object} settings.userNavSettings Individual settings for user nav icons
 *                                          that typically consist of { iconClass: string, solid: boolean, tooltip: string, addTooltip: boolean }
 * @param {Object} settings.accountOptions
 * @param {Object} settings.postNewOptions
 * @param {Object} settings.logoutOptions
 *
 */
function IconifyAO3(customSettings = {}) {
  /**
   * Merges the second object into the first
   * If a value is in `a` but not in `b`, the value stays like it is.
   * If a value is in `b` but not in `a`, it gets copied over.
   * If a value is in both `a` and `b`, the value of `b` takes preference.
   *
   * @param {Object} a original settings
   * @param {Object} b user settings overwrite
   * @param {*} c used for temp storage, don't worry about it
   */
  function mergeSettings(a, b, c) {
    for (c in b) b.hasOwnProperty(c) && ((typeof a[c])[0] == "o" ? m(a[c], b[c]) : (a[c] = b[c]));
  }

  // set global settings and overwrite with incoming settings
  const settings = {
    boxiconsVersion: "2.1.4",
  };
  mergeSettings(settings, customSettings);

  /**
   * 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@${settings?.boxiconsVersion}/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 {Object} options
   * @param {String} options.iconClass    Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
   * @param {String} options.tooltip      Adds an optional tooltip to the element.
   *                                      Only if `addTooltip = true`.
   * @param {Boolean} options.addTooltip  Indicates if a tooltip should be added to the element.
   *                                      `tooltip` needs to be present in `options`.
   * @param {Boolean} options.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(options = {}) {
    const i = document.createElement("i");
    i.classList.add("bx");
    if (options?.addTooltip && options?.tooltip) i.setAttribute("title", options.tooltip);

    if (/^bxs?-/i.test(options.iconClass)) {
      // check if the icon class has the bx(s) prefix and simply set it, ignoring any settings for solid
      i.classList.add(options.iconClass);
    } else {
      // else, add the fittings prefix
      i.classList.add(options?.solid ? "bxs-" + options.iconClass : "bx-" + options.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 {Object} options
   * @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
   * @param {String} options.tooltip   Adds a tooltip to the element
   * @param {Boolean} options.solid    Indicates if the icon should be of the "solid" variant.
   *                                   Will be ignored if iconClass has "bx(s)" prefix.
   */
  function setIcon(element, options = {}) {
    if (element.tagName !== "I") element.prepend(getNewIconElement(options));
    if (options?.tooltip) element.setAttribute("title", options.tooltip);
  }

  /**
   * 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 {Object} options
   * @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
   * @param {String} options.tooltip   Adds a tooltip to the element
   * @param {Boolean} options.solid    Indicates if the icon should be of the "solid" variant.
   *                                   Will be ignored if iconClass has "bx(s)" prefix.
   */
  function findElementsAndSetIcon(querySelector, options = {}) {
    const els = document.querySelectorAll(querySelector);
    els.forEach((el) => (el.firstChild.nodeType === Node.ELEMENT_NODE ? setIcon(el.firstChild, options) : setIcon(el, options)));
  }

  /**
   * Adds an CSS that will hide the stats titles and prepends an icon to all stats.
   */
  function iconifyStats() {
    const WordsTotal = "dl.statistics dd.words";
    const WordsWork = "dl.stats dd.words";
    const WordsSeries = ".series.meta.group dl.stats>dd:nth-of-type(1)";
    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 BookmarksSeries = ".series.meta.group dl.stats>dd:nth-of-type(4)";
    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 ChallengesCollection = "li.collection dl.stats dd a[href$=collections]";
    const FandomsCollection = "li.collection dl.stats dd a[href$=fandoms]";
    const RequestsCollection = "li.collection dl.stats dd a[href$=requests]";
    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 WorksSeries = ".series.meta.group dl.stats>dd:nth-of-type(2)";
    const SeriesComplete = ".series.meta.group dl.stats>dd:nth-of-type(3)";
    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 localSettings = {
      wordCountOptions: { tooltip: "Word Count", iconClass: "pen", solid: true },
      chaptersOptions: { tooltip: "Chapters", iconClass: "food-menu" },
      collectionsOptions: { tooltip: "Collections", iconClass: "collection", solid: true },
      commentsOptions: { tooltip: "Comments", iconClass: "chat", solid: true },
      kudosOptions: { tooltip: "Kudos", iconClass: "heart", solid: true },
      bookmarksOptions: { tooltip: "Bookmarks", iconClass: "bookmarks", solid: true },
      hitsOptions: { tooltip: "Hits", iconClass: "show-alt" },
      workSubsOptions: { tooltip: "Subscriptions", iconClass: "bell", solid: true },
      authorSubsOptions: { tooltip: "User Subscriptions", iconClass: "bell-ring", solid: true },
      commentThreadsOptions: { tooltip: "Comment Threads", iconClass: "conversation", solid: true },
      challengesOptions: { tooltip: "Challenges/Subcollections", iconClass: "collection", solid: false },
      fandomsOptions: { tooltip: "Fandoms", iconClass: "crown", solid: true },
      requestsOptions: { tooltip: "Prompts", iconClass: "invader", solid: true },
      workCountOptions: { tooltip: "Work Count", iconClass: "library" },
      seriesCompleteOptions: { tooltip: "Series Complete", iconClass: "flag-checkered", solid: true },
      kudos2HitsOptions: { tooltip: "Kudos to Hits", iconClass: "hot", solid: true },
      timeToReadOptions: { tooltip: "Time to Read", iconClass: "hourglass", solid: true },
      dateWorkPublishedOptions: { tooltip: "Published", iconClass: "calendar-plus" },
      dateWorkUpdateOptions: { tooltip: "Updated", iconClass: "calendar-edit" },
      dateWorkCompleteOptions: { tooltip: "Completed", iconClass: "calendar-check" },
    };
    // merge incoming settings into local settings (overwrite)
    mergeSettings(localSettings, settings?.statsSettings);

    // 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}, ${WordsSeries}`, localSettings.wordCountOptions);
    findElementsAndSetIcon(ChaptersWork, localSettings.chaptersOptions);
    findElementsAndSetIcon(CollectionsWork, localSettings.collectionsOptions);
    findElementsAndSetIcon(CommentsWork, localSettings.commentsOptions);
    findElementsAndSetIcon(`${KudosTotal}, ${KudosWork}`, localSettings.kudosOptions);
    findElementsAndSetIcon(`${BookmarksTotal}, ${BookmarksWork}, ${BookmarksCollection}, ${BookmarksSeries}`, localSettings.bookmarksOptions);
    findElementsAndSetIcon(`${HitsTotal}, ${HitsWork}`, localSettings.hitsOptions);
    findElementsAndSetIcon(`${SubscribersTotal}, ${SubscribersWork}`, localSettings.workSubsOptions);
    findElementsAndSetIcon(AuthorSubscribers, localSettings.authorSubsOptions);
    findElementsAndSetIcon(CommentThreads, localSettings.commentThreadsOptions);
    findElementsAndSetIcon(ChallengesCollection, localSettings.challengesOptions);
    findElementsAndSetIcon(FandomsCollection, localSettings.fandomsOptions);
    findElementsAndSetIcon(RequestsCollection, localSettings.requestsOptions);
    findElementsAndSetIcon(`${WorksCollection}, ${WorksSeries}`, localSettings.workCountOptions);
    findElementsAndSetIcon(SeriesComplete, localSettings.seriesCompleteOptions);

    // AO3E elements
    findElementsAndSetIcon(Kudos2HitsWork, localSettings.kudos2HitsOptions);
    findElementsAndSetIcon(ReadingTimeWork, localSettings.timeToReadOptions);

    // calendar icons at works page
    findElementsAndSetIcon(DatePublishedWork, localSettings.dateWorkPublishedOptions);
    const workStatus = document.querySelector(DateStatusTitle);
    if (workStatus && workStatus.innerHTML.startsWith("Updated")) {
      setIcon(document.querySelector(DateStatusWork), localSettings.dateWorkUpdateOptions);
    } else if (workStatus && workStatus.innerHTML.startsWith("Completed")) {
      setIcon(document.querySelector(DateStatusWork), localSettings.dateWorkCompleteOptions);
    }
  }

  /**
   * Replaces the "Hi, {user}!", "Post" and "Log out" text at the top of the page with icons.
   */
  function iconifyUserNav() {
    const localSettings = {
      accountOptions: { tooltip: "User Area", addTooltip: true, iconClass: "user-circle", solid: true },
      postNewOptions: { tooltip: "New Work", addTooltip: true, iconClass: "book-add", solid: true },
      logoutOptions: { tooltip: "Logout", addTooltip: true, iconClass: "log-out" },
    };
    // merge incoming settings into local settings (overwrite)
    mergeSettings(localSettings, settings?.userNavSettings);

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

    // 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(localSettings.accountOptions));
    document.querySelector(PostUserNav).replaceChildren(getNewIconElement(localSettings.postNewOptions));
    document.querySelector(LogoutUserNav).replaceChildren(getNewIconElement(localSettings.logoutOptions));
  }

  initBoxicons();

  if (settings?.iconifyStats) iconifyStats();
  if (settings?.iconifyUserNav) iconifyUserNav();
}