VoidVerified

Social enhancements for AniList.

当前为 2024-02-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name VoidVerified
  3. // @version 1.8.1
  4. // @namespace http://tampermonkey.net/
  5. // @author voidnyan
  6. // @description Social enhancements for AniList.
  7. // @homepageURL https://github.com/voidnyan/void-verified#voidverified
  8. // @supportURL https://github.com/voidnyan/void-verified/issues
  9. // @grant GM_xmlhttpRequest
  10. // @match https://anilist.co/*
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. "use strict";
  16.  
  17. const categories = {
  18. users: "users",
  19. paste: "paste",
  20. css: "css",
  21. misc: "misc",
  22. };
  23.  
  24. const defaultSettings = {
  25. copyColorFromProfile: {
  26. defaultValue: true,
  27. description: "Copy user color from their profile.",
  28. category: categories.users,
  29. },
  30. moveSubscribeButtons: {
  31. defaultValue: false,
  32. description:
  33. "Move activity subscribe button next to comments and likes.",
  34. category: categories.misc,
  35. },
  36. hideLikeCount: {
  37. defaultValue: false,
  38. description: "Hide activity and reply like counts.",
  39. category: categories.misc,
  40. },
  41. enabledForUsername: {
  42. defaultValue: true,
  43. description: "Display a verified sign next to usernames.",
  44. category: categories.users,
  45. },
  46. enabledForProfileName: {
  47. defaultValue: false,
  48. description: "Display a verified sign next to a profile name.",
  49. category: categories.users,
  50. },
  51. defaultSign: {
  52. defaultValue: "✔",
  53. description: "The default sign displayed next to a username.",
  54. category: categories.users,
  55. },
  56. highlightEnabled: {
  57. defaultValue: true,
  58. description: "Highlight user activity with a border.",
  59. category: categories.users,
  60. },
  61. highlightEnabledForReplies: {
  62. defaultValue: true,
  63. description: "Highlight replies with a border.",
  64. category: categories.users,
  65. },
  66. highlightSize: {
  67. defaultValue: "5px",
  68. description: "Width of the highlight border.",
  69. category: categories.users,
  70. },
  71. colorUserActivity: {
  72. defaultValue: false,
  73. description: "Color user activity links with user color.",
  74. category: categories.users,
  75. },
  76. colorUserReplies: {
  77. defaultValue: false,
  78. description: "Color user reply links with user color.",
  79. category: categories.users,
  80. },
  81. useDefaultHighlightColor: {
  82. defaultValue: false,
  83. description:
  84. "Use fallback highlight color when user color is not specified.",
  85. category: categories.users,
  86. },
  87. defaultHighlightColor: {
  88. defaultValue: "#FFFFFF",
  89. description: "Fallback highlight color.",
  90. category: categories.users,
  91. },
  92. globalCssEnabled: {
  93. defaultValue: false,
  94. description: "Enable custom global CSS.",
  95. category: categories.css,
  96. },
  97. globalCssAutoDisable: {
  98. defaultValue: true,
  99. description: "Disable global CSS when a profile has custom CSS.",
  100. category: categories.css,
  101. },
  102. profileCssEnabled: {
  103. defaultValue: false,
  104. description: "Load user's custom CSS when viewing their profile.",
  105. category: categories.css,
  106. authRequired: true,
  107. },
  108. activityCssEnabled: {
  109. defaultValue: false,
  110. description:
  111. "Load user's custom CSS when viewing their activity (direct link).",
  112. category: categories.css,
  113. authRequired: true,
  114. },
  115. onlyLoadCssFromVerifiedUser: {
  116. defaultValue: false,
  117. description: "Only load custom CSS from verified users.",
  118. category: categories.css,
  119. },
  120. layoutDesignerEnabled: {
  121. defaultValue: false,
  122. description: "Enable Layout Designer in the settings tab.",
  123. category: categories.misc,
  124. authRequired: true,
  125. },
  126. quickAccessEnabled: {
  127. defaultValue: false,
  128. description: "Display quick access of users in home page.",
  129. category: categories.users,
  130. },
  131. quickAccessBadge: {
  132. defaultValue: false,
  133. description:
  134. "Display a badge on quick access when changes are detected on user's layout.",
  135. category: categories.users,
  136. },
  137. quickAccessTimer: {
  138. defaultValue: true,
  139. description: "Display a timer until next update of Quick Access.",
  140. category: categories.users,
  141. },
  142. pasteEnabled: {
  143. defaultValue: false,
  144. description:
  145. "Automatically wrap pasted links and images with link and image tags.",
  146. category: categories.paste,
  147. },
  148. pasteWrapImagesWithLink: {
  149. defaultValue: false,
  150. description: "Wrap images with a link tag.",
  151. category: categories.paste,
  152. },
  153. pasteImageWidth: {
  154. defaultValue: "420",
  155. description: "Width used when pasting images.",
  156. category: categories.paste,
  157. },
  158. pasteImagesToHostService: {
  159. defaultValue: false,
  160. description:
  161. "Upload image from the clipboard to image host (configure below).",
  162. category: categories.paste,
  163. },
  164. toasterEnabled: {
  165. defaultValue: true,
  166. description: "Enable toast notifications.",
  167. category: categories.misc,
  168. },
  169. useElevatedFetch: {
  170. defaultValue: false,
  171. description:
  172. "Query AniList API with elevated browser access (this might solve some API issues).",
  173. category: categories.misc,
  174. },
  175. };
  176.  
  177. class ColorFunctions {
  178. static hexToRgb(hex) {
  179. const r = parseInt(hex.slice(1, 3), 16);
  180. const g = parseInt(hex.slice(3, 5), 16);
  181. const b = parseInt(hex.slice(5, 7), 16);
  182.  
  183. return `${r}, ${g}, ${b}`;
  184. }
  185.  
  186. static rgbToHex(rgb) {
  187. const [r, g, b] = rgb.split(",");
  188. const hex = this.generateHex(r, g, b);
  189. return hex;
  190. }
  191.  
  192. static generateHex(r, g, b) {
  193. return (
  194. "#" +
  195. [r, g, b]
  196. .map((x) => {
  197. const hex = Number(x).toString(16);
  198. return hex.length === 1 ? "0" + hex : hex;
  199. })
  200. .join("")
  201. );
  202. }
  203.  
  204. static defaultColors = [
  205. "blue",
  206. "purple",
  207. "green",
  208. "orange",
  209. "red",
  210. "pink",
  211. "gray",
  212. ];
  213.  
  214. static defaultColorRgb = {
  215. gray: "103, 123, 148",
  216. blue: "61, 180, 242",
  217. purple: "192, 99, 255",
  218. green: "76, 202, 81",
  219. orange: "239, 136, 26",
  220. red: "225, 51, 51",
  221. pink: "252, 157, 214",
  222. };
  223.  
  224. static handleAnilistColor(color) {
  225. if (this.defaultColors.includes(color)) {
  226. return this.defaultColorRgb[color];
  227. }
  228.  
  229. return this.hexToRgb(color);
  230. }
  231. }
  232.  
  233. class DOM {
  234. static create(element, classes = null, children = null) {
  235. const el = document.createElement(element);
  236. if (classes !== null) {
  237. for (const className of classes?.split(" ")) {
  238. if (className.startsWith("#")) {
  239. el.setAttribute("id", `void-${className.slice(1)}`);
  240. continue;
  241. }
  242. el.classList.add(`void-${className}`);
  243. }
  244. }
  245.  
  246. if (children) {
  247. if (Array.isArray(children)) {
  248. el.append(...children);
  249. } else {
  250. el.append(children);
  251. }
  252. }
  253.  
  254. return el;
  255. }
  256.  
  257. static getOrCreate(element, classes) {
  258. const id = classes
  259. .split(" ")
  260. .find((className) => className.startsWith("#"));
  261. return this.get(id) ?? this.create(element, classes);
  262. }
  263.  
  264. static get(selector) {
  265. return document.querySelector(selector);
  266. }
  267.  
  268. static getAll(selector) {
  269. return document.querySelectorAll(selector);
  270. }
  271. }
  272.  
  273. const ColorPicker = (value, onChange) => {
  274. const container = DOM.create("div", "color-picker-container");
  275. const colorPicker = DOM.create("input", "color-picker");
  276. colorPicker.setAttribute("type", "color");
  277. colorPicker.value = value;
  278. colorPicker.addEventListener("change", (event) => {
  279. onChange(event);
  280. });
  281.  
  282. container.append(colorPicker);
  283. const inputField = DOM.create("input", "color-picker-input");
  284. inputField.value = value ?? "#";
  285. inputField.addEventListener("change", (event) => {
  286. onChange(event);
  287. });
  288. container.append(inputField);
  289. return container;
  290. };
  291.  
  292. const InputField = (value, onChange, classes) => {
  293. const inputField = DOM.create(
  294. "input",
  295. transformClasses("input", classes)
  296. );
  297. inputField.value = value;
  298. inputField.addEventListener("change", (event) => {
  299. onChange(event);
  300. });
  301. return inputField;
  302. };
  303.  
  304. const Button = (text, onClick) => {
  305. const button = DOM.create("button", "button", text);
  306. button.addEventListener("click", (event) => {
  307. onClick(event);
  308. });
  309. return button;
  310. };
  311.  
  312. const IconButton = (text, onClick) => {
  313. const button = DOM.create("div", "icon-button", text);
  314. button.addEventListener("click", (event) => {
  315. onClick(event);
  316. });
  317. return button;
  318. };
  319.  
  320. const Note = (text) => {
  321. const note = DOM.create("div", "notice", text);
  322. return note;
  323. };
  324.  
  325. const Link = (text, href, target = "_blank", classes) => {
  326. const link = DOM.create("a", transformClasses("link", classes), text);
  327. link.setAttribute("href", href);
  328. link.setAttribute("target", target);
  329. return link;
  330. };
  331.  
  332. const TextArea = (text, onChange, classes) => {
  333. const textArea = DOM.create(
  334. "textarea",
  335. transformClasses("textarea", classes),
  336. text
  337. );
  338. textArea.addEventListener("change", (event) => {
  339. onChange(event);
  340. });
  341.  
  342. return textArea;
  343. };
  344.  
  345. const Toast = (message, type) => {
  346. const toast = DOM.create(
  347. "div",
  348. transformClasses("toast", type),
  349. message
  350. );
  351. return toast;
  352. };
  353.  
  354. const Select = (options) => {
  355. const container = DOM.create("div", "select");
  356. for (const option of options) {
  357. container.append(option);
  358. }
  359. return container;
  360. };
  361.  
  362. const Option$1 = (value, selected, onClick) => {
  363. const option = DOM.create("div", "option", value);
  364. if (selected) {
  365. option.classList.add("active");
  366. }
  367. option.addEventListener("click", onClick);
  368. return option;
  369. };
  370.  
  371. const Label = (text, element) => {
  372. const container = DOM.create("div", "label-container");
  373. const label = DOM.create("label", "label-span", text);
  374. const id = Math.random();
  375. label.setAttribute("for", id);
  376. element.setAttribute("id", id);
  377. container.append(label, element);
  378. return container;
  379. };
  380.  
  381. const Table = (head, body) => {
  382. const table = DOM.create("table", "table", [head, body]);
  383. return table;
  384. };
  385.  
  386. const TableHead = (...headers) => {
  387. const headerCells = headers.map((header) =>
  388. DOM.create("th", null, header)
  389. );
  390. const headerRow = DOM.create("tr", null, headerCells);
  391. const head = DOM.create("thead", null, headerRow);
  392. return head;
  393. };
  394.  
  395. const TableBody = (rows) => {
  396. const tableBody = DOM.create("tbody", null, rows);
  397. return tableBody;
  398. };
  399.  
  400. const Checkbox = (checked, onChange, title, disabled = false) => {
  401. const checkbox = DOM.create("input", "checkbox");
  402. checkbox.setAttribute("type", "checkbox");
  403. checkbox.checked = checked;
  404.  
  405. if (disabled) {
  406. checkbox.setAttribute("disabled", "");
  407. }
  408.  
  409. checkbox.addEventListener("change", onChange);
  410. checkbox.title = title;
  411. return checkbox;
  412. };
  413.  
  414. const SettingLabel = (text, input) => {
  415. const container = DOM.create("div", "setting-label-container", input);
  416. const label = DOM.create("label", "setting-label", text);
  417. const id = Math.random();
  418. label.setAttribute("for", id);
  419. input.setAttribute("id", id);
  420. container.append(label);
  421. return container;
  422. };
  423.  
  424. const transformClasses = (base, additional) => {
  425. let classes = base;
  426. if (additional) {
  427. classes += ` ${additional}`;
  428. }
  429. return classes;
  430. };
  431.  
  432. const toastTypes = {
  433. info: "info",
  434. success: "success",
  435. warning: "warning",
  436. error: "error",
  437. };
  438.  
  439. const toastLevels = {
  440. info: 0,
  441. success: 1,
  442. warning: 2,
  443. error: 3,
  444. };
  445.  
  446. const toastDurations = [1, 3, 5, 10];
  447.  
  448. const toastLocations = [
  449. "top-left",
  450. "top-right",
  451. "bottom-left",
  452. "bottom-right",
  453. ];
  454.  
  455. class ToasterConfig {
  456. toastLevel;
  457. duration;
  458. location;
  459. constructor(config) {
  460. this.toastLevel = config?.toastLevel ?? 2;
  461. this.duration = config?.duration ?? 5;
  462. this.location = config.location ?? "bottom-left";
  463. }
  464. }
  465.  
  466. class ToastInstance {
  467. type;
  468. message;
  469. duration;
  470. // durationLeft;
  471. // interval;
  472. constructor(message, type, duration) {
  473. this.type = type;
  474. this.message = message;
  475. this.duration = duration * 1000;
  476. }
  477.  
  478. toast() {
  479. const toast = Toast(this.message, this.type);
  480. this.durationLeft = this.duration;
  481. // This code can be used for a visual indicator
  482.  
  483. // this.interval = setInterval(
  484. // (toast) => {
  485. // if (this.durationLeft <= 0) {
  486. // this.delete(toast);
  487. // clearInterval(this.interval);
  488. // return;
  489. // }
  490. // this.durationLeft -= 100;
  491. // },
  492. // 100,
  493. // toast
  494. // );
  495.  
  496. setTimeout(() => {
  497. this.delete(toast);
  498. }, this.duration);
  499. return toast;
  500. }
  501.  
  502. delete(toast) {
  503. toast.remove();
  504. }
  505. }
  506.  
  507. class Toaster {
  508. static #config;
  509. static #configInLocalStorage = "void-verified-toaster-config";
  510. static #settings;
  511. static initializeToaster(settings) {
  512. this.#settings = settings;
  513. const config = JSON.parse(
  514. localStorage.getItem(this.#configInLocalStorage)
  515. );
  516. this.#config = new ToasterConfig(config);
  517. const toastContainer = DOM.create(
  518. "div",
  519. `#toast-container ${this.#config.location}`
  520. );
  521. document.body.append(toastContainer);
  522. }
  523.  
  524. static debug(message) {
  525. if (!this.#shouldToast(toastTypes.info)) {
  526. return;
  527. }
  528.  
  529. DOM.get("#void-toast-container").append(
  530. new ToastInstance(
  531. message,
  532. toastTypes.info,
  533. this.#config.duration
  534. ).toast()
  535. );
  536. }
  537.  
  538. static success(message) {
  539. if (!this.#shouldToast(toastTypes.success)) {
  540. return;
  541. }
  542.  
  543. DOM.get("#void-toast-container").append(
  544. new ToastInstance(
  545. message,
  546. toastTypes.success,
  547. this.#config.duration
  548. ).toast()
  549. );
  550. }
  551.  
  552. static warning(message) {
  553. if (!this.#shouldToast(toastTypes.warning)) {
  554. return;
  555. }
  556.  
  557. DOM.get("#void-toast-container").append(
  558. new ToastInstance(
  559. message,
  560. toastTypes.warning,
  561. this.#config.duration
  562. ).toast()
  563. );
  564. }
  565.  
  566. static error(message) {
  567. if (!this.#shouldToast(toastTypes.error)) {
  568. return;
  569. }
  570.  
  571. DOM.get("#void-toast-container").append(
  572. new ToastInstance(
  573. message,
  574. toastTypes.error,
  575. this.#config.duration
  576. ).toast()
  577. );
  578. }
  579.  
  580. static critical(message) {
  581. DOM.get("#void-toast-container").append(
  582. new ToastInstance(message, toastTypes.error, 8).toast()
  583. );
  584. }
  585.  
  586. static #shouldToast(type) {
  587. return (
  588. this.#settings.options.toasterEnabled.getValue() &&
  589. this.#config.toastLevel <= toastLevels[type]
  590. );
  591. }
  592.  
  593. static renderSettings(settingsUi) {
  594. const container = DOM.create("div");
  595.  
  596. container.append(DOM.create("h3", null, "Configure Toasts"));
  597.  
  598. container.append(
  599. DOM.create(
  600. "p",
  601. null,
  602. "Toasts are notifications that pop up in the corner of your screen when things are happening."
  603. )
  604. );
  605.  
  606. const options = Object.values(toastTypes).map((type) =>
  607. Option$1(
  608. type,
  609. this.#config.toastLevel === toastLevels[type],
  610. () => {
  611. this.#handleLevelChange(type);
  612. settingsUi.renderSettingsUiContent();
  613. }
  614. )
  615. );
  616. container.append(Label("Toast level", Select(options)));
  617.  
  618. const locationOptions = toastLocations.map((location) =>
  619. Option$1(location, this.#config.location === location, () => {
  620. this.#handleLocationChange(location);
  621. settingsUi.renderSettingsUiContent();
  622. })
  623. );
  624.  
  625. container.append(Label("Toast location", Select(locationOptions)));
  626.  
  627. const durationOptions = toastDurations.map((duration) =>
  628. Option$1(
  629. `${duration}s`,
  630. duration === this.#config.duration,
  631. () => {
  632. this.#handleDurationChange(duration);
  633. settingsUi.renderSettingsUiContent();
  634. }
  635. )
  636. );
  637.  
  638. container.append(Label("Toast duration", Select(durationOptions)));
  639.  
  640. container.append(
  641. Button("Test Toasts", () => {
  642. Toaster.debug("This is a debug toast.");
  643. Toaster.success("This is a success toast.");
  644. Toaster.warning("This is a warning toast.");
  645. Toaster.error("This is an error toast.");
  646. })
  647. );
  648.  
  649. return container;
  650. }
  651.  
  652. static #handleLevelChange(type) {
  653. this.#config.toastLevel = toastLevels[type];
  654. this.#saveConfig();
  655. }
  656.  
  657. static #handleLocationChange(location) {
  658. this.#config.location = location;
  659. this.#saveConfig();
  660.  
  661. const container = DOM.get("#void-toast-container");
  662. for (const className of container.classList) {
  663. container.classList.remove(className);
  664. }
  665. container.classList.add(`void-${location}`);
  666. }
  667.  
  668. static #handleDurationChange(duration) {
  669. this.#config.duration = duration;
  670. this.#saveConfig();
  671. }
  672.  
  673. static #saveConfig() {
  674. localStorage.setItem(
  675. this.#configInLocalStorage,
  676. JSON.stringify(this.#config)
  677. );
  678. }
  679. }
  680.  
  681. class AnilistAPI {
  682. settings;
  683. #userId;
  684. #url = "https://graphql.anilist.co";
  685. constructor(settings) {
  686. this.settings = settings;
  687. this.#userId = Number(JSON.parse(localStorage.getItem("auth")).id);
  688. }
  689.  
  690. async getActivityCss(activityId) {
  691. const query = `query ($activityId: Int) {
  692. Activity(id: $activityId) {
  693. ... on ListActivity {
  694. user {
  695. name
  696. about
  697. options {
  698. profileColor
  699. }
  700. }}
  701. ... on TextActivity {
  702. user {
  703. name
  704. about
  705. options {
  706. profileColor
  707. }
  708. }
  709. }
  710. ... on MessageActivity {
  711. recipient {
  712. name
  713. about
  714. options {
  715. profileColor
  716. }
  717. }
  718. }
  719. }
  720. }`;
  721.  
  722. const variables = { activityId };
  723. const options = this.#getQueryOptions(query, variables);
  724. try {
  725. const data = await this.#elevatedFetch(options);
  726. return data.Activity;
  727. } catch (error) {
  728. throw new Error("Error querying activity.", error);
  729. }
  730. }
  731.  
  732. async getUserAbout(username) {
  733. const query = `query ($username: String) {
  734. User(name: $username) {
  735. about
  736. }
  737. }`;
  738.  
  739. const variables = { username };
  740. const options = this.#getQueryOptions(query, variables);
  741. try {
  742. const data = await this.#elevatedFetch(options);
  743. return data.User.about;
  744. } catch (error) {
  745. throw new Error("Error querying user about.", error);
  746. }
  747. }
  748.  
  749. async saveUserAbout(about) {
  750. const query = `mutation ($about: String) {
  751. UpdateUser(about: $about) {
  752. about
  753. }
  754. }`;
  755. const variables = { about };
  756. const options = this.#getMutationOptions(query, variables);
  757. try {
  758. const data = await this.#elevatedFetch(options);
  759. return data;
  760. } catch (error) {
  761. throw new Error("failed to save user about.", error);
  762. }
  763. }
  764.  
  765. async saveUserColor(color) {
  766. const query = `mutation ($color: String) {
  767. UpdateUser(profileColor: $color) {
  768. options {
  769. profileColor
  770. }
  771. }
  772. }`;
  773.  
  774. const variables = { color };
  775. const options = this.#getMutationOptions(query, variables);
  776. try {
  777. const data = await this.#elevatedFetch(options);
  778. return data;
  779. } catch (error) {
  780. throw new Error("Failed to publish profile color", error);
  781. }
  782. }
  783.  
  784. async saveDonatorBadge(text) {
  785. const query = `mutation ($text: String) {
  786. UpdateUser(donatorBadge: $text) {
  787. donatorBadge
  788. }
  789. }`;
  790.  
  791. const variables = { text };
  792. const options = this.#getMutationOptions(query, variables);
  793. try {
  794. const data = await this.#elevatedFetch(options);
  795. return data;
  796. } catch (error) {
  797. throw new Error("Failed to publish donator badge", error);
  798. }
  799. }
  800.  
  801. async queryVerifiedUsers() {
  802. const accountUser = await this.queryUser(this.settings.anilistUser);
  803. this.settings.updateUserFromApi(accountUser);
  804. await this.#queryUsers(1);
  805. }
  806.  
  807. async queryUser(username) {
  808. const variables = { username };
  809. const query = `query ($username: String!) {
  810. User(name: $username) {
  811. name
  812. id
  813. avatar {
  814. large
  815. }
  816. bannerImage
  817. options {
  818. profileColor
  819. }
  820. }
  821. }
  822. `;
  823.  
  824. const options = this.#getQueryOptions(query, variables);
  825.  
  826. try {
  827. const data = await this.#elevatedFetch(options);
  828. return data.User;
  829. } catch (error) {
  830. throw new Error("Failed to query user from Anilist API", error);
  831. }
  832. }
  833.  
  834. async #elevatedFetch(options) {
  835. const runElevated =
  836. this.settings.options.useElevatedFetch.getValue();
  837. if (runElevated && GM.xmlHttpRequest) {
  838. try {
  839. const response = await GM.xmlHttpRequest({
  840. method: "POST",
  841. url: this.#url,
  842. data: options.body,
  843. headers: options.headers,
  844. });
  845. const data = JSON.parse(response.response);
  846. return data.data;
  847. } catch (error) {
  848. if (
  849. error.error.includes("Request was blocked by the user")
  850. ) {
  851. Toaster.warning(
  852. "Elevated access has been enabled in the userscript settings but user has refused permissions to run it. Using regular fetch."
  853. );
  854. } else {
  855. Toaster.debug(
  856. "Could not query AniList API with elevated access. Using regular fetch."
  857. );
  858. }
  859. console.error(error);
  860. }
  861. }
  862.  
  863. const response = await fetch(this.#url, options);
  864. const data = await response.json();
  865. return data.data;
  866. }
  867.  
  868. async #queryUsers(page) {
  869. const variables = { page, userId: this.#userId };
  870. const query = `query ($page: Int, $userId: Int!) {
  871. Page(page: $page) {
  872. following(userId: $userId) {
  873. name
  874. id
  875. avatar {
  876. large
  877. }
  878. bannerImage
  879. options {
  880. profileColor
  881. }
  882. },
  883. pageInfo {
  884. total
  885. perPage
  886. currentPage
  887. lastPage
  888. hasNextPage
  889. }
  890. }
  891. }
  892. `;
  893.  
  894. const options = this.#getQueryOptions(query, variables);
  895.  
  896. try {
  897. const data = await this.#elevatedFetch(options);
  898. this.#handleQueriedUsers(data.Page.following);
  899. const pageInfo = data.Page.pageInfo;
  900. if (pageInfo.hasNextPage) {
  901. await this.#queryUsers(pageInfo.currentPage + 1);
  902. }
  903. } catch (error) {
  904. throw new Error("Failed to query followed users.", error);
  905. }
  906. }
  907.  
  908. #handleQueriedUsers(users) {
  909. for (const user of users) {
  910. this.settings.updateUserFromApi(user);
  911. }
  912. }
  913.  
  914. #getQueryOptions(query, variables) {
  915. const options = {
  916. method: "POST",
  917. headers: {
  918. "Content-Type": "application/json",
  919. Accept: "application/json",
  920. },
  921. body: JSON.stringify({
  922. query,
  923. variables,
  924. }),
  925. };
  926.  
  927. if (this.settings.auth?.token) {
  928. options.headers.Authorization = `Bearer ${this.settings.auth.token}`;
  929. }
  930.  
  931. return options;
  932. }
  933.  
  934. #getMutationOptions(query, variables) {
  935. if (!this.settings.auth?.token) {
  936. Toaster.error(
  937. "Tried to make API query without authorizing VoidVerified. You can do so in the settings."
  938. );
  939. throw new Error("VoidVerified is missing auth token.");
  940. }
  941. let queryOptions = this.#getQueryOptions(query, variables);
  942. return queryOptions;
  943. }
  944. }
  945.  
  946. class Option {
  947. value;
  948. defaultValue;
  949. description;
  950. category;
  951. authRequired;
  952. constructor(option) {
  953. this.defaultValue = option.defaultValue;
  954. this.description = option.description;
  955. this.category = option.category;
  956. this.authRequired = option.authRequired;
  957. }
  958.  
  959. getValue() {
  960. if (this.value === "") {
  961. return this.defaultValue;
  962. }
  963. return this.value ?? this.defaultValue;
  964. }
  965. }
  966.  
  967. class Settings {
  968. localStorageUsers = "void-verified-users";
  969. localStorageSettings = "void-verified-settings";
  970. localStorageAuth = "void-verified-auth";
  971. version = GM_info.script.version;
  972. auth = null;
  973. anilistUser;
  974.  
  975. verifiedUsers = [];
  976.  
  977. options = {};
  978.  
  979. constructor() {
  980. this.verifiedUsers =
  981. JSON.parse(localStorage.getItem(this.localStorageUsers)) ?? [];
  982.  
  983. const settingsInLocalStorage =
  984. JSON.parse(localStorage.getItem(this.localStorageSettings)) ??
  985. {};
  986.  
  987. for (const [key, value] of Object.entries(defaultSettings)) {
  988. this.options[key] = new Option(value);
  989. }
  990.  
  991. for (const [key, value] of Object.entries(settingsInLocalStorage)) {
  992. if (!this.options[key]) {
  993. continue;
  994. }
  995. this.options[key].value = value.value;
  996. }
  997.  
  998. this.auth =
  999. JSON.parse(localStorage.getItem(this.localStorageAuth)) ?? null;
  1000.  
  1001. const auth = JSON.parse(localStorage.getItem("auth"));
  1002. this.anilistUser = auth?.name;
  1003. }
  1004.  
  1005. async verifyUser(username) {
  1006. if (
  1007. this.verifiedUsers.find(
  1008. (user) =>
  1009. user.username.toLowerCase() === username.toLowerCase()
  1010. )
  1011. ) {
  1012. return;
  1013. }
  1014.  
  1015. this.verifiedUsers.push({ username });
  1016. localStorage.setItem(
  1017. this.localStorageUsers,
  1018. JSON.stringify(this.verifiedUsers)
  1019. );
  1020.  
  1021. try {
  1022. Toaster.debug(`Querying ${username}.`);
  1023. const anilistAPI = new AnilistAPI(this);
  1024. const user = await anilistAPI.queryUser(username);
  1025. this.updateUserFromApi(user);
  1026. } catch (error) {
  1027. Toaster.error("Failed to query new user.");
  1028. console.error(error);
  1029. }
  1030. }
  1031.  
  1032. getUser(username) {
  1033. return this.verifiedUsers.find(
  1034. (user) => user.username === username
  1035. );
  1036. }
  1037.  
  1038. isVerified(username) {
  1039. return this.verifiedUsers.some(
  1040. (user) => user.username === username
  1041. );
  1042. }
  1043.  
  1044. updateUserOption(username, key, value) {
  1045. this.verifiedUsers = this.verifiedUsers.map((u) =>
  1046. u.username === username
  1047. ? {
  1048. ...u,
  1049. [key]: value,
  1050. }
  1051. : u
  1052. );
  1053. localStorage.setItem(
  1054. this.localStorageUsers,
  1055. JSON.stringify(this.verifiedUsers)
  1056. );
  1057. }
  1058.  
  1059. updateUserFromApi(apiUser) {
  1060. let user = this.#findVerifiedUser(apiUser);
  1061.  
  1062. if (!user) {
  1063. return;
  1064. }
  1065.  
  1066. const newUser = this.#mapApiUser(user, apiUser);
  1067. this.#mapVerifiedUsers(newUser);
  1068.  
  1069. localStorage.setItem(
  1070. this.localStorageUsers,
  1071. JSON.stringify(this.verifiedUsers)
  1072. );
  1073. }
  1074.  
  1075. #findVerifiedUser(apiUser) {
  1076. let user = this.verifiedUsers.find(
  1077. (u) => u.id && u.id === apiUser.id
  1078. );
  1079.  
  1080. if (user) {
  1081. return user;
  1082. }
  1083.  
  1084. return this.verifiedUsers.find(
  1085. (u) => u.username.toLowerCase() === apiUser.name.toLowerCase()
  1086. );
  1087. }
  1088.  
  1089. #mapVerifiedUsers(newUser) {
  1090. if (this.verifiedUsers.find((u) => u.id && u.id === newUser.id)) {
  1091. this.verifiedUsers = this.verifiedUsers.map((u) =>
  1092. u.id === newUser.id ? newUser : u
  1093. );
  1094. return;
  1095. }
  1096. this.verifiedUsers = this.verifiedUsers.map((u) =>
  1097. u.username.toLowerCase() === newUser.username.toLowerCase()
  1098. ? newUser
  1099. : u
  1100. );
  1101. }
  1102.  
  1103. #mapApiUser(user, apiUser) {
  1104. let userObject = { ...user };
  1105.  
  1106. userObject.color = ColorFunctions.handleAnilistColor(
  1107. apiUser.options.profileColor
  1108. );
  1109.  
  1110. userObject.username = apiUser.name;
  1111. userObject.avatar = apiUser.avatar.large;
  1112. userObject.banner = apiUser.bannerImage;
  1113. userObject.id = apiUser.id;
  1114. userObject.lastFetch = new Date();
  1115.  
  1116. if (
  1117. this.options.quickAccessBadge.getValue() ||
  1118. user.quickAccessBadge
  1119. ) {
  1120. if (
  1121. (user.avatar && user.avatar !== userObject.avatar) ||
  1122. (user.color && user.color !== userObject.color) ||
  1123. (user.banner && user.banner !== userObject.banner) ||
  1124. (user.username &&
  1125. user.username.toLowerCase() !==
  1126. userObject.username.toLowerCase())
  1127. ) {
  1128. userObject.quickAccessBadgeDisplay = true;
  1129. }
  1130. }
  1131.  
  1132. return userObject;
  1133. }
  1134.  
  1135. saveAuthToken(tokenObject) {
  1136. this.auth = tokenObject;
  1137. localStorage.setItem(
  1138. this.localStorageAuth,
  1139. JSON.stringify(tokenObject)
  1140. );
  1141. }
  1142.  
  1143. removeAuthToken() {
  1144. this.auth = null;
  1145. localStorage.removeItem(this.localStorageAuth);
  1146. }
  1147.  
  1148. removeUser(username) {
  1149. this.verifiedUsers = this.verifiedUsers.filter(
  1150. (user) => user.username !== username
  1151. );
  1152. localStorage.setItem(
  1153. this.localStorageUsers,
  1154. JSON.stringify(this.verifiedUsers)
  1155. );
  1156. }
  1157.  
  1158. saveSettingToLocalStorage(key, value) {
  1159. let localSettings = JSON.parse(
  1160. localStorage.getItem(this.localStorageSettings)
  1161. );
  1162.  
  1163. this.options[key].value = value;
  1164.  
  1165. if (localSettings === null) {
  1166. const settings = {
  1167. [key]: value,
  1168. };
  1169. localStorage.setItem(
  1170. this.localStorageSettings,
  1171. JSON.stringify(settings)
  1172. );
  1173. return;
  1174. }
  1175.  
  1176. localSettings[key] = { value };
  1177. localStorage.setItem(
  1178. this.localStorageSettings,
  1179. JSON.stringify(localSettings)
  1180. );
  1181. }
  1182. }
  1183.  
  1184. class StyleHandler {
  1185. settings;
  1186. usernameStyles = "";
  1187. highlightStyles = "";
  1188. otherStyles = "";
  1189.  
  1190. constructor(settings) {
  1191. this.settings = settings;
  1192. }
  1193.  
  1194. refreshStyles() {
  1195. this.createStyles();
  1196. this.createStyleLink(this.usernameStyles, "username");
  1197. this.createStyleLink(this.highlightStyles, "highlight");
  1198. this.createStyleLink(this.otherStyles, "other");
  1199. }
  1200.  
  1201. createStyles() {
  1202. this.usernameStyles = "";
  1203. this.otherStyles = "";
  1204.  
  1205. for (const user of this.settings.verifiedUsers) {
  1206. if (
  1207. this.settings.options.enabledForUsername.getValue() ||
  1208. user.enabledForUsername
  1209. ) {
  1210. this.createUsernameCSS(user);
  1211. }
  1212. }
  1213.  
  1214. if (this.settings.options.moveSubscribeButtons.getValue()) {
  1215. this.otherStyles += `
  1216. .has-label::before {
  1217. top: -30px !important;
  1218. left: unset !important;
  1219. right: -10px;
  1220. }
  1221. .has-label[label="Unsubscribe"],
  1222. .has-label[label="Subscribe"] {
  1223. font-size: 0.875em !important;
  1224. }
  1225. .has-label[label="Unsubscribe"] {
  1226. color: rgba(var(--color-green),.8);
  1227. }
  1228. `;
  1229. }
  1230.  
  1231. this.createHighlightStyles();
  1232.  
  1233. if (this.settings.options.hideLikeCount.getValue()) {
  1234. this.otherStyles += `
  1235. .like-wrap .count {
  1236. display: none;
  1237. }
  1238. `;
  1239. }
  1240. }
  1241.  
  1242. createHighlightStyles() {
  1243. this.highlightStyles = "";
  1244. for (const user of this.settings.verifiedUsers) {
  1245. if (
  1246. this.settings.options.highlightEnabled.getValue() ||
  1247. user.highlightEnabled
  1248. ) {
  1249. this.createHighlightCSS(
  1250. user,
  1251. `div.wrap:has( div.header > a.name[href*="/${user.username}/" i] )`
  1252. );
  1253. this.createHighlightCSS(
  1254. user,
  1255. `div.wrap:has( div.details > a.name[href*="/${user.username}/" i] )`
  1256. );
  1257. }
  1258.  
  1259. if (
  1260. this.settings.options.highlightEnabledForReplies.getValue() ||
  1261. user.highlightEnabledForReplies
  1262. ) {
  1263. this.createHighlightCSS(
  1264. user,
  1265. `div.reply:has( a.name[href*="/${user.username}/" i] )`
  1266. );
  1267. }
  1268.  
  1269. this.#createActivityCss(user);
  1270. }
  1271.  
  1272. this.disableHighlightOnSmallCards();
  1273. }
  1274.  
  1275. #createActivityCss(user) {
  1276. const colorUserActivity =
  1277. this.settings.options.colorUserActivity.getValue() ??
  1278. user.colorUserActivity;
  1279. const colorUserReplies =
  1280. this.settings.options.colorUserReplies.getValue() ??
  1281. user.colorUserReplies;
  1282.  
  1283. if (colorUserActivity) {
  1284. this.highlightStyles += `
  1285. div.wrap:has( div.header > a.name[href*="/${
  1286. user.username
  1287. }/"]) a,
  1288. div.wrap:has( div.details > a.name[href*="/${
  1289. user.username
  1290. }/"]) a
  1291. {
  1292. color: ${this.getUserColor(user)};
  1293. }
  1294. `;
  1295. }
  1296. if (colorUserReplies) {
  1297. this.highlightStyles += `
  1298. .reply:has(a.name[href*="/${user.username}/"]) a,
  1299. .reply:has(a.name[href*="/${
  1300. user.username
  1301. }/"]) .markdown-spoiler::before
  1302. {
  1303. color: ${this.getUserColor(user)};
  1304. }
  1305. `;
  1306. }
  1307. }
  1308.  
  1309. createUsernameCSS(user) {
  1310. this.usernameStyles += `
  1311. a.name[href*="/${user.username}/" i]::after {
  1312. content: "${
  1313. this.stringIsEmpty(user.sign) ??
  1314. this.settings.options.defaultSign.getValue()
  1315. }";
  1316. color: ${this.getUserColor(user) ?? "rgb(var(--color-blue))"}
  1317. }
  1318. `;
  1319. }
  1320.  
  1321. createHighlightCSS(user, selector) {
  1322. this.highlightStyles += `
  1323. ${selector} {
  1324. margin-right: -${this.settings.options.highlightSize.getValue()};
  1325. border-right: ${this.settings.options.highlightSize.getValue()} solid ${
  1326. this.getUserColor(user) ?? this.getDefaultHighlightColor()
  1327. };
  1328. border-radius: 5px;
  1329. }
  1330. `;
  1331. }
  1332.  
  1333. disableHighlightOnSmallCards() {
  1334. this.highlightStyles += `
  1335. div.wrap:has(div.small) {
  1336. margin-right: 0px !important;
  1337. border-right: 0px solid black !important;
  1338. }
  1339. `;
  1340. }
  1341.  
  1342. refreshHomePage() {
  1343. if (!this.settings.options.highlightEnabled.getValue()) {
  1344. return;
  1345. }
  1346. this.createHighlightStyles();
  1347. this.createStyleLink(this.highlightStyles, "highlight");
  1348. }
  1349.  
  1350. clearStyles(id) {
  1351. const styles = document.getElementById(
  1352. `void-verified-${id}-styles`
  1353. );
  1354. styles?.remove();
  1355. }
  1356.  
  1357. verifyProfile() {
  1358. if (!this.settings.options.enabledForProfileName.getValue()) {
  1359. return;
  1360. }
  1361.  
  1362. const username =
  1363. window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];
  1364.  
  1365. const user = this.settings.verifiedUsers.find(
  1366. (u) => u.username.toLowerCase() === username.toLowerCase()
  1367. );
  1368.  
  1369. if (!user) {
  1370. this.clearStyles("profile");
  1371. return;
  1372. }
  1373.  
  1374. const profileStyle = `
  1375. .name-wrapper h1.name::after {
  1376. content: "${
  1377. this.stringIsEmpty(user.sign) ??
  1378. this.settings.options.defaultSign.getValue()
  1379. }"
  1380. }
  1381. `;
  1382. this.createStyleLink(profileStyle, "profile");
  1383. }
  1384.  
  1385. getStyleLink(id) {
  1386. return document.getElementById(`void-verified-${id}-styles`);
  1387. }
  1388.  
  1389. copyUserColor() {
  1390. const usernameHeader = document.querySelector("h1.name");
  1391. const username = usernameHeader.innerHTML.trim();
  1392. const user = this.settings.verifiedUsers.find(
  1393. (u) => u.username === username
  1394. );
  1395.  
  1396. if (!user) {
  1397. return;
  1398. }
  1399.  
  1400. if (
  1401. !(
  1402. user.copyColorFromProfile ||
  1403. this.settings.options.copyColorFromProfile.getValue()
  1404. )
  1405. ) {
  1406. return;
  1407. }
  1408.  
  1409. const color =
  1410. getComputedStyle(usernameHeader).getPropertyValue(
  1411. "--color-blue"
  1412. );
  1413.  
  1414. this.settings.updateUserOption(user.username, "color", color);
  1415. }
  1416.  
  1417. getUserColor(user) {
  1418. return (
  1419. user.colorOverride ??
  1420. (user.color &&
  1421. (user.copyColorFromProfile ||
  1422. this.settings.options.copyColorFromProfile.getValue())
  1423. ? `rgb(${user.color})`
  1424. : undefined)
  1425. );
  1426. }
  1427.  
  1428. getDefaultHighlightColor() {
  1429. if (this.settings.options.useDefaultHighlightColor.getValue()) {
  1430. return this.settings.options.defaultHighlightColor.getValue();
  1431. }
  1432. return "rgb(var(--color-blue))";
  1433. }
  1434.  
  1435. createStyleLink(styles, id) {
  1436. const oldLink = document.getElementById(
  1437. `void-verified-${id}-styles`
  1438. );
  1439. const link = document.createElement("link");
  1440. link.setAttribute("id", `void-verified-${id}-styles`);
  1441. link.setAttribute("rel", "stylesheet");
  1442. link.setAttribute("type", "text/css");
  1443. link.setAttribute(
  1444. "href",
  1445. "data:text/css;charset=UTF-8," + encodeURIComponent(styles)
  1446. );
  1447. document.head?.append(link);
  1448. oldLink?.remove();
  1449. return link;
  1450. }
  1451.  
  1452. stringIsEmpty(string) {
  1453. if (!string || string.length === 0) {
  1454. return undefined;
  1455. }
  1456. return string;
  1457. }
  1458. }
  1459.  
  1460. class GlobalCSS {
  1461. settings;
  1462. styleHandler;
  1463.  
  1464. styleId = "global-css";
  1465. isCleared = false;
  1466.  
  1467. cssInLocalStorage = "void-verified-global-css";
  1468. constructor(settings) {
  1469. this.settings = settings;
  1470. this.styleHandler = new StyleHandler(settings);
  1471.  
  1472. this.css = localStorage.getItem(this.cssInLocalStorage) ?? "";
  1473. }
  1474.  
  1475. createCss() {
  1476. if (!this.settings.options.globalCssEnabled.getValue()) {
  1477. this.styleHandler.clearStyles(this.styleId);
  1478. return;
  1479. }
  1480.  
  1481. if (!this.shouldRender()) {
  1482. return;
  1483. }
  1484.  
  1485. this.isCleared = false;
  1486. this.styleHandler.createStyleLink(this.css, this.styleId);
  1487. }
  1488.  
  1489. updateCss(css) {
  1490. this.css = css;
  1491. this.createCss();
  1492. localStorage.setItem(this.cssInLocalStorage, css);
  1493. }
  1494.  
  1495. clearCssForProfile() {
  1496. if (this.isCleared) {
  1497. return;
  1498. }
  1499. if (!this.shouldRender()) {
  1500. this.styleHandler.clearStyles(this.styleId);
  1501. this.isCleared = true;
  1502. }
  1503. }
  1504.  
  1505. shouldRender() {
  1506. if (window.location.pathname.startsWith("/settings")) {
  1507. return false;
  1508. }
  1509.  
  1510. if (!this.settings.options.globalCssAutoDisable.getValue()) {
  1511. return true;
  1512. }
  1513.  
  1514. if (
  1515. !window.location.pathname.startsWith("/user/") &&
  1516. !window.location.pathname.startsWith("/activity/")
  1517. ) {
  1518. return true;
  1519. }
  1520.  
  1521. const profileCustomCss = document.getElementById(
  1522. "customCSS-automail-styles"
  1523. );
  1524.  
  1525. const styleHandler = new StyleHandler(this.settings);
  1526. const voidActivityStyles =
  1527. styleHandler.getStyleLink("activity-css");
  1528. const voidUserStyles = styleHandler.getStyleLink("user-css");
  1529.  
  1530. if (voidActivityStyles || voidUserStyles) {
  1531. return false;
  1532. }
  1533.  
  1534. if (!profileCustomCss) {
  1535. return true;
  1536. }
  1537.  
  1538. const shouldRender = profileCustomCss.innerHTML.trim().length === 0;
  1539. return shouldRender;
  1540. }
  1541. }
  1542.  
  1543. class ActivityHandler {
  1544. settings;
  1545. constructor(settings) {
  1546. this.settings = settings;
  1547. }
  1548.  
  1549. moveAndDisplaySubscribeButton() {
  1550. if (!this.settings.options.moveSubscribeButtons.getValue()) {
  1551. return;
  1552. }
  1553.  
  1554. const subscribeButtons = document.querySelectorAll(
  1555. "span[label='Unsubscribe'], span[label='Subscribe']"
  1556. );
  1557. for (const subscribeButton of subscribeButtons) {
  1558. if (subscribeButton.parentNode.classList.contains("actions")) {
  1559. continue;
  1560. }
  1561.  
  1562. const container = subscribeButton.parentNode.parentNode;
  1563. const actions = container.querySelector(".actions");
  1564. actions.append(subscribeButton);
  1565. }
  1566. }
  1567. }
  1568.  
  1569. class ImageHostBase {
  1570. conventToBase64(image) {
  1571. return new Promise(function (resolve, reject) {
  1572. var reader = new FileReader();
  1573. reader.onloadend = function (e) {
  1574. resolve({
  1575. fileName: this.name,
  1576. result: e.target.result,
  1577. error: e.target.error,
  1578. });
  1579. };
  1580. reader.readAsDataURL(image);
  1581. });
  1582. }
  1583. }
  1584.  
  1585. const imageHosts = {
  1586. imgbb: "imgbb",
  1587. imgur: "imgur",
  1588. catbox: "catbox",
  1589. };
  1590.  
  1591. const imageHostConfiguration = {
  1592. selectedHost: imageHosts.catbox,
  1593. configurations: {
  1594. imgbb: {
  1595. name: "imgbb",
  1596. apiKey: "",
  1597. },
  1598. imgur: {
  1599. name: "imgur",
  1600. clientId: "",
  1601. clientSecret: "",
  1602. expires: null,
  1603. refreshToken: null,
  1604. authToken: null,
  1605. },
  1606. catbox: {
  1607. name: "catbox",
  1608. userHash: "",
  1609. },
  1610. },
  1611. };
  1612.  
  1613. class ImageHostService {
  1614. #configuration;
  1615. #localStorage = "void-verified-image-host-config";
  1616. constructor() {
  1617. const config = JSON.parse(localStorage.getItem(this.#localStorage));
  1618. if (!config) {
  1619. localStorage.setItem(
  1620. this.#localStorage,
  1621. JSON.stringify(imageHostConfiguration)
  1622. );
  1623. } else {
  1624. for (const key of Object.keys(
  1625. imageHostConfiguration.configurations
  1626. )) {
  1627. if (config.configurations[key]) {
  1628. continue;
  1629. }
  1630. config.configurations[key] =
  1631. imageHostConfiguration.configurations[key];
  1632. }
  1633. localStorage.setItem(
  1634. this.#localStorage,
  1635. JSON.stringify(config)
  1636. );
  1637. }
  1638.  
  1639. this.#configuration = config ?? imageHostConfiguration;
  1640. }
  1641.  
  1642. getImageHostConfiguration(host) {
  1643. return this.#configuration.configurations[host];
  1644. }
  1645.  
  1646. getSelectedHost() {
  1647. return this.#configuration.selectedHost;
  1648. }
  1649.  
  1650. setSelectedHost(host) {
  1651. this.#configuration.selectedHost = host;
  1652. localStorage.setItem(
  1653. this.#localStorage,
  1654. JSON.stringify(this.#configuration)
  1655. );
  1656. }
  1657.  
  1658. setImageHostConfiguration(host, config) {
  1659. this.#configuration.configurations[host] = config;
  1660. localStorage.setItem(
  1661. this.#localStorage,
  1662. JSON.stringify(this.#configuration)
  1663. );
  1664. }
  1665. }
  1666.  
  1667. class CatboxConfig {
  1668. userHash;
  1669. name = "catbox";
  1670. constructor(config) {
  1671. this.userHash = config?.userHash ?? "";
  1672. }
  1673. }
  1674.  
  1675. class CatboxAPI extends ImageHostBase {
  1676. #url = "https://catbox.moe/user/api.php";
  1677. #configuration;
  1678. constructor(configuration) {
  1679. super();
  1680. this.#configuration = new CatboxConfig(configuration);
  1681. }
  1682.  
  1683. async uploadImage(image) {
  1684. if (!image) {
  1685. return;
  1686. }
  1687.  
  1688. const form = new FormData();
  1689. form.append("reqtype", "fileupload");
  1690. form.append("fileToUpload", image);
  1691.  
  1692. if (this.#configuration.userHash !== "") {
  1693. form.append("userhash", this.#configuration.userHash);
  1694. }
  1695.  
  1696. try {
  1697. if (GM.xmlHttpRequest) {
  1698. Toaster.debug("Uploading image to catbox.");
  1699. const response = await GM.xmlHttpRequest({
  1700. method: "POST",
  1701. url: this.#url,
  1702. data: form,
  1703. });
  1704.  
  1705. if (response.status !== 200) {
  1706. console.error(response.response);
  1707. throw new Error("Image upload to catbox failed.");
  1708. }
  1709.  
  1710. return response.response;
  1711. }
  1712. } catch (error) {
  1713. Toaster.error("Failed to upload image to catbox.");
  1714. return null;
  1715. }
  1716. }
  1717.  
  1718. renderSettings() {
  1719. const container = DOM.create("div");
  1720.  
  1721. container.append(
  1722. Label(
  1723. "Userhash",
  1724. InputField(this.#configuration.userHash, (event) => {
  1725. this.#updateUserhash(event, this.#configuration);
  1726. })
  1727. )
  1728. );
  1729.  
  1730. const p = Note(
  1731. "Catbox.moe works out of the box, but you can provide your userhash to upload images to your account. Your userscript manager should promt you to allow xmlHttpRequest. This is required to upload images to Catbox on AniList."
  1732. );
  1733. container.append(p);
  1734. return container;
  1735. }
  1736.  
  1737. #updateUserhash(event, configuration) {
  1738. const userHash = event.target.value;
  1739. const config = {
  1740. ...configuration,
  1741. userHash,
  1742. };
  1743. new ImageHostService().setImageHostConfiguration(
  1744. config.name,
  1745. config
  1746. );
  1747. }
  1748. }
  1749.  
  1750. class ImgbbAPI extends ImageHostBase {
  1751. #url = "https://api.imgbb.com/1/upload";
  1752. #configuration;
  1753. constructor(configuration) {
  1754. super();
  1755. this.#configuration = configuration;
  1756. }
  1757.  
  1758. async uploadImage(image) {
  1759. const file = await this.conventToBase64(image);
  1760. if (!file.result) {
  1761. return;
  1762. }
  1763.  
  1764. if (!this.#configuration.apiKey) {
  1765. return;
  1766. }
  1767.  
  1768. const base64 = file.result.split("base64,")[1];
  1769.  
  1770. const settings = {
  1771. method: "POST",
  1772. headers: {
  1773. Accept: "application/json",
  1774. "Content-Type": "application/x-www-form-urlencoded",
  1775. },
  1776. body:
  1777. "image=" +
  1778. encodeURIComponent(base64) +
  1779. "&name=" +
  1780. image.name.split(".")[0],
  1781. };
  1782.  
  1783. try {
  1784. Toaster.debug("Uploading image to imgbb.");
  1785. const response = await fetch(
  1786. `${this.#url}?key=${this.#configuration.apiKey}`,
  1787. settings
  1788. );
  1789. const data = await response.json();
  1790. Toaster.success("Uploaded image to imgbb.");
  1791. return data.data.url;
  1792. } catch (error) {
  1793. Toaster.error("Failed to upload image to imgbb.");
  1794. console.error(error);
  1795. return null;
  1796. }
  1797. }
  1798.  
  1799. renderSettings() {
  1800. const container = document.createElement("div");
  1801.  
  1802. const apiKey = Label(
  1803. "API key",
  1804. InputField(this.#configuration.apiKey, (event) => {
  1805. this.#updateApiKey(event, this.#configuration);
  1806. })
  1807. );
  1808.  
  1809. const note = Note(
  1810. "You need to get the API key from the following link: "
  1811. );
  1812. note.append(
  1813. Link("api.imgbb.com", "https://api.imgbb.com/", "_blank")
  1814. );
  1815. container.append(apiKey, note);
  1816.  
  1817. return container;
  1818. }
  1819.  
  1820. #updateApiKey(event, configuration) {
  1821. const apiKey = event.target.value;
  1822. const config = {
  1823. ...configuration,
  1824. apiKey,
  1825. };
  1826. new ImageHostService().setImageHostConfiguration(
  1827. config.name,
  1828. config
  1829. );
  1830. }
  1831. }
  1832.  
  1833. class ImgurAPI extends ImageHostBase {
  1834. #url = "https://api.imgur.com/3/image";
  1835. #configuration;
  1836. constructor(configuration) {
  1837. super();
  1838. this.#configuration = configuration;
  1839. }
  1840.  
  1841. async uploadImage(image) {
  1842. const file = await this.conventToBase64(image);
  1843. if (!file.result) {
  1844. return;
  1845. }
  1846.  
  1847. if (!this.#configuration.clientId) {
  1848. return;
  1849. }
  1850.  
  1851. const base64 = file.result.split("base64,")[1];
  1852.  
  1853. const formData = new FormData();
  1854. formData.append("image", base64);
  1855. formData.append("title", image.name.split(".")[0]);
  1856.  
  1857. const settings = {
  1858. method: "POST",
  1859. headers: {
  1860. Authorization: this.#configuration.authToken
  1861. ? `Bearer ${this.#configuration.authToken}`
  1862. : `Client-ID ${this.#configuration.clientId}`,
  1863. },
  1864. body: formData,
  1865. };
  1866.  
  1867. try {
  1868. Toaster.debug("Uploading image to imgur.");
  1869. const response = await fetch(this.#url, settings);
  1870. const data = await response.json();
  1871. Toaster.success("Uploaded image to imgur.");
  1872. return data.data.link;
  1873. } catch (error) {
  1874. Toaster.error("Failed to upload image to imgur.");
  1875. console.error("Failed to upload image to imgur.", error);
  1876. return null;
  1877. }
  1878. }
  1879.  
  1880. renderSettings(settingsUi) {
  1881. const container = DOM.create("div");
  1882.  
  1883. const clientId = Label(
  1884. "Client ID",
  1885. InputField(this.#configuration?.clientId ?? "", (event) => {
  1886. this.#updateConfig(event, "clientId", this.#configuration);
  1887. settingsUi.renderSettingsUiContent();
  1888. })
  1889. );
  1890.  
  1891. const clientSecret = Label(
  1892. "Client Secret",
  1893. InputField(this.#configuration?.clientSecret ?? "", (event) => {
  1894. this.#updateConfig(
  1895. event,
  1896. "clientSecret",
  1897. this.#configuration
  1898. );
  1899. settingsUi.renderSettingsUiContent();
  1900. })
  1901. );
  1902.  
  1903. container.append(clientId, clientSecret);
  1904.  
  1905. if (
  1906. this.#configuration.clientId &&
  1907. this.#configuration.clientSecret &&
  1908. !this.#configuration.authToken
  1909. ) {
  1910. const authLink = DOM.create("a", null, "Authorize Imgur");
  1911. authLink.classList.add("button");
  1912. authLink.setAttribute(
  1913. "href",
  1914. `https://api.imgur.com/oauth2/authorize?client_id=${
  1915. this.#configuration.clientId
  1916. }&response_type=token`
  1917. );
  1918. container.append(authLink);
  1919. }
  1920.  
  1921. if (this.#configuration.authToken) {
  1922. const revokeAuthButton = DOM.create(
  1923. "button",
  1924. null,
  1925. "Clear Authorization"
  1926. );
  1927. revokeAuthButton.classList.add("button");
  1928. revokeAuthButton.addEventListener("click", () => {
  1929. this.#revokeAuth();
  1930. settingsUi.renderSettingsUiContent();
  1931. });
  1932. container.append(revokeAuthButton);
  1933. }
  1934.  
  1935. this.#renderNote(container);
  1936. return container;
  1937. }
  1938.  
  1939. handleAuth() {
  1940. const hash = window.location.hash.substring(1);
  1941. if (!hash) {
  1942. return;
  1943. }
  1944.  
  1945. const [path, token, expires, _, refreshToken] = hash.split("&");
  1946.  
  1947. if (path !== "void_imgur") {
  1948. return;
  1949. }
  1950.  
  1951. let config = { ...this.#configuration };
  1952. config.authToken = token.split("=")[1];
  1953. config.refreshToken = refreshToken.split("=")[1];
  1954.  
  1955. config.expires = new Date(
  1956. new Date().getTime() + Number(expires.split("=")[1])
  1957. );
  1958.  
  1959. new ImageHostService().setImageHostConfiguration(
  1960. imageHosts.imgur,
  1961. config
  1962. );
  1963.  
  1964. window.history.replaceState(
  1965. null,
  1966. "",
  1967. "https://anilist.co/settings/developer"
  1968. );
  1969. }
  1970.  
  1971. async refreshAuthToken() {
  1972. if (
  1973. !this.#configuration.refreshToken ||
  1974. !this.#configuration.clientSecret ||
  1975. !this.#configuration.clientId
  1976. ) {
  1977. return;
  1978. }
  1979.  
  1980. if (new Date() < new Date(this.#configuration.expires)) {
  1981. return;
  1982. }
  1983.  
  1984. const formData = new FormData();
  1985. formData.append("refresh_token", this.#configuration.refreshToken);
  1986. formData.append("client_id", this.#configuration.clientId);
  1987. formData.append("client_secret", this.#configuration.clientSecret);
  1988. formData.append("grant_type", "refresh_token");
  1989.  
  1990. try {
  1991. Toaster.debug("Refreshing imgur token.");
  1992. const response = await fetch(
  1993. "https://api.imgur.com/oauth2/token",
  1994. {
  1995. method: "POST",
  1996. body: formData,
  1997. }
  1998. );
  1999. if (!response.status === 200) {
  2000. console.error("Failed to reauthorize Imgur");
  2001. return;
  2002. }
  2003. const data = await response.json();
  2004. const config = {
  2005. ...this.#configuration,
  2006. authToken: data.access_token,
  2007. expires: new Date(new Date().getTime() + data.expires_in),
  2008. };
  2009. new ImageHostService().setImageHostConfiguration(
  2010. imageHosts.imgur,
  2011. config
  2012. );
  2013. Toaster.success("Refreshed imgur access token.");
  2014. } catch (error) {
  2015. Toaster.error("Error while refreshing imgur token.");
  2016. console.error(error);
  2017. }
  2018. }
  2019.  
  2020. #renderNote(container) {
  2021. const note = Note("How to setup Imgur integration");
  2022.  
  2023. const registerLink = Link(
  2024. "api.imgur.com",
  2025. "https://api.imgur.com/oauth2/addclient",
  2026. "_blank"
  2027. );
  2028. const stepList = DOM.create("ol", null, [
  2029. DOM.create("li", null, [
  2030. "Register your application: ",
  2031. registerLink,
  2032. ". Use 'https://anilist.co/settings/developer#void_imgur' as callback URL.",
  2033. ]),
  2034. DOM.create(
  2035. "li",
  2036. null,
  2037. "Fill the client id and secret fields with the value Imgur provided."
  2038. ),
  2039. DOM.create(
  2040. "li",
  2041. null,
  2042. "Click on authorize (you can skip this step if you don't want images tied to your account)."
  2043. ),
  2044. ]);
  2045. note.append(stepList);
  2046. note.append(
  2047. "Hitting Imgur API limits might get your API access blocked."
  2048. );
  2049.  
  2050. container.append(note);
  2051. }
  2052.  
  2053. #revokeAuth() {
  2054. const config = {
  2055. ...this.#configuration,
  2056. authToken: null,
  2057. refreshToken: null,
  2058. };
  2059.  
  2060. new ImageHostService().setImageHostConfiguration(
  2061. imageHosts.imgur,
  2062. config
  2063. );
  2064. }
  2065.  
  2066. #updateConfig(event, key, configuration) {
  2067. const value = event.target.value;
  2068. const config = {
  2069. ...configuration,
  2070. [key]: value,
  2071. };
  2072. new ImageHostService().setImageHostConfiguration(
  2073. imageHosts.imgur,
  2074. config
  2075. );
  2076. }
  2077. }
  2078.  
  2079. class ImageApiFactory {
  2080. getImageHostInstance() {
  2081. const imageHostService = new ImageHostService();
  2082. switch (imageHostService.getSelectedHost()) {
  2083. case imageHosts.imgbb:
  2084. return new ImgbbAPI(
  2085. imageHostService.getImageHostConfiguration(
  2086. imageHosts.imgbb
  2087. )
  2088. );
  2089. case imageHosts.imgur:
  2090. return new ImgurAPI(
  2091. imageHostService.getImageHostConfiguration(
  2092. imageHosts.imgur
  2093. )
  2094. );
  2095. case imageHosts.catbox:
  2096. return new CatboxAPI(
  2097. imageHostService.getImageHostConfiguration(
  2098. imageHosts.catbox
  2099. )
  2100. );
  2101. }
  2102. }
  2103. }
  2104.  
  2105. // MIT License
  2106.  
  2107. // Copyright (c) 2020 Refactoring UI Inc.
  2108.  
  2109. // Permission is hereby granted, free of charge, to any person obtaining a copy
  2110. // of this software and associated documentation files (the "Software"), to deal
  2111. // in the Software without restriction, including without limitation the rights
  2112. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  2113. // copies of the Software, and to permit persons to whom the Software is
  2114. // furnished to do so, subject to the following conditions:
  2115.  
  2116. // The above copyright notice and this permission notice shall be included in all
  2117. // copies or substantial portions of the Software.
  2118.  
  2119. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  2120. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  2121. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  2122. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  2123. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  2124. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  2125. // SOFTWARE.
  2126.  
  2127. const RefreshIcon = () => {
  2128. const icon = new DOMParser().parseFromString(
  2129. `<svg
  2130. xmlns="http://www.w3.org/2000/svg"
  2131. fill="none"
  2132. viewBox="0 0 24 24"
  2133. stroke-width="1.5"
  2134. stroke="currentColor"
  2135. class="w-6 h-6"
  2136. >
  2137. <path
  2138. stroke-linecap="round"
  2139. stroke-linejoin="round"
  2140. d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
  2141. />
  2142. </svg>`,
  2143. "text/html"
  2144. ).body.childNodes[0];
  2145. return icon;
  2146. };
  2147.  
  2148. const subCategories = {
  2149. users: "users",
  2150. authorization: "authorization",
  2151. imageHost: "image host",
  2152. layout: "layout & CSS",
  2153. globalCss: "global CSS",
  2154. toasts: "toasts",
  2155. };
  2156.  
  2157. class SettingsUserInterface {
  2158. settings;
  2159. styleHandler;
  2160. globalCSS;
  2161. userCSS;
  2162. layoutDesigner;
  2163. AnilistBlue = "120, 180, 255";
  2164. #activeCategory = "all";
  2165. #activeSubCategory = subCategories.users;
  2166.  
  2167. constructor(
  2168. settings,
  2169. styleHandler,
  2170. globalCSS,
  2171. userCSS,
  2172. layoutDesigner
  2173. ) {
  2174. this.settings = settings;
  2175. this.styleHandler = styleHandler;
  2176. this.globalCSS = globalCSS;
  2177. this.userCSS = userCSS;
  2178. this.layoutDesigner = layoutDesigner;
  2179. }
  2180.  
  2181. renderSettingsUi() {
  2182. this.#checkAuthFromUrl();
  2183. const container = DOM.get(".settings.container > .content");
  2184. const settingsContainerExists =
  2185. DOM.get("#void-verified-settings") !== null;
  2186. if (!settingsContainerExists) {
  2187. const settingsContainer = DOM.create(
  2188. "div",
  2189. "#verified-settings settings"
  2190. );
  2191. container.append(settingsContainer);
  2192. }
  2193.  
  2194. this.renderSettingsUiContent();
  2195. }
  2196.  
  2197. renderSettingsUiContent() {
  2198. const settingsContainer = DOM.create("div");
  2199.  
  2200. this.#renderSettingsHeader(settingsContainer);
  2201. this.#renderCategories(settingsContainer);
  2202. this.#renderOptions(settingsContainer);
  2203. this.#handleSubcategories(settingsContainer);
  2204.  
  2205. DOM.get("#void-verified-settings").replaceChildren(
  2206. settingsContainer
  2207. );
  2208. }
  2209.  
  2210. #handleSubcategories(settingsContainer) {
  2211. this.#renderSubCategoriesNavigation(settingsContainer);
  2212. switch (this.#activeSubCategory) {
  2213. case subCategories.users:
  2214. this.#renderUserTable(settingsContainer);
  2215. break;
  2216. case subCategories.authorization:
  2217. this.#creatAuthenticationSection(settingsContainer);
  2218. break;
  2219. case subCategories.imageHost:
  2220. this.#renderImageHostSettings(settingsContainer);
  2221. break;
  2222. case subCategories.layout:
  2223. settingsContainer.append(
  2224. this.layoutDesigner.renderSettings(this)
  2225. );
  2226. if (
  2227. this.settings.auth?.token &&
  2228. (this.settings.options.profileCssEnabled.getValue() ||
  2229. this.settings.options.activityCssEnabled.getValue())
  2230. ) {
  2231. this.#renderCustomCssEditor(
  2232. settingsContainer,
  2233. this.userCSS
  2234. );
  2235. }
  2236. break;
  2237. case subCategories.globalCss:
  2238. if (this.settings.options.globalCssEnabled.getValue()) {
  2239. this.#renderCustomCssEditor(
  2240. settingsContainer,
  2241. this.globalCSS
  2242. );
  2243. }
  2244. break;
  2245. case subCategories.toasts:
  2246. settingsContainer.append(Toaster.renderSettings(this));
  2247. }
  2248. }
  2249.  
  2250. #renderOptions(settingsContainer) {
  2251. const settingsListContainer = DOM.create("div", "settings-list");
  2252. for (const [key, setting] of Object.entries(
  2253. this.settings.options
  2254. )) {
  2255. if (
  2256. setting.category !== this.#activeCategory &&
  2257. this.#activeCategory !== "all"
  2258. ) {
  2259. continue;
  2260. }
  2261. this.#renderSetting(setting, settingsListContainer, key);
  2262. }
  2263.  
  2264. settingsContainer.append(settingsListContainer);
  2265. }
  2266.  
  2267. removeSettingsUi() {
  2268. const settings = document.querySelector("#void-verified-settings");
  2269. settings?.remove();
  2270. }
  2271.  
  2272. #renderSettingsHeader(settingsContainer) {
  2273. const headerContainer = DOM.create("div", "settings-header");
  2274. const header = DOM.create("h1", null, "VoidVerified Settings");
  2275.  
  2276. const versionInfo = DOM.create("p", null, [
  2277. "Version: ",
  2278. DOM.create("span", null, this.settings.version),
  2279. ]);
  2280.  
  2281. headerContainer.append(header);
  2282. headerContainer.append(versionInfo);
  2283. const author = DOM.create("p", null, [
  2284. "Author: ",
  2285. Link("voidnyan", "https://anilist.co/user/voidnyan/"),
  2286. ]);
  2287.  
  2288. headerContainer.append(header, versionInfo, author);
  2289.  
  2290. settingsContainer.append(headerContainer);
  2291. }
  2292.  
  2293. #renderCategories(settingsContainer) {
  2294. const nav = DOM.create("nav", "nav");
  2295. const list = DOM.create("ol");
  2296.  
  2297. const onClick = (_category) => {
  2298. this.#activeCategory = _category;
  2299. this.renderSettingsUiContent();
  2300. };
  2301.  
  2302. list.append(
  2303. this.#createNavBtn(
  2304. "all",
  2305. "all" === this.#activeCategory,
  2306. () => {
  2307. onClick("all");
  2308. }
  2309. )
  2310. );
  2311.  
  2312. for (const category of Object.values(categories)) {
  2313. list.append(
  2314. this.#createNavBtn(
  2315. category,
  2316. category === this.#activeCategory,
  2317. () => {
  2318. onClick(category);
  2319. }
  2320. )
  2321. );
  2322. }
  2323.  
  2324. nav.append(list);
  2325. settingsContainer.append(nav);
  2326. }
  2327.  
  2328. #renderSubCategoriesNavigation(settingsContainer) {
  2329. const nav = DOM.create("nav", "nav");
  2330. const list = DOM.create("ol");
  2331.  
  2332. for (const subCategory of Object.values(subCategories)) {
  2333. if (!this.#shouldDisplaySubCategory(subCategory)) {
  2334. continue;
  2335. }
  2336. list.append(
  2337. this.#createNavBtn(
  2338. subCategory,
  2339. this.#activeSubCategory === subCategory,
  2340. () => {
  2341. this.#activeSubCategory = subCategory;
  2342. this.renderSettingsUiContent();
  2343. }
  2344. )
  2345. );
  2346. }
  2347.  
  2348. nav.append(list);
  2349. settingsContainer.append(nav);
  2350. }
  2351.  
  2352. #shouldDisplaySubCategory(subCategory) {
  2353. switch (subCategory) {
  2354. case subCategories.users:
  2355. return true;
  2356. case subCategories.authorization:
  2357. return true;
  2358. case subCategories.imageHost:
  2359. return this.settings.options.pasteImagesToHostService.getValue();
  2360. case subCategories.layout:
  2361. return (
  2362. this.settings.auth?.token &&
  2363. (this.settings.options.profileCssEnabled.getValue() ||
  2364. this.settings.options.activityCssEnabled.getValue() ||
  2365. this.settings.options.layoutDesignerEnabled.getValue())
  2366. );
  2367. case subCategories.globalCss:
  2368. return this.settings.options.globalCssEnabled.getValue();
  2369. case subCategories.toasts:
  2370. return this.settings.options.toasterEnabled.getValue();
  2371. }
  2372. }
  2373.  
  2374. #createNavBtn(category, isActive, onClick) {
  2375. const className = isActive ? "active" : null;
  2376. const li = DOM.create("li", className, category);
  2377.  
  2378. li.addEventListener("click", () => {
  2379. onClick();
  2380. });
  2381.  
  2382. return li;
  2383. }
  2384.  
  2385. #renderUserTable(settingsContainer) {
  2386. const tableContainer = DOM.create(
  2387. "div",
  2388. "table #verified-user-table"
  2389. );
  2390.  
  2391. tableContainer.style = `
  2392. margin-top: 25px;
  2393. `;
  2394. const head = TableHead("Username", "Sign", "Color", "Other");
  2395.  
  2396. const rows = this.settings.verifiedUsers.map((user) =>
  2397. this.#createUserRow(user)
  2398. );
  2399. const body = TableBody(rows);
  2400.  
  2401. const table = Table(head, body);
  2402. tableContainer.append(table);
  2403.  
  2404. const inputForm = DOM.create("form");
  2405.  
  2406. inputForm.addEventListener("submit", (event) => {
  2407. this.#handleVerifyUserForm(event, this.settings);
  2408. });
  2409.  
  2410. const inputFormLabel = DOM.create("label", null, "Add user");
  2411. inputFormLabel.setAttribute("for", "void-verified-add-user");
  2412.  
  2413. inputForm.append(inputFormLabel);
  2414. inputForm.append(InputField("", () => {}, "#verified-add-user"));
  2415. tableContainer.append(inputForm);
  2416.  
  2417. settingsContainer.append(tableContainer);
  2418.  
  2419. const fallbackColorOption =
  2420. this.settings.options.defaultHighlightColor;
  2421. settingsContainer.append(
  2422. DOM.create("h5", null, "Fallback color"),
  2423. ColorPicker(fallbackColorOption.getValue(), (event) => {
  2424. this.#handleOption(event, "fallbackColor");
  2425. })
  2426. );
  2427. }
  2428.  
  2429. #createUserRow(user) {
  2430. const row = DOM.create("tr");
  2431. const userLink = DOM.create("a", null, user.username);
  2432. userLink.setAttribute(
  2433. "href",
  2434. `https://anilist.co/user/${user.username}/`
  2435. );
  2436. userLink.setAttribute("target", "_blank");
  2437. row.append(DOM.create("td", null, userLink));
  2438.  
  2439. const signInput = InputField(
  2440. user.sign ?? "",
  2441. (event) => {
  2442. this.#updateUserOption(
  2443. user.username,
  2444. "sign",
  2445. event.target.value
  2446. );
  2447. },
  2448. "sign"
  2449. );
  2450.  
  2451. const signCell = DOM.create("td", null, signInput);
  2452. signCell.append(
  2453. this.#createUserCheckbox(
  2454. user.enabledForUsername,
  2455. user.username,
  2456. "enabledForUsername",
  2457. this.settings.options.enabledForUsername.getValue()
  2458. )
  2459. );
  2460.  
  2461. row.append(DOM.create("th", null, signCell));
  2462.  
  2463. const colorInputContainer = DOM.create("div");
  2464.  
  2465. const colorInput = DOM.create("input");
  2466. colorInput.setAttribute("type", "color");
  2467. colorInput.value = this.#getUserColorPickerColor(user);
  2468. colorInput.addEventListener(
  2469. "change",
  2470. (event) => this.#handleUserColorChange(event, user.username),
  2471. false
  2472. );
  2473.  
  2474. colorInputContainer.append(colorInput);
  2475.  
  2476. colorInputContainer.append(
  2477. IconButton(RefreshIcon(), () => {
  2478. this.#handleUserColorReset(user.username);
  2479. })
  2480. );
  2481.  
  2482. colorInputContainer.append(
  2483. this.#createUserCheckbox(
  2484. user.copyColorFromProfile,
  2485. user.username,
  2486. "copyColorFromProfile",
  2487. this.settings.options.copyColorFromProfile.getValue()
  2488. )
  2489. );
  2490.  
  2491. colorInputContainer.append(
  2492. this.#createUserCheckbox(
  2493. user.highlightEnabled,
  2494. user.username,
  2495. "highlightEnabled",
  2496. this.settings.options.highlightEnabled.getValue()
  2497. )
  2498. );
  2499.  
  2500. colorInputContainer.append(
  2501. this.#createUserCheckbox(
  2502. user.highlightEnabledForReplies,
  2503. user.username,
  2504. "highlightEnabledForReplies",
  2505. this.settings.options.highlightEnabledForReplies.getValue()
  2506. )
  2507. );
  2508.  
  2509. colorInputContainer.append(
  2510. this.#createUserCheckbox(
  2511. user.colorUserActivity,
  2512. user.username,
  2513. "colorUserActivity",
  2514. this.settings.options.colorUserActivity.getValue()
  2515. )
  2516. );
  2517.  
  2518. colorInputContainer.append(
  2519. this.#createUserCheckbox(
  2520. user.colorUserReplies,
  2521. user.username,
  2522. "colorUserReplies",
  2523. this.settings.options.colorUserReplies.getValue()
  2524. )
  2525. );
  2526.  
  2527. const colorCell = DOM.create("td", null, colorInputContainer);
  2528. row.append(colorCell);
  2529.  
  2530. const quickAccessCheckbox = this.#createUserCheckbox(
  2531. user.quickAccessEnabled,
  2532. user.username,
  2533. "quickAccessEnabled",
  2534. this.settings.options.quickAccessEnabled.getValue()
  2535. );
  2536.  
  2537. const otherCell = DOM.create("td", null, quickAccessCheckbox);
  2538.  
  2539. const cssEnabledCheckbox = this.#createUserCheckbox(
  2540. user.onlyLoadCssFromVerifiedUser,
  2541. user.username,
  2542. "onlyLoadCssFromVerifiedUser",
  2543. this.settings.options.onlyLoadCssFromVerifiedUser.getValue()
  2544. );
  2545.  
  2546. otherCell.append(cssEnabledCheckbox);
  2547.  
  2548. row.append(otherCell);
  2549.  
  2550. const deleteButton = DOM.create("button", null, "❌");
  2551. deleteButton.addEventListener("click", () =>
  2552. this.#removeUser(user.username)
  2553. );
  2554. row.append(DOM.create("th", null, deleteButton));
  2555. return row;
  2556. }
  2557.  
  2558. #getUserColorPickerColor(user) {
  2559. if (user.colorOverride) {
  2560. return user.colorOverride;
  2561. }
  2562.  
  2563. if (
  2564. user.color &&
  2565. (user.copyColorFromProfile ||
  2566. this.settings.options.copyColorFromProfile.getValue())
  2567. ) {
  2568. return ColorFunctions.rgbToHex(user.color);
  2569. }
  2570.  
  2571. if (this.settings.options.useDefaultHighlightColor.getValue()) {
  2572. return this.settings.options.defaultHighlightColor.getValue();
  2573. }
  2574.  
  2575. return ColorFunctions.rgbToHex(this.AnilistBlue);
  2576. }
  2577.  
  2578. #createUserCheckbox(isChecked, username, settingKey, disabled) {
  2579. const onChange = (event) => {
  2580. this.#updateUserOption(
  2581. username,
  2582. settingKey,
  2583. event.target.checked
  2584. );
  2585. this.renderSettingsUiContent();
  2586. };
  2587. const checkbox = Checkbox(
  2588. isChecked,
  2589. onChange,
  2590. this.settings.options[settingKey].description,
  2591. disabled
  2592. );
  2593. return checkbox;
  2594. }
  2595.  
  2596. #handleUserColorReset(username) {
  2597. this.#updateUserOption(username, "colorOverride", undefined);
  2598. this.renderSettingsUiContent();
  2599. }
  2600.  
  2601. #handleUserColorChange(event, username) {
  2602. const color = event.target.value;
  2603. this.#updateUserOption(username, "colorOverride", color);
  2604. }
  2605.  
  2606. async #handleVerifyUserForm(event, settings) {
  2607. event.preventDefault();
  2608.  
  2609. const usernameInput = DOM.get("#void-verified-add-user");
  2610. const username = usernameInput.value;
  2611. await settings.verifyUser(username);
  2612. usernameInput.value = "";
  2613. this.renderSettingsUiContent();
  2614. }
  2615.  
  2616. #updateUserOption(username, key, value) {
  2617. this.settings.updateUserOption(username, key, value);
  2618. this.styleHandler.refreshStyles();
  2619. }
  2620.  
  2621. #removeUser(username) {
  2622. this.settings.removeUser(username);
  2623. this.renderSettingsUiContent();
  2624. this.styleHandler.refreshStyles();
  2625. }
  2626.  
  2627. #renderSetting(setting, settingsContainer, settingKey) {
  2628. if (setting.category === categories.hidden) {
  2629. return;
  2630. }
  2631. const value = setting.getValue();
  2632. const type = typeof value;
  2633.  
  2634. let input;
  2635.  
  2636. const onChange = (event) => {
  2637. this.#handleOption(event, settingKey, type);
  2638. };
  2639.  
  2640. if (type === "boolean") {
  2641. input = Checkbox(value, onChange);
  2642. } else if (settingKey == "defaultHighlightColor") {
  2643. return;
  2644. } else if (type === "string") {
  2645. input = InputField(value, onChange);
  2646. }
  2647. input.setAttribute("id", settingKey);
  2648.  
  2649. settingsContainer.append(SettingLabel(setting.description, input));
  2650. }
  2651.  
  2652. #handleOption(event, settingKey, type) {
  2653. const value =
  2654. type === "boolean" ? event.target.checked : event.target.value;
  2655. this.settings.saveSettingToLocalStorage(settingKey, value);
  2656. this.styleHandler.refreshStyles();
  2657.  
  2658. if (!this.#shouldDisplaySubCategory(this.#activeSubCategory)) {
  2659. this.#activeSubCategory = subCategories.users;
  2660. }
  2661.  
  2662. this.renderSettingsUiContent();
  2663. }
  2664.  
  2665. // TODO: separate userCSS
  2666. #renderCustomCssEditor(settingsContainer, cssHandler) {
  2667. const cssName = cssHandler instanceof GlobalCSS ? "global" : "user";
  2668. const container = DOM.create("div", "css-editor");
  2669. const label = DOM.create("label", null, `Custom ${cssName} CSS`);
  2670. label.setAttribute("for", `void-verified-${cssName}-css-editor`);
  2671. container.append(label);
  2672.  
  2673. const textarea = TextArea(cssHandler.css, (event) => {
  2674. this.#handleCustomCssEditor(event, cssHandler);
  2675. });
  2676. container.append(textarea);
  2677.  
  2678. if (cssName === "global") {
  2679. const notice = DOM.create("div");
  2680. notice.innerText =
  2681. "Please note that Custom CSS is disabled in the settings. \nIn the event that you accidentally disable rendering of critical parts of AniList, navigate to the settings by URL";
  2682. notice.style.fontSize = "11px";
  2683. container.append(notice);
  2684. } else {
  2685. const publishButton = DOM.create("button", null, "Publish");
  2686. publishButton.classList.add("button");
  2687. publishButton.addEventListener("click", (event) =>
  2688. this.#handlePublishCss(event, cssHandler)
  2689. );
  2690.  
  2691. const previewButton = DOM.create(
  2692. "button",
  2693. null,
  2694. cssHandler.preview ? "Disable Preview" : "Enable Preview"
  2695. );
  2696. previewButton.classList.add("button");
  2697. previewButton.addEventListener("click", () => {
  2698. cssHandler.togglePreview();
  2699. previewButton.innerText = cssHandler.preview
  2700. ? "Disable Preview"
  2701. : "Enable Preview";
  2702. });
  2703.  
  2704. const resetButton = DOM.create("button", null, "Reset");
  2705. resetButton.classList.add("button");
  2706. resetButton.addEventListener("click", () => {
  2707. if (window.confirm("Your changes will be lost.")) {
  2708. cssHandler.getAuthUserCss().then(() => {
  2709. textarea.value = cssHandler.css;
  2710. });
  2711. }
  2712. });
  2713.  
  2714. container.append(publishButton);
  2715. container.append(previewButton);
  2716. container.append(resetButton);
  2717. }
  2718.  
  2719. settingsContainer.append(container);
  2720. }
  2721.  
  2722. // TODO: separate userCSS
  2723. #handlePublishCss(event, cssHandler) {
  2724. const btn = event.target;
  2725. btn.innerText = "Publishing...";
  2726. cssHandler.publishUserCss().then(() => {
  2727. btn.innerText = "Publish";
  2728. });
  2729. }
  2730.  
  2731. // TODO: separate userCSS
  2732. #handleCustomCssEditor(event, cssHandler) {
  2733. const value = event.target.value;
  2734. cssHandler.updateCss(value);
  2735. }
  2736.  
  2737. // TODO: separate to imageHostService?
  2738. #renderImageHostSettings(settingsContainer) {
  2739. const container = DOM.create("div");
  2740.  
  2741. const imageHostService = new ImageHostService();
  2742. const imageApiFactory = new ImageApiFactory();
  2743.  
  2744. const imageHostOptions = Object.values(imageHosts).map(
  2745. (imageHost) =>
  2746. Option$1(
  2747. imageHost,
  2748. imageHost === imageHostService.getSelectedHost(),
  2749. () => {
  2750. imageHostService.setSelectedHost(imageHost);
  2751. this.renderSettingsUiContent();
  2752. }
  2753. )
  2754. );
  2755.  
  2756. const select = Select(imageHostOptions);
  2757. container.append(Label("Image host", select));
  2758.  
  2759. const hostSpecificSettings = DOM.create("div");
  2760. const imageHostApi = imageApiFactory.getImageHostInstance();
  2761. hostSpecificSettings.append(imageHostApi.renderSettings(this));
  2762.  
  2763. container.append(hostSpecificSettings);
  2764. settingsContainer.append(container);
  2765. }
  2766.  
  2767. #creatAuthenticationSection(settingsContainer) {
  2768. const isAuthenticated =
  2769. this.settings.auth !== null &&
  2770. new Date(this.settings.auth?.expires) > new Date();
  2771.  
  2772. const clientId = 15519;
  2773.  
  2774. const authenticationContainer = DOM.create("div");
  2775.  
  2776. const header = DOM.create("h3", null, "Authorize VoidVerified");
  2777. const description = DOM.create(
  2778. "p",
  2779. null,
  2780. "Some features of VoidVerified might need your access token to work correctly or fully. Below is a list of features using your access token. If you do not wish to use any of these features, you do not need to authenticate. If revoking authentication, be sure to revoke VoidVerified from Anilist Apps as well."
  2781. );
  2782.  
  2783. const list = DOM.create("ul");
  2784. for (const option of Object.values(this.settings.options).filter(
  2785. (o) => o.authRequired
  2786. )) {
  2787. list.append(DOM.create("li", null, option.description));
  2788. }
  2789.  
  2790. const authLink = DOM.create(
  2791. "a",
  2792. "button",
  2793. "Authenticate VoidVerified"
  2794. );
  2795. authLink.setAttribute(
  2796. "href",
  2797. `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`
  2798. );
  2799.  
  2800. const removeAuthButton = DOM.create(
  2801. "button",
  2802. null,
  2803. "Revoke auth token"
  2804. );
  2805. removeAuthButton.classList.add("button");
  2806. removeAuthButton.addEventListener("click", () => {
  2807. this.settings.removeAuthToken();
  2808. this.renderSettingsUiContent();
  2809. });
  2810.  
  2811. authenticationContainer.append(header);
  2812. authenticationContainer.append(description);
  2813. authenticationContainer.append(list);
  2814. authenticationContainer.append(
  2815. !isAuthenticated ? authLink : removeAuthButton
  2816. );
  2817.  
  2818. settingsContainer.append(authenticationContainer);
  2819. }
  2820.  
  2821. #checkAuthFromUrl() {
  2822. const hash = window.location.hash.substring(1);
  2823. if (!hash) {
  2824. return;
  2825. }
  2826.  
  2827. const [path, token, type, expiress] = hash.split("&");
  2828.  
  2829. if (path === "void_imgur") {
  2830. const imgurConfig =
  2831. new ImageHostService().getImageHostConfiguration(
  2832. imageHosts.imgur
  2833. );
  2834. new ImgurAPI(imgurConfig).handleAuth();
  2835. }
  2836. if (path !== "void_auth") {
  2837. return;
  2838. }
  2839.  
  2840. const expiresDate = new Date(
  2841. new Date().getTime() + Number(expiress.split("=")[1]) * 1000
  2842. );
  2843.  
  2844. this.settings.saveAuthToken({
  2845. token: token.split("=")[1],
  2846. expires: expiresDate,
  2847. });
  2848.  
  2849. window.history.replaceState(
  2850. null,
  2851. "",
  2852. "https://anilist.co/settings/developer"
  2853. );
  2854. }
  2855. }
  2856.  
  2857. class QuickAccess {
  2858. settings;
  2859. #quickAccessId = "void-quick-access";
  2860. #lastFetchedLocalStorage = "void-verified-last-fetched";
  2861. #lastFetched;
  2862. #queryInProgress = false;
  2863.  
  2864. #apiQueryTimeoutInMinutes = 15;
  2865. #apiQueryTimeout = this.#apiQueryTimeoutInMinutes * 60 * 1000;
  2866. constructor(settings) {
  2867. this.settings = settings;
  2868. const fetched = localStorage.getItem(this.#lastFetchedLocalStorage);
  2869. if (fetched) {
  2870. this.#lastFetched = new Date(fetched);
  2871. }
  2872. }
  2873.  
  2874. async renderQuickAccess() {
  2875. if (this.#queryInProgress) {
  2876. return;
  2877. }
  2878.  
  2879. const queried = await this.#queryUsers();
  2880.  
  2881. if (!queried && this.#quickAccessRendered()) {
  2882. this.#updateTimer();
  2883. return;
  2884. }
  2885.  
  2886. if (
  2887. !this.settings.options.quickAccessEnabled.getValue() &&
  2888. !this.settings.verifiedUsers.some(
  2889. (user) => user.quickAccessEnabled
  2890. )
  2891. ) {
  2892. return;
  2893. }
  2894.  
  2895. const quickAccessContainer = DOM.getOrCreate(
  2896. "div",
  2897. "#quick-access quick-access"
  2898. );
  2899.  
  2900. const sectionHeader = document.createElement("div");
  2901. sectionHeader.setAttribute("class", "section-header");
  2902. const title = document.createElement("h2");
  2903. title.append("Quick Access");
  2904. title.setAttribute(
  2905. "title",
  2906. `Last updated at ${this.#lastFetched.toLocaleTimeString()}`
  2907. );
  2908. sectionHeader.append(title);
  2909.  
  2910. const timer = DOM.create("span", "quick-access-timer", "");
  2911.  
  2912. const refreshButton = IconButton(RefreshIcon(), () => {
  2913. this.#queryUsers(true);
  2914. });
  2915.  
  2916. sectionHeader.append(
  2917. DOM.create("div", null, [timer, refreshButton])
  2918. );
  2919.  
  2920. const quickAccessBody = document.createElement("div");
  2921. quickAccessBody.setAttribute("class", "void-quick-access-wrap");
  2922.  
  2923. for (const user of this.#getQuickAccessUsers()) {
  2924. quickAccessBody.append(this.#createQuickAccessLink(user));
  2925. }
  2926.  
  2927. const section = document.querySelector(
  2928. ".container > .home > div:nth-child(2)"
  2929. );
  2930.  
  2931. quickAccessContainer.replaceChildren(
  2932. sectionHeader,
  2933. quickAccessBody
  2934. );
  2935.  
  2936. if (DOM.get("#void-quick-access")) {
  2937. return;
  2938. }
  2939. section.insertBefore(quickAccessContainer, section.firstChild);
  2940. }
  2941.  
  2942. #updateTimer() {
  2943. if (!this.settings.options.quickAccessTimer.getValue()) {
  2944. return;
  2945. }
  2946. const timer = DOM.get(".void-quick-access-timer");
  2947. const nextQuery = new Date(
  2948. this.#lastFetched.getTime() + this.#apiQueryTimeout
  2949. );
  2950. const timeLeftInSeconds = Math.floor(
  2951. (nextQuery - new Date()) / 1000
  2952. );
  2953. const timeLeftInMinutes = timeLeftInSeconds / 60;
  2954.  
  2955. if (timeLeftInMinutes > 1) {
  2956. timer.replaceChildren(`${Math.floor(timeLeftInSeconds / 60)}m`);
  2957. return;
  2958. }
  2959.  
  2960. timer.replaceChildren(`${timeLeftInSeconds}s`);
  2961. }
  2962.  
  2963. async #queryUsers(ignoreLastFetched = false) {
  2964. const currentTime = new Date();
  2965.  
  2966. if (
  2967. !this.#lastFetched ||
  2968. currentTime - this.#lastFetched > this.#apiQueryTimeout ||
  2969. ignoreLastFetched
  2970. ) {
  2971. try {
  2972. Toaster.debug("Querying Quick Access users.");
  2973. this.#queryInProgress = true;
  2974. const anilistAPI = new AnilistAPI(this.settings);
  2975. await anilistAPI.queryVerifiedUsers();
  2976. Toaster.success("Quick Access users updated.");
  2977. } catch (error) {
  2978. Toaster.error("Querying Quick Access failed.");
  2979. console.error(error);
  2980. } finally {
  2981. this.#lastFetched = new Date();
  2982. localStorage.setItem(
  2983. this.#lastFetchedLocalStorage,
  2984. this.#lastFetched
  2985. );
  2986. this.#queryInProgress = false;
  2987. return true;
  2988. }
  2989. } else {
  2990. return false;
  2991. }
  2992. }
  2993.  
  2994. clearBadge() {
  2995. const username =
  2996. window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];
  2997. this.settings.updateUserOption(
  2998. username,
  2999. "quickAccessBadgeDisplay",
  3000. false
  3001. );
  3002. }
  3003.  
  3004. #createQuickAccessLink(user) {
  3005. const container = document.createElement("a");
  3006. container.setAttribute("class", "void-quick-access-item");
  3007. container.setAttribute(
  3008. "href",
  3009. `https://anilist.co/user/${user.username}/`
  3010. );
  3011.  
  3012. const image = document.createElement("div");
  3013. image.style.backgroundImage = `url(${user.avatar})`;
  3014. image.setAttribute("class", "void-quick-access-pfp");
  3015. container.append(image);
  3016.  
  3017. const username = document.createElement("div");
  3018. username.append(user.username);
  3019. username.setAttribute("class", "void-quick-access-username");
  3020.  
  3021. if (
  3022. (this.settings.options.quickAccessBadge.getValue() ||
  3023. user.quickAccessBadge) &&
  3024. user.quickAccessBadgeDisplay
  3025. ) {
  3026. container.classList.add("void-quick-access-badge");
  3027. }
  3028.  
  3029. container.append(username);
  3030. return container;
  3031. }
  3032.  
  3033. #quickAccessRendered() {
  3034. const quickAccess = document.getElementById(this.#quickAccessId);
  3035. return quickAccess !== null;
  3036. }
  3037.  
  3038. #getQuickAccessUsers() {
  3039. if (this.settings.options.quickAccessEnabled.getValue()) {
  3040. return this.settings.verifiedUsers;
  3041. }
  3042.  
  3043. return this.settings.verifiedUsers.filter(
  3044. (user) => user.quickAccessEnabled
  3045. );
  3046. }
  3047. }
  3048.  
  3049. // Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
  3050. // This work is free. You can redistribute it and/or modify it
  3051. // under the terms of the WTFPL, Version 2
  3052. // For more information see LICENSE.txt or http://www.wtfpl.net/
  3053. //
  3054. // For more information, the home page:
  3055. // http://pieroxy.net/blog/pages/lz-string/testing.html
  3056. //
  3057. // LZ-based compression algorithm, version 1.4.4
  3058. var LZString = (function () {
  3059. // private property
  3060. var f = String.fromCharCode;
  3061. var keyStrBase64 =
  3062. "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  3063. var keyStrUriSafe =
  3064. "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
  3065. var baseReverseDic = {};
  3066.  
  3067. function getBaseValue(alphabet, character) {
  3068. if (!baseReverseDic[alphabet]) {
  3069. baseReverseDic[alphabet] = {};
  3070. for (var i = 0; i < alphabet.length; i++) {
  3071. baseReverseDic[alphabet][alphabet.charAt(i)] = i;
  3072. }
  3073. }
  3074. return baseReverseDic[alphabet][character];
  3075. }
  3076.  
  3077. var LZString = {
  3078. compressToBase64: function (input) {
  3079. if (input == null) return "";
  3080. var res = LZString._compress(input, 6, function (a) {
  3081. return keyStrBase64.charAt(a);
  3082. });
  3083. switch (
  3084. res.length % 4 // To produce valid Base64
  3085. ) {
  3086. default: // When could this happen ?
  3087. case 0:
  3088. return res;
  3089. case 1:
  3090. return res + "===";
  3091. case 2:
  3092. return res + "==";
  3093. case 3:
  3094. return res + "=";
  3095. }
  3096. },
  3097.  
  3098. decompressFromBase64: function (input) {
  3099. if (input == null) return "";
  3100. if (input == "") return null;
  3101. return LZString._decompress(input.length, 32, function (index) {
  3102. return getBaseValue(keyStrBase64, input.charAt(index));
  3103. });
  3104. },
  3105.  
  3106. compressToUTF16: function (input) {
  3107. if (input == null) return "";
  3108. return (
  3109. LZString._compress(input, 15, function (a) {
  3110. return f(a + 32);
  3111. }) + " "
  3112. );
  3113. },
  3114.  
  3115. decompressFromUTF16: function (compressed) {
  3116. if (compressed == null) return "";
  3117. if (compressed == "") return null;
  3118. return LZString._decompress(
  3119. compressed.length,
  3120. 16384,
  3121. function (index) {
  3122. return compressed.charCodeAt(index) - 32;
  3123. }
  3124. );
  3125. },
  3126.  
  3127. //compress into uint8array (UCS-2 big endian format)
  3128. compressToUint8Array: function (uncompressed) {
  3129. var compressed = LZString.compress(uncompressed);
  3130. var buf = new Uint8Array(compressed.length * 2); // 2 bytes per character
  3131.  
  3132. for (
  3133. var i = 0, TotalLen = compressed.length;
  3134. i < TotalLen;
  3135. i++
  3136. ) {
  3137. var current_value = compressed.charCodeAt(i);
  3138. buf[i * 2] = current_value >>> 8;
  3139. buf[i * 2 + 1] = current_value % 256;
  3140. }
  3141. return buf;
  3142. },
  3143.  
  3144. //decompress from uint8array (UCS-2 big endian format)
  3145. decompressFromUint8Array: function (compressed) {
  3146. if (compressed === null || compressed === undefined) {
  3147. return LZString.decompress(compressed);
  3148. } else {
  3149. var buf = new Array(compressed.length / 2); // 2 bytes per character
  3150. for (var i = 0, TotalLen = buf.length; i < TotalLen; i++) {
  3151. buf[i] =
  3152. compressed[i * 2] * 256 + compressed[i * 2 + 1];
  3153. }
  3154.  
  3155. var result = [];
  3156. buf.forEach(function (c) {
  3157. result.push(f(c));
  3158. });
  3159. return LZString.decompress(result.join(""));
  3160. }
  3161. },
  3162.  
  3163. //compress into a string that is already URI encoded
  3164. compressToEncodedURIComponent: function (input) {
  3165. if (input == null) return "";
  3166. return LZString._compress(input, 6, function (a) {
  3167. return keyStrUriSafe.charAt(a);
  3168. });
  3169. },
  3170.  
  3171. //decompress from an output of compressToEncodedURIComponent
  3172. decompressFromEncodedURIComponent: function (input) {
  3173. if (input == null) return "";
  3174. if (input == "") return null;
  3175. input = input.replace(/ /g, "+");
  3176. return LZString._decompress(input.length, 32, function (index) {
  3177. return getBaseValue(keyStrUriSafe, input.charAt(index));
  3178. });
  3179. },
  3180.  
  3181. compress: function (uncompressed) {
  3182. return LZString._compress(uncompressed, 16, function (a) {
  3183. return f(a);
  3184. });
  3185. },
  3186. _compress: function (uncompressed, bitsPerChar, getCharFromInt) {
  3187. if (uncompressed == null) return "";
  3188. var i,
  3189. value,
  3190. context_dictionary = {},
  3191. context_dictionaryToCreate = {},
  3192. context_c = "",
  3193. context_wc = "",
  3194. context_w = "",
  3195. context_enlargeIn = 2, // Compensate for the first entry which should not count
  3196. context_dictSize = 3,
  3197. context_numBits = 2,
  3198. context_data = [],
  3199. context_data_val = 0,
  3200. context_data_position = 0,
  3201. ii;
  3202.  
  3203. for (ii = 0; ii < uncompressed.length; ii += 1) {
  3204. context_c = uncompressed.charAt(ii);
  3205. if (
  3206. !Object.prototype.hasOwnProperty.call(
  3207. context_dictionary,
  3208. context_c
  3209. )
  3210. ) {
  3211. context_dictionary[context_c] = context_dictSize++;
  3212. context_dictionaryToCreate[context_c] = true;
  3213. }
  3214.  
  3215. context_wc = context_w + context_c;
  3216. if (
  3217. Object.prototype.hasOwnProperty.call(
  3218. context_dictionary,
  3219. context_wc
  3220. )
  3221. ) {
  3222. context_w = context_wc;
  3223. } else {
  3224. if (
  3225. Object.prototype.hasOwnProperty.call(
  3226. context_dictionaryToCreate,
  3227. context_w
  3228. )
  3229. ) {
  3230. if (context_w.charCodeAt(0) < 256) {
  3231. for (i = 0; i < context_numBits; i++) {
  3232. context_data_val = context_data_val << 1;
  3233. if (
  3234. context_data_position ==
  3235. bitsPerChar - 1
  3236. ) {
  3237. context_data_position = 0;
  3238. context_data.push(
  3239. getCharFromInt(context_data_val)
  3240. );
  3241. context_data_val = 0;
  3242. } else {
  3243. context_data_position++;
  3244. }
  3245. }
  3246. value = context_w.charCodeAt(0);
  3247. for (i = 0; i < 8; i++) {
  3248. context_data_val =
  3249. (context_data_val << 1) | (value & 1);
  3250. if (
  3251. context_data_position ==
  3252. bitsPerChar - 1
  3253. ) {
  3254. context_data_position = 0;
  3255. context_data.push(
  3256. getCharFromInt(context_data_val)
  3257. );
  3258. context_data_val = 0;
  3259. } else {
  3260. context_data_position++;
  3261. }
  3262. value = value >> 1;
  3263. }
  3264. } else {
  3265. value = 1;
  3266. for (i = 0; i < context_numBits; i++) {
  3267. context_data_val =
  3268. (context_data_val << 1) | value;
  3269. if (
  3270. context_data_position ==
  3271. bitsPerChar - 1
  3272. ) {
  3273. context_data_position = 0;
  3274. context_data.push(
  3275. getCharFromInt(context_data_val)
  3276. );
  3277. context_data_val = 0;
  3278. } else {
  3279. context_data_position++;
  3280. }
  3281. value = 0;
  3282. }
  3283. value = context_w.charCodeAt(0);
  3284. for (i = 0; i < 16; i++) {
  3285. context_data_val =
  3286. (context_data_val << 1) | (value & 1);
  3287. if (
  3288. context_data_position ==
  3289. bitsPerChar - 1
  3290. ) {
  3291. context_data_position = 0;
  3292. context_data.push(
  3293. getCharFromInt(context_data_val)
  3294. );
  3295. context_data_val = 0;
  3296. } else {
  3297. context_data_position++;
  3298. }
  3299. value = value >> 1;
  3300. }
  3301. }
  3302. context_enlargeIn--;
  3303. if (context_enlargeIn == 0) {
  3304. context_enlargeIn = Math.pow(
  3305. 2,
  3306. context_numBits
  3307. );
  3308. context_numBits++;
  3309. }
  3310. delete context_dictionaryToCreate[context_w];
  3311. } else {
  3312. value = context_dictionary[context_w];
  3313. for (i = 0; i < context_numBits; i++) {
  3314. context_data_val =
  3315. (context_data_val << 1) | (value & 1);
  3316. if (context_data_position == bitsPerChar - 1) {
  3317. context_data_position = 0;
  3318. context_data.push(
  3319. getCharFromInt(context_data_val)
  3320. );
  3321. context_data_val = 0;
  3322. } else {
  3323. context_data_position++;
  3324. }
  3325. value = value >> 1;
  3326. }
  3327. }
  3328. context_enlargeIn--;
  3329. if (context_enlargeIn == 0) {
  3330. context_enlargeIn = Math.pow(2, context_numBits);
  3331. context_numBits++;
  3332. }
  3333. // Add wc to the dictionary.
  3334. context_dictionary[context_wc] = context_dictSize++;
  3335. context_w = String(context_c);
  3336. }
  3337. }
  3338.  
  3339. // Output the code for w.
  3340. if (context_w !== "") {
  3341. if (
  3342. Object.prototype.hasOwnProperty.call(
  3343. context_dictionaryToCreate,
  3344. context_w
  3345. )
  3346. ) {
  3347. if (context_w.charCodeAt(0) < 256) {
  3348. for (i = 0; i < context_numBits; i++) {
  3349. context_data_val = context_data_val << 1;
  3350. if (context_data_position == bitsPerChar - 1) {
  3351. context_data_position = 0;
  3352. context_data.push(
  3353. getCharFromInt(context_data_val)
  3354. );
  3355. context_data_val = 0;
  3356. } else {
  3357. context_data_position++;
  3358. }
  3359. }
  3360. value = context_w.charCodeAt(0);
  3361. for (i = 0; i < 8; i++) {
  3362. context_data_val =
  3363. (context_data_val << 1) | (value & 1);
  3364. if (context_data_position == bitsPerChar - 1) {
  3365. context_data_position = 0;
  3366. context_data.push(
  3367. getCharFromInt(context_data_val)
  3368. );
  3369. context_data_val = 0;
  3370. } else {
  3371. context_data_position++;
  3372. }
  3373. value = value >> 1;
  3374. }
  3375. } else {
  3376. value = 1;
  3377. for (i = 0; i < context_numBits; i++) {
  3378. context_data_val =
  3379. (context_data_val << 1) | value;
  3380. if (context_data_position == bitsPerChar - 1) {
  3381. context_data_position = 0;
  3382. context_data.push(
  3383. getCharFromInt(context_data_val)
  3384. );
  3385. context_data_val = 0;
  3386. } else {
  3387. context_data_position++;
  3388. }
  3389. value = 0;
  3390. }
  3391. value = context_w.charCodeAt(0);
  3392. for (i = 0; i < 16; i++) {
  3393. context_data_val =
  3394. (context_data_val << 1) | (value & 1);
  3395. if (context_data_position == bitsPerChar - 1) {
  3396. context_data_position = 0;
  3397. context_data.push(
  3398. getCharFromInt(context_data_val)
  3399. );
  3400. context_data_val = 0;
  3401. } else {
  3402. context_data_position++;
  3403. }
  3404. value = value >> 1;
  3405. }
  3406. }
  3407. context_enlargeIn--;
  3408. if (context_enlargeIn == 0) {
  3409. context_enlargeIn = Math.pow(2, context_numBits);
  3410. context_numBits++;
  3411. }
  3412. delete context_dictionaryToCreate[context_w];
  3413. } else {
  3414. value = context_dictionary[context_w];
  3415. for (i = 0; i < context_numBits; i++) {
  3416. context_data_val =
  3417. (context_data_val << 1) | (value & 1);
  3418. if (context_data_position == bitsPerChar - 1) {
  3419. context_data_position = 0;
  3420. context_data.push(
  3421. getCharFromInt(context_data_val)
  3422. );
  3423. context_data_val = 0;
  3424. } else {
  3425. context_data_position++;
  3426. }
  3427. value = value >> 1;
  3428. }
  3429. }
  3430. context_enlargeIn--;
  3431. if (context_enlargeIn == 0) {
  3432. context_enlargeIn = Math.pow(2, context_numBits);
  3433. context_numBits++;
  3434. }
  3435. }
  3436.  
  3437. // Mark the end of the stream
  3438. value = 2;
  3439. for (i = 0; i < context_numBits; i++) {
  3440. context_data_val = (context_data_val << 1) | (value & 1);
  3441. if (context_data_position == bitsPerChar - 1) {
  3442. context_data_position = 0;
  3443. context_data.push(getCharFromInt(context_data_val));
  3444. context_data_val = 0;
  3445. } else {
  3446. context_data_position++;
  3447. }
  3448. value = value >> 1;
  3449. }
  3450.  
  3451. // Flush the last char
  3452. while (true) {
  3453. context_data_val = context_data_val << 1;
  3454. if (context_data_position == bitsPerChar - 1) {
  3455. context_data.push(getCharFromInt(context_data_val));
  3456. break;
  3457. } else context_data_position++;
  3458. }
  3459. return context_data.join("");
  3460. },
  3461.  
  3462. decompress: function (compressed) {
  3463. if (compressed == null) return "";
  3464. if (compressed == "") return null;
  3465. return LZString._decompress(
  3466. compressed.length,
  3467. 32768,
  3468. function (index) {
  3469. return compressed.charCodeAt(index);
  3470. }
  3471. );
  3472. },
  3473.  
  3474. _decompress: function (length, resetValue, getNextValue) {
  3475. var dictionary = [],
  3476. enlargeIn = 4,
  3477. dictSize = 4,
  3478. numBits = 3,
  3479. entry = "",
  3480. result = [],
  3481. i,
  3482. w,
  3483. bits,
  3484. resb,
  3485. maxpower,
  3486. power,
  3487. c,
  3488. data = {
  3489. val: getNextValue(0),
  3490. position: resetValue,
  3491. index: 1,
  3492. };
  3493.  
  3494. for (i = 0; i < 3; i += 1) {
  3495. dictionary[i] = i;
  3496. }
  3497.  
  3498. bits = 0;
  3499. maxpower = Math.pow(2, 2);
  3500. power = 1;
  3501. while (power != maxpower) {
  3502. resb = data.val & data.position;
  3503. data.position >>= 1;
  3504. if (data.position == 0) {
  3505. data.position = resetValue;
  3506. data.val = getNextValue(data.index++);
  3507. }
  3508. bits |= (resb > 0 ? 1 : 0) * power;
  3509. power <<= 1;
  3510. }
  3511.  
  3512. switch (bits) {
  3513. case 0:
  3514. bits = 0;
  3515. maxpower = Math.pow(2, 8);
  3516. power = 1;
  3517. while (power != maxpower) {
  3518. resb = data.val & data.position;
  3519. data.position >>= 1;
  3520. if (data.position == 0) {
  3521. data.position = resetValue;
  3522. data.val = getNextValue(data.index++);
  3523. }
  3524. bits |= (resb > 0 ? 1 : 0) * power;
  3525. power <<= 1;
  3526. }
  3527. c = f(bits);
  3528. break;
  3529. case 1:
  3530. bits = 0;
  3531. maxpower = Math.pow(2, 16);
  3532. power = 1;
  3533. while (power != maxpower) {
  3534. resb = data.val & data.position;
  3535. data.position >>= 1;
  3536. if (data.position == 0) {
  3537. data.position = resetValue;
  3538. data.val = getNextValue(data.index++);
  3539. }
  3540. bits |= (resb > 0 ? 1 : 0) * power;
  3541. power <<= 1;
  3542. }
  3543. c = f(bits);
  3544. break;
  3545. case 2:
  3546. return "";
  3547. }
  3548. dictionary[3] = c;
  3549. w = c;
  3550. result.push(c);
  3551. while (true) {
  3552. if (data.index > length) {
  3553. return "";
  3554. }
  3555.  
  3556. bits = 0;
  3557. maxpower = Math.pow(2, numBits);
  3558. power = 1;
  3559. while (power != maxpower) {
  3560. resb = data.val & data.position;
  3561. data.position >>= 1;
  3562. if (data.position == 0) {
  3563. data.position = resetValue;
  3564. data.val = getNextValue(data.index++);
  3565. }
  3566. bits |= (resb > 0 ? 1 : 0) * power;
  3567. power <<= 1;
  3568. }
  3569.  
  3570. switch ((c = bits)) {
  3571. case 0:
  3572. bits = 0;
  3573. maxpower = Math.pow(2, 8);
  3574. power = 1;
  3575. while (power != maxpower) {
  3576. resb = data.val & data.position;
  3577. data.position >>= 1;
  3578. if (data.position == 0) {
  3579. data.position = resetValue;
  3580. data.val = getNextValue(data.index++);
  3581. }
  3582. bits |= (resb > 0 ? 1 : 0) * power;
  3583. power <<= 1;
  3584. }
  3585.  
  3586. dictionary[dictSize++] = f(bits);
  3587. c = dictSize - 1;
  3588. enlargeIn--;
  3589. break;
  3590. case 1:
  3591. bits = 0;
  3592. maxpower = Math.pow(2, 16);
  3593. power = 1;
  3594. while (power != maxpower) {
  3595. resb = data.val & data.position;
  3596. data.position >>= 1;
  3597. if (data.position == 0) {
  3598. data.position = resetValue;
  3599. data.val = getNextValue(data.index++);
  3600. }
  3601. bits |= (resb > 0 ? 1 : 0) * power;
  3602. power <<= 1;
  3603. }
  3604. dictionary[dictSize++] = f(bits);
  3605. c = dictSize - 1;
  3606. enlargeIn--;
  3607. break;
  3608. case 2:
  3609. return result.join("");
  3610. }
  3611.  
  3612. if (enlargeIn == 0) {
  3613. enlargeIn = Math.pow(2, numBits);
  3614. numBits++;
  3615. }
  3616.  
  3617. if (dictionary[c]) {
  3618. entry = dictionary[c];
  3619. } else {
  3620. if (c === dictSize) {
  3621. entry = w + w.charAt(0);
  3622. } else {
  3623. return null;
  3624. }
  3625. }
  3626. result.push(entry);
  3627.  
  3628. // Add w+entry[0] to the dictionary.
  3629. dictionary[dictSize++] = w + entry.charAt(0);
  3630. enlargeIn--;
  3631.  
  3632. w = entry;
  3633.  
  3634. if (enlargeIn == 0) {
  3635. enlargeIn = Math.pow(2, numBits);
  3636. numBits++;
  3637. }
  3638. }
  3639. },
  3640. };
  3641. return LZString;
  3642. })();
  3643.  
  3644. var LZString$1 = LZString;
  3645.  
  3646. class UserCSS {
  3647. #settings;
  3648. #currentActivity;
  3649. #currentUser;
  3650. css = "";
  3651. preview = false;
  3652. cssInLocalStorage = "void-verified-user-css";
  3653. broadcastChannel;
  3654.  
  3655. constructor(settings) {
  3656. this.#settings = settings;
  3657. if (
  3658. this.#settings.auth?.token &&
  3659. this.#settings.options.profileCssEnabled.getValue()
  3660. ) {
  3661. const cssInLocalStorage = JSON.parse(
  3662. localStorage.getItem(this.cssInLocalStorage)
  3663. );
  3664. if (cssInLocalStorage) {
  3665. this.css = cssInLocalStorage.css;
  3666. this.preview = cssInLocalStorage.preview;
  3667. } else {
  3668. this.getAuthUserCss();
  3669. }
  3670. }
  3671.  
  3672. this.broadcastChannel = new BroadcastChannel("user-css");
  3673. this.broadcastChannel.addEventListener("message", (event) =>
  3674. this.#handleBroadcastMessage(event, this.#settings)
  3675. );
  3676. }
  3677.  
  3678. async checkActivityCss() {
  3679. if (
  3680. !this.#settings.options.activityCssEnabled.getValue() ||
  3681. !window.location.pathname.startsWith("/activity/")
  3682. ) {
  3683. return;
  3684. }
  3685.  
  3686. const activityId = window.location.pathname.match(
  3687. /^\/activity\/([^/]*)\/?/
  3688. )[1];
  3689.  
  3690. if (this.#currentActivity === activityId) {
  3691. return;
  3692. }
  3693.  
  3694. this.#currentActivity = activityId;
  3695. let activity;
  3696. try {
  3697. Toaster.debug("Querying user activity.");
  3698. const anilistAPI = new AnilistAPI(this.#settings);
  3699. activity = await anilistAPI.getActivityCss(activityId);
  3700. } catch {
  3701. Toaster.error("Failed to get activity CSS.");
  3702. return;
  3703. }
  3704.  
  3705. const username = activity.user?.name ?? activity.recipient?.name;
  3706.  
  3707. const userColor =
  3708. activity.user?.options.profileColor ??
  3709. activity.recipient?.options.profileColor;
  3710. const rgb = ColorFunctions.handleAnilistColor(userColor);
  3711.  
  3712. const activityEntry = document.querySelector(
  3713. ".container > .activity-entry"
  3714. );
  3715.  
  3716. activityEntry.style.setProperty("--color-blue", rgb);
  3717. activityEntry.style.setProperty("--color-blue-dim", rgb);
  3718.  
  3719. if (username === this.#settings.anilistUser && this.preview) {
  3720. this.#renderCss(this.css, "user-css");
  3721. return;
  3722. }
  3723.  
  3724. if (username === this.#currentUser) {
  3725. this.#clearGlobalCss();
  3726. return;
  3727. }
  3728. new StyleHandler(this.#settings).clearStyles("user-css");
  3729.  
  3730. if (!this.#shouldRenderCss(username)) {
  3731. return;
  3732. }
  3733.  
  3734. const about = activity.user?.about ?? activity.recipient?.about;
  3735.  
  3736. const css = this.#decodeAbout(about)?.customCSS;
  3737. if (css) {
  3738. this.#renderCss(css, "user-css");
  3739. } else {
  3740. Toaster.debug("User has no custom CSS.");
  3741. }
  3742.  
  3743. this.#currentUser = username;
  3744. }
  3745.  
  3746. resetCurrentActivity() {
  3747. this.#currentActivity = null;
  3748. }
  3749.  
  3750. async checkUserCss() {
  3751. if (
  3752. !this.#settings.options.profileCssEnabled.getValue() ||
  3753. !window.location.pathname.startsWith("/user/")
  3754. ) {
  3755. return;
  3756. }
  3757.  
  3758. const username =
  3759. window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];
  3760.  
  3761. if (username === this.#currentUser) {
  3762. return;
  3763. }
  3764.  
  3765. if (username === this.#settings.anilistUser && this.preview) {
  3766. this.#renderCss(this.css, "user-css");
  3767. return;
  3768. }
  3769.  
  3770. if (!this.#shouldRenderCss(username)) {
  3771. new StyleHandler(this.#settings).clearStyles("user-css");
  3772. return;
  3773. }
  3774.  
  3775. this.#currentUser = username;
  3776.  
  3777. let about;
  3778. try {
  3779. Toaster.debug("Querying user CSS.");
  3780. const anilistAPI = new AnilistAPI(this.#settings);
  3781. about = await anilistAPI.getUserAbout(username);
  3782. } catch (error) {
  3783. Toaster.error("Failed to load user's CSS.");
  3784. return;
  3785. }
  3786.  
  3787. const css = this.#decodeAbout(about)?.customCSS;
  3788. if (!css) {
  3789. Toaster.debug("User has no custom CSS.");
  3790. new StyleHandler(this.#settings).clearStyles("user-css");
  3791. }
  3792. this.#renderCss(css, "user-css");
  3793. }
  3794.  
  3795. resetCurrentUser() {
  3796. this.#currentUser = null;
  3797. }
  3798.  
  3799. updateCss(css) {
  3800. this.css = css;
  3801. if (this.preview) {
  3802. this.broadcastChannel.postMessage({ type: "css", css });
  3803. }
  3804. this.#saveToLocalStorage();
  3805. }
  3806.  
  3807. async publishUserCss() {
  3808. const username = this.#settings.anilistUser;
  3809. if (!username) {
  3810. return;
  3811. }
  3812.  
  3813. const anilistAPI = new AnilistAPI(this.#settings);
  3814. let about;
  3815. try {
  3816. Toaster.debug(
  3817. "Querying account user about to merge changes into."
  3818. );
  3819. about = await anilistAPI.getUserAbout(username);
  3820. } catch (error) {
  3821. Toaster.error(
  3822. "Failed to get current about for merging new CSS."
  3823. );
  3824. }
  3825. if (!about) {
  3826. about = "";
  3827. }
  3828. let aboutJson = this.#decodeAbout(about);
  3829. aboutJson.customCSS = this.css;
  3830. const compressedAbout = LZString$1.compressToBase64(
  3831. JSON.stringify(aboutJson)
  3832. );
  3833.  
  3834. const target = about.match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/)?.[1];
  3835.  
  3836. if (target) {
  3837. about = about.replace(target, compressedAbout);
  3838. } else {
  3839. about = `[](json${compressedAbout})\n\n` + about;
  3840. }
  3841. try {
  3842. Toaster.debug("Publishing CSS.");
  3843. await anilistAPI.saveUserAbout(about);
  3844. Toaster.success("CSS published.");
  3845. } catch (error) {
  3846. Toaster.error("Failed to publish CSS changes.");
  3847. }
  3848. }
  3849.  
  3850. togglePreview() {
  3851. this.preview = !this.preview;
  3852. this.broadcastChannel.postMessage({
  3853. type: "preview",
  3854. preview: this.preview,
  3855. });
  3856. this.#saveToLocalStorage();
  3857. }
  3858.  
  3859. async getAuthUserCss() {
  3860. const anilistAPI = new AnilistAPI(this.#settings);
  3861. const username = this.#settings.anilistUser;
  3862. if (!username) {
  3863. return;
  3864. }
  3865. try {
  3866. Toaster.debug("Querying account user CSS.");
  3867. const about = await anilistAPI.getUserAbout(username);
  3868. const css = this.#decodeAbout(about).customCSS;
  3869. this.css = css;
  3870. this.#saveToLocalStorage();
  3871. return css;
  3872. } catch (error) {
  3873. Toaster.error("Failed to query account user CSS.");
  3874. }
  3875. }
  3876.  
  3877. #handleBroadcastMessage(event, settings) {
  3878. switch (event.data.type) {
  3879. case "css":
  3880. this.#handlePreviewCssMessage(event.data.css, settings);
  3881. break;
  3882. case "preview":
  3883. this.#handlePreviewToggleMessage(event.data.preview);
  3884. break;
  3885. }
  3886. }
  3887.  
  3888. #handlePreviewCssMessage(css, settings) {
  3889. this.css = css;
  3890. const hasUserCss = document.getElementById(
  3891. "void-verified-user-css-styles"
  3892. );
  3893. if (hasUserCss) {
  3894. new StyleHandler(settings).createStyleLink(css, "user-css");
  3895. }
  3896. }
  3897.  
  3898. #handlePreviewToggleMessage(preview) {
  3899. this.preview = preview;
  3900. const hasUserCss = document.getElementById(
  3901. "void-verified-user-css-styles"
  3902. );
  3903. if (!hasUserCss) {
  3904. return;
  3905. }
  3906.  
  3907. this.resetCurrentUser();
  3908. this.resetCurrentActivity();
  3909.  
  3910. this.checkUserCss();
  3911. this.checkActivityCss();
  3912. }
  3913.  
  3914. #saveToLocalStorage() {
  3915. localStorage.setItem(
  3916. this.cssInLocalStorage,
  3917. JSON.stringify({
  3918. css: this.css,
  3919. preview: this.preview,
  3920. })
  3921. );
  3922. }
  3923.  
  3924. #shouldRenderCss(username) {
  3925. const user = this.#settings.getUser(username);
  3926. if (
  3927. this.#settings.options.onlyLoadCssFromVerifiedUser.getValue() &&
  3928. !this.#settings.isVerified(username)
  3929. ) {
  3930. return false;
  3931. }
  3932. if (user?.onlyLoadCssFromVerifiedUser) {
  3933. return true;
  3934. }
  3935. return !this.#userSpecificRenderingExists();
  3936. }
  3937.  
  3938. #userSpecificRenderingExists() {
  3939. return this.#settings.verifiedUsers.some(
  3940. (user) => user.onlyLoadCssFromVerifiedUser
  3941. );
  3942. }
  3943.  
  3944. #renderCss(css, id) {
  3945. if (!css) {
  3946. return;
  3947. }
  3948.  
  3949. const styleHandler = new StyleHandler(this.#settings);
  3950. styleHandler.createStyleLink(css, id);
  3951. this.#clearGlobalCss();
  3952. }
  3953.  
  3954. #clearGlobalCss() {
  3955. if (this.#settings.options.globalCssAutoDisable.getValue()) {
  3956. new StyleHandler(this.#settings).clearStyles("global-css");
  3957. }
  3958. }
  3959.  
  3960. #decodeAbout(about) {
  3961. let json = (about || "").match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/);
  3962. if (!json) {
  3963. return {
  3964. customCss: "",
  3965. };
  3966. }
  3967.  
  3968. let jsonData;
  3969. try {
  3970. jsonData = JSON.parse(atob(json[1]));
  3971. } catch (e) {
  3972. jsonData = JSON.parse(LZString$1.decompressFromBase64(json[1]));
  3973. }
  3974. return jsonData;
  3975. }
  3976. }
  3977.  
  3978. const markdownRegex = [
  3979. {
  3980. regex: /^##### (.*$)/gim,
  3981. format: "<h5>$1</h5>",
  3982. },
  3983. {
  3984. regex: /^#### (.*$)/gim,
  3985. format: "<h4>$1</h4>",
  3986. },
  3987. {
  3988. regex: /^### (.*$)/gim,
  3989. format: "<h3>$1</h3>",
  3990. },
  3991. {
  3992. regex: /^## (.*$)/gim,
  3993. format: "<h2>$1</h2>",
  3994. },
  3995. {
  3996. regex: /^# (.*$)/gim,
  3997. format: "<h1>$1</h1>",
  3998. },
  3999. {
  4000. regex: /\_\_(.*)\_\_/gim,
  4001. format: "<strong>$1</strong>",
  4002. },
  4003. {
  4004. regex: /\_(.*)\_/gim,
  4005. format: "<em>$1</em>",
  4006. },
  4007. {
  4008. regex: /(?:\r\n|\r|\n)/g,
  4009. format: "<br>",
  4010. },
  4011. {
  4012. regex: /\~~~(.*)\~~~/gim,
  4013. format: "<center>$1</center>",
  4014. },
  4015. {
  4016. regex: /\[([^\]]*)\]\(([^\)]+)\)/gi,
  4017. format: "<a href='$2' target='_blank'>$1</a>",
  4018. },
  4019. {
  4020. regex: /\~\!(.*)\!\~/gi,
  4021. format: "<span class='markdown-spoiler'><span>$1</span></span>",
  4022. },
  4023. {
  4024. regex: /img([0-9]+%?)\(([^\)]+)\)/g,
  4025. format: "<img src='$2' width='$1' >",
  4026. },
  4027. ];
  4028.  
  4029. class Markdown {
  4030. static parse(markdown) {
  4031. let html = markdown;
  4032. for (const parser of markdownRegex) {
  4033. html = html.replace(parser.regex, parser.format);
  4034. }
  4035.  
  4036. return html;
  4037. }
  4038. }
  4039.  
  4040. class Layout {
  4041. avatar;
  4042. banner;
  4043. bio;
  4044. color;
  4045. donatorBadge;
  4046.  
  4047. constructor(layout) {
  4048. this.avatar = layout?.avatar ?? "";
  4049. this.banner = layout?.banner ?? "";
  4050. this.bio = layout?.bio ?? "";
  4051. this.color = layout?.color ?? "";
  4052. this.donatorBadge = layout?.donatorBadge ?? "";
  4053. }
  4054. }
  4055.  
  4056. class LayoutDesigner {
  4057. #settings;
  4058. #layoutsInLocalStorage = "void-verified-layouts";
  4059. #originalHtml;
  4060. #broadcastChannel;
  4061. #donatorTier = 0;
  4062. #anilistSettings;
  4063. #layout;
  4064. #layouts = {
  4065. selectedLayout: 0,
  4066. preview: false,
  4067. disableCss: false,
  4068. layoutsList: [new Layout()],
  4069. };
  4070.  
  4071. constructor(settings) {
  4072. this.#settings = settings;
  4073.  
  4074. this.#broadcastChannel = new BroadcastChannel("void-layouts");
  4075. this.#broadcastChannel.addEventListener("message", (event) =>
  4076. this.#handleBroadcastMessage(event)
  4077. );
  4078.  
  4079. const layouts = JSON.parse(
  4080. localStorage.getItem(this.#layoutsInLocalStorage)
  4081. );
  4082. if (layouts) {
  4083. this.#layouts = layouts;
  4084. this.#layouts.layoutsList = layouts.layoutsList.map(
  4085. (layout) => new Layout(layout)
  4086. );
  4087. }
  4088.  
  4089. this.#anilistSettings = JSON.parse(localStorage.getItem("auth"));
  4090.  
  4091. this.#donatorTier = this.#anilistSettings?.donatorTier;
  4092. this.#layout = this.#getSelectedLayout();
  4093. }
  4094.  
  4095. renderLayoutPreview() {
  4096. if (!this.#settings.options.layoutDesignerEnabled.getValue()) {
  4097. return;
  4098. }
  4099.  
  4100. if (!window.location.pathname.startsWith("/user/")) {
  4101. return;
  4102. }
  4103. const username =
  4104. window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];
  4105.  
  4106. if (
  4107. username !== this.#settings.anilistUser ||
  4108. !this.#layouts.preview
  4109. ) {
  4110. return;
  4111. }
  4112.  
  4113. this.#handleAvatar(this.#layout.avatar);
  4114. this.#handleBanner(this.#layout.banner);
  4115. this.#handleColor(this.#layout.color);
  4116. this.#handleDonatorBadge(this.#layout.donatorBadge);
  4117. this.#handleCss();
  4118. this.#handleAbout(Markdown.parse(this.#layout.bio ?? ""));
  4119. }
  4120.  
  4121. #handleBroadcastMessage(event) {
  4122. switch (event.data.type) {
  4123. case "preview":
  4124. this.#handlePreviewToggleMessage(event.data.preview);
  4125. break;
  4126. case "layout":
  4127. this.#handleLayoutMessage(event.data.layout);
  4128. break;
  4129. case "css":
  4130. this.#handleCssMessage(event.data.disableCss);
  4131. break;
  4132. }
  4133. }
  4134.  
  4135. #handlePreviewToggleMessage(preview) {
  4136. this.#layouts.preview = preview;
  4137. if (preview) {
  4138. return;
  4139. }
  4140.  
  4141. this.#handleAvatar(this.#anilistSettings?.avatar?.large);
  4142. this.#handleBanner(this.#anilistSettings?.bannerImage);
  4143. this.#handleColor(this.#anilistSettings.options.profileColor);
  4144. this.#handleDonatorBadge(this.#anilistSettings.donatorBadge);
  4145. this.#layouts.disableCss = false;
  4146. this.#handleCss();
  4147. this.#handleAbout(this.#originalHtml);
  4148. }
  4149.  
  4150. #handleLayoutMessage(layout) {
  4151. this.#layout = layout;
  4152. }
  4153.  
  4154. #handleCssMessage(disableCss) {
  4155. this.#layouts.disableCss = disableCss;
  4156. }
  4157.  
  4158. #handleAvatar(avatar) {
  4159. if (avatar === "") {
  4160. return;
  4161. }
  4162.  
  4163. const avatarElement = DOM.get("img.avatar");
  4164. avatarElement.src = avatar;
  4165.  
  4166. const avatarLinks = DOM.getAll(
  4167. `a.avatar[href*="${this.#settings.anilistUser}"]`
  4168. );
  4169. for (const avatarLink of avatarLinks) {
  4170. avatarLink.style = `background-image: url(${avatar})`;
  4171. }
  4172. }
  4173.  
  4174. #handleBanner(banner) {
  4175. if (banner === "") {
  4176. return;
  4177. }
  4178.  
  4179. const bannerElement = DOM.get(".banner");
  4180. bannerElement.style = `background-image: url(${banner})`;
  4181. }
  4182.  
  4183. #handleColor(value) {
  4184. let color;
  4185. try {
  4186. color = ColorFunctions.handleAnilistColor(value);
  4187. } catch (err) {
  4188. return;
  4189. }
  4190.  
  4191. const pageContent = DOM.get(".page-content > .user");
  4192. pageContent.style.setProperty("--color-blue", color);
  4193. pageContent.style.setProperty("--color-blue-dim", color);
  4194. }
  4195.  
  4196. #handleDonatorBadge(donatorText) {
  4197. if (this.#donatorTier < 3 || donatorText === "") {
  4198. return;
  4199. }
  4200.  
  4201. const donatorBadge = DOM.get(".donator-badge");
  4202. donatorBadge.innerText = donatorText;
  4203. }
  4204.  
  4205. #handleCss() {
  4206. if (this.#layouts.disableCss) {
  4207. DOM.get("#void-verified-user-css-styles")?.setAttribute(
  4208. "disabled",
  4209. true
  4210. );
  4211. } else {
  4212. DOM.get("#void-verified-user-css-styles")?.removeAttribute(
  4213. "disabled"
  4214. );
  4215. }
  4216. }
  4217.  
  4218. #handleAbout(about) {
  4219. const aboutContainer = DOM.get(".about .markdown");
  4220.  
  4221. if (!this.#originalHtml) {
  4222. this.#originalHtml = aboutContainer.innerHTML;
  4223. }
  4224.  
  4225. aboutContainer.innerHTML =
  4226. about !== "" ? about : this.#originalHtml;
  4227. }
  4228.  
  4229. renderSettings(settingsUi) {
  4230. if (!this.#settings.options.layoutDesignerEnabled.getValue()) {
  4231. return "";
  4232. }
  4233. const container = DOM.create("div", "layout-designer-container");
  4234.  
  4235. const header = DOM.create("h3", null, "Layout Designer");
  4236.  
  4237. const imageSection = DOM.create("div");
  4238.  
  4239. imageSection.append(
  4240. this.#createImageField(
  4241. "avatar",
  4242. this.#layout.avatar,
  4243. settingsUi
  4244. )
  4245. );
  4246.  
  4247. imageSection.append(
  4248. this.#createImageField(
  4249. "banner",
  4250. this.#layout.banner,
  4251. settingsUi
  4252. )
  4253. );
  4254.  
  4255. const imageUploadNote = Note(
  4256. "You can preview avatar & banner by providing a link to an image. If you have configured a image host, you can upload images by pasting them to the fields. "
  4257. );
  4258.  
  4259. imageUploadNote.append(
  4260. DOM.create("br"),
  4261. "Unfortunately AniList API does not support third parties uploading new avatars or banners. You have to upload them separately."
  4262. );
  4263.  
  4264. const colorSelection = this.#createColorSelection(settingsUi);
  4265.  
  4266. const previewButton = Button(
  4267. this.#layouts.preview ? "Disable Preview" : "Enable Preview",
  4268. () => {
  4269. this.#togglePreview(settingsUi);
  4270. }
  4271. );
  4272.  
  4273. const cssButton = Button(
  4274. this.#layouts.disableCss ? "Enable Css" : "Disable Css",
  4275. () => {
  4276. this.#toggleCss();
  4277. cssButton.innerText = this.#layouts.disableCss
  4278. ? "Enable Css"
  4279. : "Disable Css";
  4280. }
  4281. );
  4282.  
  4283. const getAboutButton = Button("Reset About", () => {
  4284. this.#getUserAbout(settingsUi);
  4285. });
  4286.  
  4287. container.append(
  4288. header,
  4289. imageSection,
  4290. imageUploadNote,
  4291. colorSelection
  4292. );
  4293.  
  4294. if (this.#donatorTier >= 3) {
  4295. container.append(this.#createDonatorBadgeField(settingsUi));
  4296. }
  4297.  
  4298. container.append(
  4299. this.#createAboutSection(settingsUi),
  4300. getAboutButton
  4301. );
  4302.  
  4303. if (this.#settings.auth?.token) {
  4304. const saveAboutButton = Button("Publish About", (event) => {
  4305. this.#publishAbout(event, settingsUi);
  4306. });
  4307. container.append(saveAboutButton);
  4308. }
  4309.  
  4310. container.append(previewButton);
  4311.  
  4312. if (this.#layouts.preview) {
  4313. container.append(cssButton);
  4314. }
  4315. return container;
  4316. }
  4317.  
  4318. #createInputField(field, value, settingsUi) {
  4319. const input = InputField(value, (event) => {
  4320. this.#updateOption(field, event.target.value, settingsUi);
  4321. });
  4322. return input;
  4323. }
  4324.  
  4325. #createImageField(field, value, settingsUi) {
  4326. const container = DOM.create("div", "layout-image-container");
  4327. const header = DOM.create("h5", "layout-header", field);
  4328. const display = DOM.create("div", `layout-image-display ${field}`);
  4329. display.style.backgroundImage = `url(${value})`;
  4330. const input = this.#createInputField(field, value, settingsUi);
  4331.  
  4332. container.append(header, display, input);
  4333. return container;
  4334. }
  4335.  
  4336. #createDonatorBadgeField(settingsUi) {
  4337. const container = DOM.create(
  4338. "div",
  4339. "layout-donator-badge-container"
  4340. );
  4341. const donatorHeader = DOM.create(
  4342. "h5",
  4343. "layout-header",
  4344. "Donator Badge"
  4345. );
  4346. const donatorInput = InputField(
  4347. this.#layout.donatorBadge,
  4348. (event) => {
  4349. this.#updateOption(
  4350. "donatorBadge",
  4351. event.target.value,
  4352. settingsUi
  4353. );
  4354. }
  4355. );
  4356. donatorInput.setAttribute("maxlength", 24);
  4357.  
  4358. container.append(donatorHeader, donatorInput);
  4359.  
  4360. if (
  4361. this.#layout.donatorBadge !==
  4362. this.#anilistSettings.donatorBadge &&
  4363. this.#layout.donatorBadge !== "" &&
  4364. this.#settings.auth?.token
  4365. ) {
  4366. const publishButton = Button(
  4367. "Publish Donator Badge",
  4368. (event) => {
  4369. this.#publishDonatorText(event, settingsUi);
  4370. }
  4371. );
  4372. container.append(DOM.create("div", null, publishButton));
  4373. }
  4374.  
  4375. return container;
  4376. }
  4377.  
  4378. #createColorSelection(settingsUi) {
  4379. const container = DOM.create("div", "layout-color-selection");
  4380.  
  4381. const header = DOM.create("h5", "layout-header", "Color");
  4382. container.append(header);
  4383.  
  4384. for (const anilistColor of ColorFunctions.defaultColors) {
  4385. container.append(
  4386. this.#createColorButton(anilistColor, settingsUi)
  4387. );
  4388. }
  4389.  
  4390. if (this.#donatorTier >= 2) {
  4391. const isDefaultColor = ColorFunctions.defaultColors.some(
  4392. (color) => color === this.#layout.color
  4393. );
  4394.  
  4395. const colorInput = ColorPicker(
  4396. isDefaultColor ? "" : this.#layout.color,
  4397. (event) => {
  4398. this.#updateOption(
  4399. "color",
  4400. event.target.value,
  4401. settingsUi
  4402. );
  4403. }
  4404. );
  4405. if (!isDefaultColor && this.#layout.color !== "") {
  4406. colorInput.classList.add("active");
  4407. }
  4408. container.append(colorInput);
  4409. }
  4410.  
  4411. if (
  4412. this.#settings.auth?.token &&
  4413. this.#layout.color.toLocaleLowerCase() !==
  4414. this.#anilistSettings?.options?.profileColor?.toLocaleLowerCase() &&
  4415. this.#layout.color !== ""
  4416. ) {
  4417. const publishButton = Button("Publish Color", (event) => {
  4418. this.#publishColor(event, settingsUi);
  4419. });
  4420. container.append(DOM.create("div", null, publishButton));
  4421. }
  4422.  
  4423. return container;
  4424. }
  4425.  
  4426. #createAboutSection(settingsUi) {
  4427. const container = DOM.create("div");
  4428. const aboutHeader = DOM.create("h5", "layout-header", "About");
  4429. const aboutInput = TextArea(this.#layout.bio, (event) => {
  4430. this.#updateOption("bio", event.target.value, settingsUi);
  4431. });
  4432. const note = Note(
  4433. "Please note that VoidVerified does not have access to AniList's markdown parser. AniList specific features might not be available while previewing. Recommended to be used for smaller changes like previewing a different image for a layout."
  4434. );
  4435.  
  4436. container.append(aboutHeader, aboutInput, note);
  4437. return container;
  4438. }
  4439.  
  4440. async #publishAbout(event, settingsUi) {
  4441. const button = event.target;
  4442. button.innerText = "Publishing...";
  4443.  
  4444. try {
  4445. const anilistAPI = new AnilistAPI(this.#settings);
  4446. let currentAbout = await anilistAPI.getUserAbout(
  4447. this.#settings.anilistUser
  4448. );
  4449. if (!currentAbout) {
  4450. currentAbout = "";
  4451. }
  4452. const about = this.#transformAbout(
  4453. currentAbout,
  4454. this.#layout.bio
  4455. );
  4456.  
  4457. await anilistAPI.saveUserAbout(about);
  4458. Toaster.success("About published.");
  4459. settingsUi.renderSettingsUiContent();
  4460. } catch (error) {
  4461. console.error(error);
  4462. Toaster.error("Failed to publish about.");
  4463. }
  4464. }
  4465.  
  4466. #transformAbout(currentAbout, newAbout) {
  4467. const json = currentAbout.match(
  4468. /^\[\]\(json([A-Za-z0-9+/=]+)\)/
  4469. )?.[1];
  4470.  
  4471. if (!json) {
  4472. return newAbout;
  4473. }
  4474.  
  4475. const about = `[](json${json})` + newAbout;
  4476. return about;
  4477. }
  4478.  
  4479. async #publishColor(event, settingsUi) {
  4480. const button = event.target;
  4481. const color = this.#layout.color;
  4482. button.innerText = "Publishing...";
  4483.  
  4484. try {
  4485. const anilistAPI = new AnilistAPI(this.#settings);
  4486. const result = await anilistAPI.saveUserColor(color);
  4487. const profileColor = result.UpdateUser?.options?.profileColor;
  4488. this.#anilistSettings.options.profileColor = profileColor;
  4489. Toaster.success("Color published.");
  4490. } catch (error) {
  4491. Toaster.error("Failed to publish color.");
  4492. console.error("Failed to publish color.", error);
  4493. } finally {
  4494. settingsUi.renderSettingsUiContent();
  4495. }
  4496. }
  4497.  
  4498. async #publishDonatorText(event, settingsUi) {
  4499. const button = event.target;
  4500. const donatorText = this.#layout.donatorBadge;
  4501. button.innerText = "Publishing...";
  4502.  
  4503. try {
  4504. const anilistAPI = new AnilistAPI(this.#settings);
  4505. const result = await anilistAPI.saveDonatorBadge(donatorText);
  4506. const donatorBadge = result.UpdateUser?.donatorBadge;
  4507. this.#anilistSettings.donatorBadge = donatorBadge;
  4508. Toaster.success("Donator badge published.");
  4509. } catch (error) {
  4510. Toaster.error("Failed to publish donator badge.");
  4511. console.error("Failed to publish donator badge.", error);
  4512. } finally {
  4513. settingsUi.renderSettingsUiContent();
  4514. }
  4515. }
  4516.  
  4517. async #getUserAbout(settingsUi) {
  4518. if (
  4519. this.#layout.bio !== "" &&
  4520. !window.confirm(
  4521. "Are you sure you want to reset about? Any changes will be lost."
  4522. )
  4523. ) {
  4524. return;
  4525. }
  4526.  
  4527. try {
  4528. Toaster.debug("Querying user about.");
  4529. const anilistAPI = new AnilistAPI(this.#settings);
  4530. const about = await anilistAPI.getUserAbout(
  4531. this.#settings.anilistUser
  4532. );
  4533. const clearedAbout = this.#removeJson(about);
  4534.  
  4535. this.#updateOption("bio", clearedAbout, settingsUi);
  4536. Toaster.success("About reset.");
  4537. } catch (error) {
  4538. Toaster.error(
  4539. "Failed to query current about from AniList API."
  4540. );
  4541. }
  4542. }
  4543.  
  4544. #removeJson(about) {
  4545. return about.replace(/^\[\]\(json([A-Za-z0-9+/=]+)\)/, "");
  4546. }
  4547.  
  4548. #createColorButton(anilistColor, settingsUi) {
  4549. const button = DOM.create("div", "color-button");
  4550. button.style.backgroundColor = `rgb(${ColorFunctions.handleAnilistColor(
  4551. anilistColor
  4552. )})`;
  4553.  
  4554. button.addEventListener("click", () => {
  4555. this.#updateOption("color", anilistColor, settingsUi);
  4556. });
  4557.  
  4558. if (this.#layout.color === anilistColor) {
  4559. button.classList.add("active");
  4560. }
  4561.  
  4562. return button;
  4563. }
  4564.  
  4565. #updateOption(field, value, settingsUi) {
  4566. this.#layout[field] = value;
  4567. this.#updateLayout(this.#layout);
  4568. settingsUi.renderSettingsUiContent();
  4569. }
  4570.  
  4571. #togglePreview(settingsUi) {
  4572. this.#layouts.preview = !this.#layouts.preview;
  4573. if (!this.#layouts.preview) {
  4574. this.#layouts.disableCss = false;
  4575. }
  4576. this.#broadcastChannel.postMessage({
  4577. type: "preview",
  4578. preview: this.#layouts.preview,
  4579. });
  4580. this.#saveToLocalStorage();
  4581. settingsUi.renderSettingsUiContent();
  4582. }
  4583.  
  4584. #toggleCss() {
  4585. this.#layouts.disableCss = !this.#layouts.disableCss;
  4586. this.#broadcastChannel.postMessage({
  4587. type: "css",
  4588. disableCss: this.#layouts.disableCss,
  4589. });
  4590. this.#saveToLocalStorage();
  4591. }
  4592.  
  4593. #getSelectedLayout() {
  4594. return this.#layouts.layoutsList[this.#layouts.selectedLayout];
  4595. }
  4596.  
  4597. #updateLayout(layout) {
  4598. this.#layouts.layoutsList[this.#layouts.selectedLayout] = layout;
  4599. this.#saveToLocalStorage();
  4600. this.#broadcastChannel.postMessage({
  4601. type: "layout",
  4602. layout: this.#layout,
  4603. });
  4604. }
  4605.  
  4606. #saveToLocalStorage() {
  4607. localStorage.setItem(
  4608. this.#layoutsInLocalStorage,
  4609. JSON.stringify(this.#layouts)
  4610. );
  4611. }
  4612. }
  4613.  
  4614. class IntervalScriptHandler {
  4615. styleHandler;
  4616. settingsUi;
  4617. activityHandler;
  4618. settings;
  4619. globalCSS;
  4620. quickAccess;
  4621. userCSS;
  4622. layoutDesigner;
  4623. constructor(settings) {
  4624. this.settings = settings;
  4625.  
  4626. this.styleHandler = new StyleHandler(settings);
  4627. this.globalCSS = new GlobalCSS(settings);
  4628. this.userCSS = new UserCSS(settings);
  4629. this.layoutDesigner = new LayoutDesigner(settings);
  4630.  
  4631. this.settingsUi = new SettingsUserInterface(
  4632. settings,
  4633. this.styleHandler,
  4634. this.globalCSS,
  4635. this.userCSS,
  4636. this.layoutDesigner
  4637. );
  4638. this.activityHandler = new ActivityHandler(settings);
  4639. this.quickAccess = new QuickAccess(settings);
  4640. }
  4641.  
  4642. currentPath = "";
  4643. evaluationIntervalInSeconds = 1;
  4644. hasPathChanged(path) {
  4645. if (path === this.currentPath) {
  4646. return false;
  4647. }
  4648. this.currentPath = path;
  4649. return true;
  4650. }
  4651.  
  4652. handleIntervalScripts(intervalScriptHandler) {
  4653. const path = window.location.pathname;
  4654.  
  4655. intervalScriptHandler.activityHandler.moveAndDisplaySubscribeButton();
  4656. intervalScriptHandler.globalCSS.clearCssForProfile();
  4657. intervalScriptHandler.layoutDesigner.renderLayoutPreview();
  4658.  
  4659. if (path === "/home") {
  4660. intervalScriptHandler.styleHandler.refreshHomePage();
  4661. intervalScriptHandler.quickAccess.renderQuickAccess();
  4662. }
  4663.  
  4664. if (!path.startsWith("/settings/developer")) {
  4665. intervalScriptHandler.settingsUi.removeSettingsUi();
  4666. }
  4667.  
  4668. if (!intervalScriptHandler.hasPathChanged(path)) {
  4669. return;
  4670. }
  4671.  
  4672. if (path.startsWith("/user/")) {
  4673. intervalScriptHandler.userCSS.checkUserCss();
  4674. intervalScriptHandler.quickAccess.clearBadge();
  4675. intervalScriptHandler.styleHandler.verifyProfile();
  4676. } else {
  4677. intervalScriptHandler.styleHandler.clearStyles("profile");
  4678. }
  4679.  
  4680. if (path.startsWith("/activity/")) {
  4681. intervalScriptHandler.userCSS.checkActivityCss();
  4682. }
  4683.  
  4684. if (!path.startsWith("/activity/") && !path.startsWith("/user/")) {
  4685. intervalScriptHandler.userCSS.resetCurrentActivity();
  4686. intervalScriptHandler.userCSS.resetCurrentUser();
  4687. intervalScriptHandler.styleHandler.clearStyles("user-css");
  4688. }
  4689.  
  4690. intervalScriptHandler.globalCSS.createCss();
  4691.  
  4692. if (path.startsWith("/settings/developer")) {
  4693. intervalScriptHandler.settingsUi.renderSettingsUi();
  4694. }
  4695. }
  4696.  
  4697. enableScriptIntervalHandling() {
  4698. const interval = setInterval(() => {
  4699. try {
  4700. this.handleIntervalScripts(this);
  4701. } catch (error) {
  4702. Toaster.critical([
  4703. "A critical error has occured running interval script loop. VoidVerified is not working correctly. Please check developer console and contact ",
  4704. Link(
  4705. "voidnyan",
  4706. "https://anilist.co/user/voidnyan/",
  4707. "_blank"
  4708. ),
  4709. ".",
  4710. ]);
  4711. clearInterval(interval);
  4712. console.error(error);
  4713. }
  4714. }, this.evaluationIntervalInSeconds * 1000);
  4715. }
  4716. }
  4717.  
  4718. class PasteHandler {
  4719. settings;
  4720.  
  4721. #imageFormats = [
  4722. "jpg",
  4723. "png",
  4724. "gif",
  4725. "webp",
  4726. "apng",
  4727. "avif",
  4728. "jpeg",
  4729. "svg",
  4730. ];
  4731.  
  4732. #uploadInProgress = false;
  4733. constructor(settings) {
  4734. this.settings = settings;
  4735. }
  4736.  
  4737. setup() {
  4738. window.addEventListener("paste", (event) => {
  4739. this.#handlePaste(event);
  4740. });
  4741. }
  4742.  
  4743. async #handlePaste(event) {
  4744. if (
  4745. event.target.tagName !== "TEXTAREA" &&
  4746. event.target.tagName !== "INPUT"
  4747. ) {
  4748. return;
  4749. }
  4750.  
  4751. const clipboard = event.clipboardData.getData("text/plain").trim();
  4752. let result = [];
  4753.  
  4754. const file = event.clipboardData.items[0]?.getAsFile();
  4755. if (
  4756. file &&
  4757. this.settings.options.pasteImagesToHostService.getValue()
  4758. ) {
  4759. event.preventDefault();
  4760. result = await this.#handleImages(event);
  4761. } else if (this.settings.options.pasteEnabled.getValue()) {
  4762. event.preventDefault();
  4763. const rows = clipboard.split("\n");
  4764.  
  4765. for (let row of rows) {
  4766. result.push(this.#handleRow(row, event));
  4767. }
  4768. } else {
  4769. return;
  4770. }
  4771.  
  4772. const transformedClipboard = result.join("\n\n");
  4773. window.document.execCommand(
  4774. "insertText",
  4775. false,
  4776. transformedClipboard
  4777. );
  4778. }
  4779.  
  4780. async #handleImages(event) {
  4781. const _files = event.clipboardData.items;
  4782. if (this.#uploadInProgress) {
  4783. return;
  4784. }
  4785. this.#uploadInProgress = true;
  4786. document.body.classList.add("void-upload-in-progress");
  4787.  
  4788. const imageApi = new ImageApiFactory().getImageHostInstance();
  4789.  
  4790. const files = Object.values(_files).map((file) => file.getAsFile());
  4791. const images = files.filter((file) =>
  4792. file.type.startsWith("image/")
  4793. );
  4794.  
  4795. try {
  4796. const results = await Promise.all(
  4797. images.map((image) => imageApi.uploadImage(image))
  4798. );
  4799. return results
  4800. .filter((url) => url !== null)
  4801. .map((url) => this.#handleRow(url, event));
  4802. } catch (error) {
  4803. console.error(error);
  4804. return [];
  4805. } finally {
  4806. this.#uploadInProgress = false;
  4807. document.body.classList.remove("void-upload-in-progress");
  4808. }
  4809. }
  4810.  
  4811. #handleRow(row, event) {
  4812. if (
  4813. event.target.parentElement.classList.contains(
  4814. "void-css-editor"
  4815. ) ||
  4816. event.target.tagName === "INPUT"
  4817. ) {
  4818. return row;
  4819. }
  4820.  
  4821. row = row.trim();
  4822. if (
  4823. this.#imageFormats.some((format) =>
  4824. row.toLowerCase().endsWith(format)
  4825. )
  4826. ) {
  4827. return this.#handleImg(row);
  4828. } else if (row.toLowerCase().startsWith("http")) {
  4829. return `[](${row})`;
  4830. } else {
  4831. return row;
  4832. }
  4833. }
  4834.  
  4835. #handleImg(row) {
  4836. const img = `img${this.settings.options.pasteImageWidth.getValue()}(${row})`;
  4837. let result = img;
  4838. if (this.settings.options.pasteWrapImagesWithLink.getValue()) {
  4839. result = `[ ${img} ](${row})`;
  4840. }
  4841. return result;
  4842. }
  4843. }
  4844.  
  4845. const styles = /* css */ `
  4846. :root {
  4847. --void-info: 46, 149, 179;
  4848. --void-error: 188, 53, 46;
  4849. --void-success: 80, 162, 80;
  4850. --void-warning: 232, 180, 2;
  4851. }
  4852.  
  4853.  
  4854. a[href="/settings/developer" i]::after{content: " & Void"}
  4855. .void-settings .void-nav ol {
  4856. display: flex;
  4857. margin: 8px 0px;
  4858. padding: 0;
  4859. }
  4860.  
  4861. .void-nav {
  4862. margin-top: 3rem;
  4863. }
  4864.  
  4865. .void-settings .void-nav li {
  4866. list-style: none;
  4867. display: block;
  4868. color: rgb(var(--color-text));
  4869. padding: 4px 8px;
  4870. text-transform: capitalize;
  4871. background: rgb(var(--color-foreground-blue));
  4872. cursor: pointer;
  4873. min-width: 50px;
  4874. text-align: center;
  4875. font-size: 1.4rem;
  4876. }
  4877.  
  4878. .void-settings .void-nav li.void-active,
  4879. .void-settings .void-nav li:hover {
  4880. background: rgb(var(--color-blue));
  4881. color: rgb(var(--color-text-bright));
  4882. }
  4883.  
  4884. .void-settings .void-nav li:first-child {
  4885. border-radius: 4px 0px 0px 4px;
  4886. }
  4887.  
  4888. .void-settings .void-nav li:last-child {
  4889. border-radius: 0px 4px 4px 0px;
  4890. }
  4891. .void-settings .void-settings-header {
  4892. margin-top: 30px;
  4893. }
  4894.  
  4895. .void-settings .void-table table {
  4896. border-collapse: collapse;
  4897. }
  4898.  
  4899. .void-settings .void-table :is(th, td) {
  4900. padding: 2px 6px !important;
  4901. }
  4902.  
  4903. .void-settings .void-table :is(th, td):first-child {
  4904. border-radius: 4px 0px 0px 4px;
  4905. }
  4906.  
  4907. .void-settings .void-table :is(th, td):last-child {
  4908. border-radius: 0px 4px 4px 0px;
  4909. }
  4910.  
  4911. .void-settings .void-table tbody tr:hover {
  4912. background-color: rgba(var(--color-foreground-blue), .7);
  4913. }
  4914.  
  4915. .void-settings .void-table input[type="color"] {
  4916. border: 0;
  4917. height: 24px;
  4918. width: 40px;
  4919. padding: 0;
  4920. background-color: unset;
  4921. cursor: pointer;
  4922. }
  4923.  
  4924. .void-settings .void-table button {
  4925. background: unset;
  4926. border: none;
  4927. cursor: pointer;
  4928. padding: 0;
  4929. }
  4930.  
  4931. .void-settings .void-table form {
  4932. padding: 8px;
  4933. display: flex;
  4934. align-items: center;
  4935. gap: 8px;
  4936. }
  4937.  
  4938. .void-settings .void-settings-header span {
  4939. color: rgb(var(--color-blue));
  4940. }
  4941.  
  4942. .void-settings .void-settings-list {
  4943. display: flex;
  4944. flex-direction: column;
  4945. gap: 5px;
  4946. }
  4947.  
  4948. .void-setting-label {
  4949. margin-left: 6px;
  4950. vertical-align: middle;
  4951. cursor: pointer;
  4952. }
  4953.  
  4954. .void-setting-label-container .void-checkbox {
  4955. vertical-align: middle;
  4956. }
  4957.  
  4958. .void-checkbox {
  4959. cursor: pointer;
  4960. }
  4961.  
  4962. .void-settings .void-settings-list input.void-input {
  4963. width: 50px;
  4964. text-align: center;
  4965. height: 20px;
  4966. font-size: 12px;
  4967. }
  4968.  
  4969. .void-settings .void-settings-list label {
  4970. margin-left: 8px;
  4971. }
  4972.  
  4973. .void-settings .void-css-editor label {
  4974. margin-top: 20px;
  4975. fontSize: 2rem;
  4976. display: inline-block;
  4977. }
  4978.  
  4979. .void-textarea {
  4980. width: 100%;
  4981. height: 300px;
  4982. min-height: 200px;
  4983. resize: vertical;
  4984. background: rgb(var(--color-foreground-blue));
  4985. color: rgb(var(--color-text));
  4986. padding: 4px;
  4987. border-radius: 4px;
  4988. border: 2px solid transparent;
  4989. outline: none !important;
  4990. }
  4991.  
  4992. .void-textarea:focus {
  4993. border: 2px solid rgb(var(--color-blue)) !important;
  4994. }
  4995.  
  4996. .void-layout-image-container {
  4997. padding: 4px;
  4998. display: inline-block;
  4999. }
  5000.  
  5001. .void-layout-image-container:first-child {
  5002. width: 35%;
  5003. }
  5004.  
  5005. .void-layout-image-container:last-child {
  5006. width: 65%;
  5007. }
  5008.  
  5009. .void-layout-header {
  5010. text-transform: uppercase;
  5011. margin-top: 2.2em;
  5012. margin-bottom: .8em;
  5013. }
  5014.  
  5015. .void-layout-image-display {
  5016. height: 140px;
  5017. background-repeat: no-repeat;
  5018. margin: auto;
  5019. margin-bottom: 6px;
  5020. border-radius: 4px;
  5021. }
  5022.  
  5023.  
  5024.  
  5025. .void-layout-image-display.void-banner {
  5026. width: 100%;
  5027. background-size: cover;
  5028. background-position: 50% 50%;
  5029. background-size:
  5030. }
  5031.  
  5032. .void-layout-image-display.void-avatar {
  5033. background-size: contain;
  5034. width: 140px;
  5035. }
  5036.  
  5037. .void-layout-image-container input {
  5038. width: 100%;
  5039. }
  5040.  
  5041. .void-layout-color-selection {
  5042. margin-top: 10px;
  5043. margin-bottom: 10px;
  5044. }
  5045.  
  5046. .void-layout-color-selection .void-color-button {
  5047. width: 50px;
  5048. height: 50px;
  5049. display: inline-flex;
  5050. border-radius: 4px;
  5051. margin-right: 10px;
  5052. }
  5053.  
  5054. .void-layout-color-selection .void-color-button.active {
  5055. border: 4px solid rgb(var(--color-text));
  5056. }
  5057.  
  5058. .void-layout-color-selection .void-color-picker-container.active {
  5059. border: 2px solid rgb(var(--color-text));
  5060. }
  5061.  
  5062. .void-color-picker-container {
  5063. display: inline-block;
  5064. vertical-align: top;
  5065. width: 75px;
  5066. height: 50px;
  5067. border: 2px solid transparent;
  5068. border-radius: 4px;
  5069. box-sizing: border-box;
  5070. }
  5071.  
  5072. .void-color-picker-container:has(:focus) {
  5073. border: 2px solid rgb(var(--color-text));
  5074. }
  5075.  
  5076. .void-color-picker-input {
  5077. width: 100%;
  5078. height: 20px;
  5079. background-color: rgba(var(--color-background), .6);
  5080. padding: 1px;
  5081. font-size: 11px;
  5082. color: rgb(var(--color-text));
  5083. outline: none;
  5084. appearance: none;
  5085. -webkit-appearance: none;
  5086. text-align: center;
  5087. border: unset;
  5088. border-radius: 0px 0px 4px 4px;
  5089. }
  5090.  
  5091. .void-color-picker {
  5092. /* width: 100%;;
  5093. height: 50px; */
  5094. block-size: 30px;
  5095. border-width: 0px;
  5096. padding: 0px;
  5097. background-color: unset;
  5098. inline-size: 100%;
  5099. border-radius: 4px;
  5100. appearance: none;
  5101. vertical-align: top;
  5102. padding-block: 0px;
  5103. padding-inline: 0px;
  5104. outline: none;
  5105. }
  5106.  
  5107. .void-color-picker::-webkit-color-swatch,
  5108. .void-color-picker::-moz-color-swatch {
  5109. border: none;
  5110. border-radius: 4px;
  5111. }
  5112.  
  5113. .void-color-picker::-webkit-color-swatch-wrapper,
  5114. .void-color-picker::-webkit-color-swatch-wrapper {
  5115. padding: 0px;
  5116. border-radius: 4px;
  5117. }
  5118.  
  5119. .void-input {
  5120. background-color: rgba(var(--color-background), .6);
  5121. padding: 4px 6px;
  5122. color: rgb(var(--color-text));
  5123. outline: none;
  5124. appearance: none;
  5125. -webkit-appearance: none;
  5126. border: 2px solid transparent;
  5127. border-radius: 4px;
  5128. box-sizing: border-box;
  5129. }
  5130.  
  5131. a.void-link {
  5132. color: rgb(var(--color-blue)) !important;
  5133. }
  5134.  
  5135. .void-input.void-sign {
  5136. width: 75px;
  5137. text-align: center;
  5138. height: 20px;
  5139. font-size: 14px;
  5140. }
  5141.  
  5142. .void-input:focus {
  5143. border: 2px solid rgb(var(--color-blue));
  5144. }
  5145.  
  5146. .void-button {
  5147. align-items: center;
  5148. background: rgb(var(--color-blue));
  5149. border-radius: 4px;
  5150. color: rgb(var(--color-text-bright));
  5151. cursor: pointer;
  5152. display: inline-flex;
  5153. font-size: 1.3rem;
  5154. padding: 10px 15px;
  5155. outline: none;
  5156. appearance: none;
  5157. -webkit-appearance: none;
  5158. border: 0px solid rgb(var(--color-background));
  5159. vertical-align: top;
  5160. margin-top: 15px;
  5161. margin-right: 10px;
  5162. }
  5163.  
  5164. .void-icon-button {
  5165. display: inline-block;
  5166. cursor: pointer;
  5167. margin-left: 4px;
  5168. margin-right: 4px;
  5169. vertical-align: middle;
  5170. }
  5171.  
  5172. .void-icon-button svg {
  5173. height: 12px;
  5174. vertical-align: middle;
  5175. display: inline-block;
  5176. }
  5177. .void-quick-access .void-quick-access-wrap {
  5178. background: rgb(var(--color-foreground));
  5179. display: grid;
  5180. grid-template-columns: repeat(auto-fill, 60px);
  5181. grid-template-rows: repeat(auto-fill, 80px);
  5182. gap: 15px;
  5183. padding: 15px;
  5184. margin-bottom: 25px;
  5185. }
  5186. .void-quick-access .section-header {
  5187. display: flex;
  5188. justify-content: space-between;
  5189. }
  5190.  
  5191. .void-quick-access-timer {
  5192. font-size: 12px;
  5193. color: rgb(var(--color-text));
  5194. }
  5195.  
  5196. .void-quick-access-item {
  5197. display: inline-block;
  5198. }
  5199.  
  5200. .void-quick-access-pfp {
  5201. background-size: contain;
  5202. background-repeat: no-repeat;
  5203. height: 60px;
  5204. width: 60px;
  5205. border-radius: 4px;
  5206. }
  5207.  
  5208. .void-quick-access-username {
  5209. display: inline-block;
  5210. text-align: center;
  5211. bottom: -20px;
  5212. width: 100%;
  5213. word-break: break-all;
  5214. font-size: 1.2rem;
  5215. }
  5216.  
  5217. .void-quick-access-badge {
  5218. position: relative;
  5219. }
  5220.  
  5221. .void-quick-access-badge::after {
  5222. content: "New";
  5223. background: rgb(var(--color-blue));
  5224. border-radius: 10px;
  5225. padding: 2px 4px;
  5226. font-size: 9px;
  5227. position: absolute;
  5228. top: 2px;
  5229. right: -10px;
  5230. color: white;
  5231. }
  5232.  
  5233. .void-notice {
  5234. font-size: 11px;
  5235. margin-top: 5px;
  5236. }
  5237.  
  5238. .void-select {
  5239. display: inline-flex;
  5240. }
  5241.  
  5242. .void-select .void-option {
  5243. padding: 3px 8px;
  5244. background: rgb(var(--color-foreground-blue));
  5245. font-size: 12px;
  5246. cursor: pointer;
  5247. }
  5248.  
  5249. .void-select .void-option:first-child {
  5250. border-radius: 4px 0px 0px 4px;
  5251. }
  5252.  
  5253. .void-select .void-option:last-child {
  5254. border-radius: 0px 4px 4px 0px;
  5255. }
  5256.  
  5257. .void-select .void-option.active {
  5258. background: rgb(var(--color-blue));
  5259. color: rgb(var(--color-text-bright));
  5260. }
  5261.  
  5262. .void-label-container {
  5263. margin-top: 6px;
  5264. margin-bottom: 6px;
  5265. }
  5266.  
  5267. .void-label-span {
  5268. margin-right: 10px;
  5269. min-width: 200px;
  5270. display: inline-block;
  5271. }
  5272.  
  5273. .void-upload-in-progress {
  5274. cursor: wait !important;
  5275. }
  5276.  
  5277. #void-toast-container {
  5278. position: fixed;
  5279. display: flex;
  5280. flex-direction: column;
  5281. gap: 10px;
  5282. }
  5283.  
  5284. #void-toast-container.void-bottom-left {
  5285. bottom: 10px;
  5286. left: 10px;
  5287. flex-direction: column-reverse;
  5288. }
  5289.  
  5290. #void-toast-container.void-bottom-right {
  5291. bottom: 10px;
  5292. right: 10px;
  5293. flex-direction: column-reverse;
  5294. }
  5295.  
  5296. #void-toast-container.void-top-left {
  5297. top: 70px;
  5298. left: 10px;
  5299. }
  5300.  
  5301. #void-toast-container.void-top-right {
  5302. top: 70px;
  5303. right: 10px;
  5304. }
  5305.  
  5306. .void-toast {
  5307. font-size: 14px;
  5308. color: rgb(var(--color-text-bright));
  5309. min-width: 150px;
  5310. max-width: 300px;
  5311. min-heigth: 50px;
  5312. padding: 10px 8px;
  5313. border-radius: 4px;
  5314. }
  5315.  
  5316. .void-info {
  5317. background: rgb(var(--void-info));
  5318. }
  5319.  
  5320. .void-success {
  5321. background: rgb(var(--void-success));
  5322. }
  5323.  
  5324. .void-error {
  5325. background: rgb(var(--void-error));
  5326. }
  5327.  
  5328. .void-warning {
  5329. background: rgb(var(--void-warning));
  5330. }
  5331. .
  5332. `;
  5333.  
  5334. const settings = new Settings();
  5335. Toaster.initializeToaster(settings);
  5336. const styleHandler = new StyleHandler(settings);
  5337. styleHandler.refreshStyles();
  5338.  
  5339. try {
  5340. const intervalScriptHandler = new IntervalScriptHandler(settings);
  5341. intervalScriptHandler.enableScriptIntervalHandling();
  5342. } catch (error) {
  5343. Toaster.critical(
  5344. "A critical error has occured setting up intervalScriptHandler. Please check developer console and contact voidnyan."
  5345. );
  5346. console.error(error);
  5347. }
  5348.  
  5349. try {
  5350. const pasteHandler = new PasteHandler(settings);
  5351. pasteHandler.setup();
  5352. } catch (error) {
  5353. Toaster.critical(
  5354. "A critical error has occured setting up pasteHandler. Please check developer console and contact voidnyan."
  5355. );
  5356. }
  5357.  
  5358. styleHandler.createStyleLink(styles, "script");
  5359.  
  5360. new ImgurAPI(
  5361. new ImageHostService().getImageHostConfiguration(imageHosts.imgur)
  5362. ).refreshAuthToken();
  5363.  
  5364. console.log(`VoidVerified ${settings.version} loaded.`);
  5365. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址