สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @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();
}