AO3LucideIcons

Reusable library that initialized lucide icons and serves functions to turn stats and menus into icons

Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @require https://update.greasyfork.ip-ddns.com/scripts/539241/1607168/AO3LucideIcons.js

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

// ==UserLibrary==
// @name         AO3LucideIcons
// @description  Reusable library that initialized lucide icons and serves functions to turn stats and menus into icons
// @license      MIT

// ==/UserScript==

// ==/UserLibrary==

/**
 *
 * @param {Object} settings
 * @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 { icon: 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 { icon: 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 = {};
  mergeSettings(settings, customSettings);

  /**
   * Initialises lucide.dev
   */
  function initLucide() {
    lucide.createIcons();
    const lucideCSS = document.createElement("style");
    lucideCSS.setAttribute("type", "text/css");
    lucideCSS.innerHTML = `
        .lucide {
          width: 1em;
          height: 1em;
          margin-right: .3em;
        }
          
        .lucide-icon {
          vertical-align: middle;
        }`;
    document.head.appendChild(lucideCSS);
  }

  /**
   * Creates a new element with the icon class added to the classList.
   *
   * @param {Object} options
   * @param {String} options.icon  Name of the icon to use.
   * @returns <i> Element with the neccessary classes for a boxicons icon.
   */
  function getNewIconElement(options = {}) {
    const i = document.createElement("i");
    const wrapper = document.createElement("span");

    i.setAttribute("data-lucide", options.icon);
    wrapper.appendChild(i);
    wrapper.classList.add("lucide-icon");

    return wrapper;
  }

  /**
   * 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.icon         Name of the icon to use
   * @param {String} options.tooltip      Adds a tooltip to the element
   * @param {Boolean} options.addTooltip  Indicates if a tooltip should be added to the element.
   *                                      `tooltip` needs to be present in `options`.
   */
  function setIcon(element, options = {}) {
    if (element.tagName !== "I") element.prepend(getNewIconElement(options));
    if (options?.addTooltip && 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.icon         Name of the icon to use.
   * @param {String} options.tooltip      Adds a tooltip to the element.
   * @param {Boolean} options.addTooltip  Indicates if a tooltip should be added to the element.
   *                                      `tooltip` needs to be present in `options`.
   */
  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 TotalWords = "dl.statistics dd.words";
    const TotalHits = "dl.statistics dd.hits";
    const TotalKudos = "dl.statistics dd.kudos";
    const TotalBookmarks = "dl.statistics dd.bookmarks";
    const TotalCommentThreads = "dl.statistics dd.comment.thread";
    const TotalSubscribersWorks = "dl.statistics dd[class=subscriptions]";
    const TotalSubscribersAuthor = "dl.statistics dd.user.subscriptions";

    const WorkLanguage = "dl.stats dd.language,.work.meta.group dd.language";
    const WorkWords = "dl.stats dd.words";
    const WorkChapters = "dl.stats dd.chapters";
    const WorkHits = "dl.stats dd.hits";
    const WorkKudos = "dl.stats dd.kudos";
    const WorkComments = "dl.stats dd.comments";
    const WorkBookmarks = "dl.stats dd.bookmarks";
    const WorkSeries = ".work.blurb.group ul.series>li,.work.meta.group dd.series";
    const WorkCollections = "dl.stats dd.collections,.work.meta.group dd.collections";
    const WorkSubscribers = "dl.stats dd.subscriptions";
    const WorkKudos2Hits = "dl.stats dd.kudos-hits-ratio";
    const WorkReadingTime = "dl.stats dd.reading-time";
    const WorkDatePublished = "dl.work dl.stats dd.published";

    const SeriesWords = ".series.group dl.stats>dd.words";
    const SeriesWorks = ".series.group dl.stats>dd.works";
    const SeriesBookmarks = ".series.group dl.stats>dd.bookmarks";
    const SeriesComplete = ".series.group dl.stats>dd[title~=Complete]";

    const CollectionWorks = ".collection.group dl.stats dd a[href$=works]";
    const CollectionFandoms = ".collection.group dl.stats dd a[href$=fandoms]";
    const CollectionRequests = ".collection.group dl.stats dd a[href$=requests]";
    const CollectionBookmarks = ".collection.group dl.stats dd a[href$=bookmarks]";
    const CollectionChallenges = ".collection.group dl.stats dd a[href$=collections]";

    const DateStatusTitle = "dl.work dl.stats dt.status";
    const DateStatusWork = "dl.work dl.stats dd.status";

    const localSettings = {
      languageOptions: { tooltip: "Language", addTooltip: true, icon: "languages" },
      wordCountOptions: { tooltip: "Word Count", addTooltip: true, icon: "pencil-line" },
      chaptersOptions: { tooltip: "Chapters", addTooltip: true, icon: "book-open-text" },
      seriesOptions: { tooltip: "Series", addTooltip: true, icon: "component" },
      collectionsOptions: { tooltip: "Collections", addTooltip: true, icon: "gallery-vertical-end" },
      commentsOptions: { tooltip: "Comments", addTooltip: true, icon: "message-square" },
      kudosOptions: { tooltip: "Kudos", addTooltip: true, icon: "sparkles" },
      bookmarksOptions: { tooltip: "Bookmarks", addTooltip: true, icon: "bookmark" },
      hitsOptions: { tooltip: "Hits", addTooltip: true, icon: "eye" },
      workSubsOptions: { tooltip: "Subscriptions", addTooltip: true, icon: "mail-check" },
      authorSubsOptions: { tooltip: "User Subscriptions", addTooltip: true, icon: "user-round-check" },
      commentThreadsOptions: { tooltip: "Comment Threads", addTooltip: true, icon: "messages-square" },
      challengesOptions: { tooltip: "Challenges/Subcollections", addTooltip: true, icon: "swords" },
      fandomsOptions: { tooltip: "Fandoms", addTooltip: true, icon: "crown" },
      requestsOptions: { tooltip: "Prompts", addTooltip: true, icon: "cake-slice" },
      workCountOptions: { tooltip: "Work Count", addTooltip: true, icon: "library" },
      seriesCompleteOptions: { tooltip: "Series Complete", addTooltip: true, icon: "circle-check-big" },
      kudos2HitsOptions: { tooltip: "Kudos to Hits", addTooltip: true, icon: "flame" },
      timeToReadOptions: { tooltip: "Time to Read", addTooltip: true, icon: "hourglass" },
      dateWorkPublishedOptions: { tooltip: "Published", addTooltip: true, icon: "calendar-plus" },
      dateWorkUpdateOptions: { tooltip: "Updated", addTooltip: true, icon: "calendar-clock" },
      dateWorkCompleteOptions: { tooltip: "Completed", addTooltip: true, icon: "calendar-check-2" },
    };
    // 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 = `
        .meta dt, dl.stats dt {
          display: none;
        }`;
    document.head.appendChild(statsCSS);

    findElementsAndSetIcon(WorkLanguage, localSettings.languageOptions);
    findElementsAndSetIcon(`${TotalWords}, ${WorkWords}, ${SeriesWords}`, localSettings.wordCountOptions);
    findElementsAndSetIcon(WorkChapters, localSettings.chaptersOptions);
    findElementsAndSetIcon(WorkSeries, localSettings.seriesOptions);
    findElementsAndSetIcon(WorkCollections, localSettings.collectionsOptions);
    findElementsAndSetIcon(WorkComments, localSettings.commentsOptions);
    findElementsAndSetIcon(`${TotalKudos}, ${WorkKudos}`, localSettings.kudosOptions);
    findElementsAndSetIcon(`${TotalBookmarks}, ${WorkBookmarks}, ${CollectionBookmarks}, ${SeriesBookmarks}`, localSettings.bookmarksOptions);
    findElementsAndSetIcon(`${TotalHits}, ${WorkHits}`, localSettings.hitsOptions);
    findElementsAndSetIcon(`${TotalSubscribersWorks}, ${WorkSubscribers}`, localSettings.workSubsOptions);
    findElementsAndSetIcon(TotalSubscribersAuthor, localSettings.authorSubsOptions);
    findElementsAndSetIcon(TotalCommentThreads, localSettings.commentThreadsOptions);
    findElementsAndSetIcon(CollectionChallenges, localSettings.challengesOptions);
    findElementsAndSetIcon(CollectionFandoms, localSettings.fandomsOptions);
    findElementsAndSetIcon(CollectionRequests, localSettings.requestsOptions);
    findElementsAndSetIcon(`${CollectionWorks}, ${SeriesWorks}`, localSettings.workCountOptions);
    findElementsAndSetIcon(SeriesComplete, localSettings.seriesCompleteOptions);

    // AO3E elements
    findElementsAndSetIcon(WorkKudos2Hits, localSettings.kudos2HitsOptions);
    findElementsAndSetIcon(WorkReadingTime, localSettings.timeToReadOptions);

    // calendar icons at works page
    findElementsAndSetIcon(WorkDatePublished, 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, icon: "circle-user-round" },
      postNewOptions: { tooltip: "New Work", addTooltip: true, icon: "book-plus" },
      logoutOptions: { tooltip: "Logout", addTooltip: true, icon: "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} .lucide {
        /* 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));
  }

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