AO3Boxicons

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

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/497064/1489249/AO3Boxicons.js

  1. // ==UserScript==
  2. // @exclude *
  3. // @author Yours Truly
  4. // @version 1.2.1
  5.  
  6. // ==UserLibrary==
  7. // @name AO3Boxicons
  8. // @description Reusable library that initialized the boxicons css and serves functions to turn stats and menus into icons
  9. // @license MIT
  10.  
  11. // ==/UserScript==
  12.  
  13. // ==/UserLibrary==
  14.  
  15. /**
  16. *
  17. * @param {Object} settings
  18. * @param {String} settings.boxiconsVersion Used version of https://boxicons.com/
  19. * @param {Boolean} settings.iconifyStats Flag that indicates if the AO3 work stat names should be turned into icons
  20. * @param {Object} settings.statsSettings Individual settings for stat icons
  21. * that typically consist of { iconClass: string, solid: boolean, tooltip: string }
  22. * @param {Object} settings.statsSettings.wordCountOptions
  23. * @param {Object} settings.statsSettings.chaptersOptions
  24. * @param {Object} settings.statsSettings.collectionsOptions
  25. * @param {Object} settings.statsSettings.commentsOptions
  26. * @param {Object} settings.statsSettings.kudosOptions
  27. * @param {Object} settings.statsSettings.bookmarksOptions
  28. * @param {Object} settings.statsSettings.hitsOptions
  29. * @param {Object} settings.statsSettings.workSubsOptions
  30. * @param {Object} settings.statsSettings.authorSubsOptions
  31. * @param {Object} settings.statsSettings.commentThreadsOptions
  32. * @param {Object} settings.statsSettings.challengesOptions
  33. * @param {Object} settings.statsSettings.fandomsOptions
  34. * @param {Object} settings.statsSettings.requestOptions
  35. * @param {Object} settings.statsSettings.workCountOptions
  36. * @param {Object} settings.statsSettings.seriesCompleteOptions
  37. * @param {Object} settings.statsSettings.kudos2HitsOptions
  38. * @param {Object} settings.statsSettings.timeToReadOptions
  39. * @param {Object} settings.statsSettings.dateWorkPublishedOptions
  40. * @param {Object} settings.statsSettings.dateWorkUpdateOptions
  41. * @param {Object} settings.statsSettings.dateWorkCompleteOptions
  42. * @param {Object} settings.iconifyUserNav Flag that indicates if the AO3 user navigation should be turned into icons
  43. * @param {Object} settings.userNavSettings Individual settings for user nav icons
  44. * that typically consist of { iconClass: string, solid: boolean, tooltip: string, addTooltip: boolean }
  45. * @param {Object} settings.accountOptions
  46. * @param {Object} settings.postNewOptions
  47. * @param {Object} settings.logoutOptions
  48. *
  49. */
  50. function IconifyAO3(customSettings = {}) {
  51. /**
  52. * Merges the second object into the first
  53. * If a value is in `a` but not in `b`, the value stays like it is.
  54. * If a value is in `b` but not in `a`, it gets copied over.
  55. * If a value is in both `a` and `b`, the value of `b` takes preference.
  56. *
  57. * @param {Object} a original settings
  58. * @param {Object} b user settings overwrite
  59. * @param {*} c used for temp storage, don't worry about it
  60. */
  61. function mergeSettings(a, b, c) {
  62. for (c in b) b.hasOwnProperty(c) && ((typeof a[c])[0] == "o" ? m(a[c], b[c]) : (a[c] = b[c]));
  63. }
  64.  
  65. // set global settings and overwrite with incoming settings
  66. const settings = {
  67. boxiconsVersion: "2.1.4",
  68. };
  69. mergeSettings(settings, customSettings);
  70.  
  71. /**
  72. * Initialises boxicons.com css and adds a small css to add some space between icon and stats count.
  73. */
  74. function initBoxicons() {
  75. // load boxicon style
  76. const boxicons = document.createElement("link");
  77. boxicons.setAttribute("href", `https://unpkg.com/boxicons@${settings?.boxiconsVersion}/css/boxicons.min.css`);
  78. boxicons.setAttribute("rel", "stylesheet");
  79. document.head.appendChild(boxicons);
  80.  
  81. // css that adds margin for icons
  82. const boxiconsCSS = document.createElement("style");
  83. boxiconsCSS.setAttribute("type", "text/css");
  84. boxiconsCSS.innerHTML = `
  85. i.bx {
  86. margin-right: .3em;
  87. }`;
  88. document.head.appendChild(boxiconsCSS);
  89. }
  90.  
  91. /**
  92. * Creates a new element with the icon class added to the classList.
  93. *
  94. * @param {Object} options
  95. * @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
  96. * @param {String} options.tooltip Adds an optional tooltip to the element.
  97. * Only if `addTooltip = true`.
  98. * @param {Boolean} options.addTooltip Indicates if a tooltip should be added to the element.
  99. * `tooltip` needs to be present in `options`.
  100. * @param {Boolean} options.solid Indicates if the icon should be of the "solid" variant.
  101. * Will be ignored if `iconClass` has "bx(s)" prefix.
  102. * @returns <i> Element with the neccessary classes for a boxicons icon.
  103. */
  104. function getNewIconElement(options = {}) {
  105. const i = document.createElement("i");
  106. i.classList.add("bx");
  107. if (options?.addTooltip && options?.tooltip) i.setAttribute("title", options.tooltip);
  108.  
  109. if (/^bxs?-/i.test(options.iconClass)) {
  110. // check if the icon class has the bx(s) prefix and simply set it, ignoring any settings for solid
  111. i.classList.add(options.iconClass);
  112. } else {
  113. // else, add the fittings prefix
  114. i.classList.add(options?.solid ? "bxs-" + options.iconClass : "bx-" + options.iconClass);
  115. }
  116. return i;
  117. }
  118.  
  119. /**
  120. * Prepends the given boxicons class to the given element.
  121. * Note: If the element is an <i> tag, nothing will happen, as we assume that the <i> is already an icon.
  122. *
  123. * @param {HTMLElement} element Parent element that the icon class should be prepended to.
  124. * @param {Object} options
  125. * @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
  126. * @param {String} options.tooltip Adds a tooltip to the element
  127. * @param {Boolean} options.solid Indicates if the icon should be of the "solid" variant.
  128. * Will be ignored if iconClass has "bx(s)" prefix.
  129. */
  130. function setIcon(element, options = {}) {
  131. if (element.tagName !== "I") element.prepend(getNewIconElement(options));
  132. if (options?.tooltip) element.setAttribute("title", options.tooltip);
  133. }
  134.  
  135. /**
  136. * Iterates through all elements that apply to the given querySelector and adds an element with the given icon class to it.
  137. *
  138. * @param {String} querySelector CSS selector for the elements to find and iconify.
  139. * @param {Object} options
  140. * @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
  141. * @param {String} options.tooltip Adds a tooltip to the element
  142. * @param {Boolean} options.solid Indicates if the icon should be of the "solid" variant.
  143. * Will be ignored if iconClass has "bx(s)" prefix.
  144. */
  145. function findElementsAndSetIcon(querySelector, options = {}) {
  146. const els = document.querySelectorAll(querySelector);
  147. els.forEach((el) => (el.firstChild.nodeType === Node.ELEMENT_NODE ? setIcon(el.firstChild, options) : setIcon(el, options)));
  148. }
  149.  
  150. /**
  151. * Adds an CSS that will hide the stats titles and prepends an icon to all stats.
  152. */
  153. function iconifyStats() {
  154. const WordsTotal = "dl.statistics dd.words";
  155. const WordsWork = "dl.stats dd.words";
  156. const WordsSeries = ".series.meta.group dl.stats>dd:nth-of-type(1)";
  157. const ChaptersWork = "dl.stats dd.chapters";
  158. const CollectionsWork = "dl.stats dd.collections";
  159. const CommentsWork = "dl.stats dd.comments";
  160. const KudosTotal = "dl.statistics dd.kudos";
  161. const KudosWork = "dl.stats dd.kudos";
  162. const BookmarksTotal = "dl.statistics dd.bookmarks";
  163. const BookmarksWork = "dl.stats dd.bookmarks";
  164. const BookmarksSeries = ".series.meta.group dl.stats>dd:nth-of-type(4)";
  165. const BookmarksCollection = "li.collection dl.stats dd a[href$=bookmarks]";
  166. const HitsTotal = "dl.statistics dd.hits";
  167. const HitsWork = "dl.stats dd.hits";
  168. const SubscribersTotal = "dl.statistics dd[class=subscriptions]";
  169. const SubscribersWork = "dl.stats dd.subscriptions";
  170. const ChallengesCollection = "li.collection dl.stats dd a[href$=collections]";
  171. const FandomsCollection = "li.collection dl.stats dd a[href$=fandoms]";
  172. const RequestsCollection = "li.collection dl.stats dd a[href$=requests]";
  173. const AuthorSubscribers = "dl.statistics dd.user.subscriptions";
  174. const CommentThreads = "dl.statistics dd.comment.thread";
  175. const WorksCollection = "li.collection dl.stats dd a[href$=works]";
  176. const WorksSeries = ".series.meta.group dl.stats>dd:nth-of-type(2)";
  177. const SeriesComplete = ".series.meta.group dl.stats>dd:nth-of-type(3)";
  178. const Kudos2HitsWork = "dl.stats dd.kudos-hits-ratio";
  179. const ReadingTimeWork = "dl.stats dd.reading-time";
  180. const DatePublishedWork = "dl.work dl.stats dd.published";
  181. const DateStatusTitle = "dl.work dl.stats dt.status";
  182. const DateStatusWork = "dl.work dl.stats dd.status";
  183.  
  184. const localSettings = {
  185. wordCountOptions: { tooltip: "Word Count", iconClass: "pen", solid: true },
  186. chaptersOptions: { tooltip: "Chapters", iconClass: "food-menu" },
  187. collectionsOptions: { tooltip: "Collections", iconClass: "collection", solid: true },
  188. commentsOptions: { tooltip: "Comments", iconClass: "chat", solid: true },
  189. kudosOptions: { tooltip: "Kudos", iconClass: "heart", solid: true },
  190. bookmarksOptions: { tooltip: "Bookmarks", iconClass: "bookmarks", solid: true },
  191. hitsOptions: { tooltip: "Hits", iconClass: "show-alt" },
  192. workSubsOptions: { tooltip: "Subscriptions", iconClass: "bell", solid: true },
  193. authorSubsOptions: { tooltip: "User Subscriptions", iconClass: "bell-ring", solid: true },
  194. commentThreadsOptions: { tooltip: "Comment Threads", iconClass: "conversation", solid: true },
  195. challengesOptions: { tooltip: "Challenges/Subcollections", iconClass: "collection", solid: false },
  196. fandomsOptions: { tooltip: "Fandoms", iconClass: "crown", solid: true },
  197. requestsOptions: { tooltip: "Prompts", iconClass: "invader", solid: true },
  198. workCountOptions: { tooltip: "Work Count", iconClass: "library" },
  199. seriesCompleteOptions: { tooltip: "Series Complete", iconClass: "flag-checkered", solid: true },
  200. kudos2HitsOptions: { tooltip: "Kudos to Hits", iconClass: "hot", solid: true },
  201. timeToReadOptions: { tooltip: "Time to Read", iconClass: "hourglass", solid: true },
  202. dateWorkPublishedOptions: { tooltip: "Published", iconClass: "calendar-plus" },
  203. dateWorkUpdateOptions: { tooltip: "Updated", iconClass: "calendar-edit" },
  204. dateWorkCompleteOptions: { tooltip: "Completed", iconClass: "calendar-check" },
  205. };
  206. // merge incoming settings into local settings (overwrite)
  207. mergeSettings(localSettings, settings?.statsSettings);
  208.  
  209. // css to hide stats titles
  210. const statsCSS = document.createElement("style");
  211. statsCSS.setAttribute("type", "text/css");
  212. statsCSS.innerHTML = `
  213. dl.stats dt {
  214. display: none !important;
  215. }`;
  216. document.head.appendChild(statsCSS);
  217.  
  218. findElementsAndSetIcon(`${WordsTotal}, ${WordsWork}, ${WordsSeries}`, localSettings.wordCountOptions);
  219. findElementsAndSetIcon(ChaptersWork, localSettings.chaptersOptions);
  220. findElementsAndSetIcon(CollectionsWork, localSettings.collectionsOptions);
  221. findElementsAndSetIcon(CommentsWork, localSettings.commentsOptions);
  222. findElementsAndSetIcon(`${KudosTotal}, ${KudosWork}`, localSettings.kudosOptions);
  223. findElementsAndSetIcon(`${BookmarksTotal}, ${BookmarksWork}, ${BookmarksCollection}, ${BookmarksSeries}`, localSettings.bookmarksOptions);
  224. findElementsAndSetIcon(`${HitsTotal}, ${HitsWork}`, localSettings.hitsOptions);
  225. findElementsAndSetIcon(`${SubscribersTotal}, ${SubscribersWork}`, localSettings.workSubsOptions);
  226. findElementsAndSetIcon(AuthorSubscribers, localSettings.authorSubsOptions);
  227. findElementsAndSetIcon(CommentThreads, localSettings.commentThreadsOptions);
  228. findElementsAndSetIcon(ChallengesCollection, localSettings.challengesOptions);
  229. findElementsAndSetIcon(FandomsCollection, localSettings.fandomsOptions);
  230. findElementsAndSetIcon(RequestsCollection, localSettings.requestsOptions);
  231. findElementsAndSetIcon(`${WorksCollection}, ${WorksSeries}`, localSettings.workCountOptions);
  232. findElementsAndSetIcon(SeriesComplete, localSettings.seriesCompleteOptions);
  233.  
  234. // AO3E elements
  235. findElementsAndSetIcon(Kudos2HitsWork, localSettings.kudos2HitsOptions);
  236. findElementsAndSetIcon(ReadingTimeWork, localSettings.timeToReadOptions);
  237.  
  238. // calendar icons at works page
  239. findElementsAndSetIcon(DatePublishedWork, localSettings.dateWorkPublishedOptions);
  240. const workStatus = document.querySelector(DateStatusTitle);
  241. if (workStatus && workStatus.innerHTML.startsWith("Updated")) {
  242. setIcon(document.querySelector(DateStatusWork), localSettings.dateWorkUpdateOptions);
  243. } else if (workStatus && workStatus.innerHTML.startsWith("Completed")) {
  244. setIcon(document.querySelector(DateStatusWork), localSettings.dateWorkCompleteOptions);
  245. }
  246. }
  247.  
  248. /**
  249. * Replaces the "Hi, {user}!", "Post" and "Log out" text at the top of the page with icons.
  250. */
  251. function iconifyUserNav() {
  252. const localSettings = {
  253. accountOptions: { tooltip: "User Area", addTooltip: true, iconClass: "user-circle", solid: true },
  254. postNewOptions: { tooltip: "New Work", addTooltip: true, iconClass: "book-add", solid: true },
  255. logoutOptions: { tooltip: "Logout", addTooltip: true, iconClass: "log-out" },
  256. };
  257. // merge incoming settings into local settings (overwrite)
  258. mergeSettings(localSettings, settings?.userNavSettings);
  259.  
  260. const AccountUserNav = "#header a.dropdown-toggle[href*='/users/']";
  261. const PostUserNav = "#header a.dropdown-toggle[href*='/works/new']";
  262. const LogoutUserNav = "#header a[href*='/users/logout']";
  263.  
  264. // add css for user navigation icons
  265. const userNavCss = document.createElement("style");
  266. userNavCss.setAttribute("type", "text/css");
  267. userNavCss.innerHTML = `
  268. ${LogoutUserNav},
  269. ${AccountUserNav},
  270. ${PostUserNav} {
  271. /* font size needs to be higher to make icons the right size */
  272. font-size: 1.25rem;
  273. /* left and right padding for a slightly bigger hover hitbox */
  274. padding: 0 .3rem;
  275. }
  276. ${LogoutUserNav} i.bx {
  277. /* overwrite the right margin for logout icon */
  278. margin-right: 0;
  279. /* add left margin instead to add more space to user actions */
  280. margin-left: .3em;
  281. }`;
  282. document.head.appendChild(userNavCss);
  283.  
  284. // replace text with icons
  285. document.querySelector(AccountUserNav).replaceChildren(getNewIconElement(localSettings.accountOptions));
  286. document.querySelector(PostUserNav).replaceChildren(getNewIconElement(localSettings.postNewOptions));
  287. document.querySelector(LogoutUserNav).replaceChildren(getNewIconElement(localSettings.logoutOptions));
  288. }
  289.  
  290. initBoxicons();
  291.  
  292. if (settings?.iconifyStats) iconifyStats();
  293. if (settings?.iconifyUserNav) iconifyUserNav();
  294. }