VoidVerified

Display a verified sign next to user's name in AniList.

当前为 2023-11-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          VoidVerified
// @version       1.2.0
// @namespace     http://tampermonkey.net/
// @author        voidnyan
// @description   Display a verified sign next to user's name in AniList.
// @homepageURL   https://github.com/voidnyan/void-verified#voidverified
// @supportURL    https://github.com/voidnyan/void-verified/issues
// @grant         none
// @match         https://anilist.co/*
// @license MIT
// ==/UserScript==

(function () {
	'use strict';

	const defaultSettings = {
		copyColorFromProfile: {
			defaultValue: true,
			description: "Copy user color from their profile.",
		},
		moveSubscribeButtons: {
			defaultValue: false,
			description:
				"Move activity subscribe button next to comments and likes.",
		},
		hideLikeCount: {
			defaultValue: false,
			description: "Hide activity and reply like counts.",
		},
		enabledForUsername: {
			defaultValue: true,
			description: "Display a verified sign next to usernames.",
		},
		enabledForProfileName: {
			defaultValue: false,
			description: "Display a verified sign next to a profile name.",
		},
		defaultSign: {
			defaultValue: "✔",
			description: "The default sign displayed next to a username.",
		},
		highlightEnabled: {
			defaultValue: true,
			description: "Highlight user activity with a border.",
		},
		highlightEnabledForReplies: {
			defaultValue: true,
			description: "Highlight replies with a border.",
		},
		highlightSize: {
			defaultValue: "5px",
			description: "Width of the highlight border.",
		},
		useDefaultHighlightColor: {
			defaultValue: false,
			description:
				"Use fallback highlight color when user color is not specified.",
		},
		defaultHighlightColor: {
			defaultValue: "#FFFFFF",
			description: "Fallback highlight color.",
		},
		globalCssEnabled: {
			defaultValue: false,
			description: "Enable custom global CSS.",
		},
		globalCssAutoDisable: {
			defaultValue: true,
			description: "Disable global CSS when a profile has custom CSS.",
		},
		quickAccessEnabled: {
			defaultValue: false,
			description: "Display quick access of users in home page.",
		},
	};

	class ColorFunctions {
		static hexToRgb(hex) {
			const r = parseInt(hex.slice(1, 3), 16);
			const g = parseInt(hex.slice(3, 5), 16);
			const b = parseInt(hex.slice(5, 7), 16);

			return `${r}, ${g}, ${b}`;
		}

		static rgbToHex(rgb) {
			const [r, g, b] = rgb.split(",");
			const hex = this.generateHex(r, g, b);
			return hex;
		}

		static generateHex(r, g, b) {
			return (
				"#" +
				[r, g, b]
					.map((x) => {
						const hex = Number(x).toString(16);
						return hex.length === 1 ? "0" + hex : hex;
					})
					.join("")
			);
		}
	}

	class AnilistAPI {
		apiQueryTimeoutInMinutes = 30;
		apiQueryTimeout = this.apiQueryTimeoutInMinutes * 60 * 1000;

		settings;
		constructor(settings) {
			this.settings = settings;
		}

		queryUserData() {
			this.#createUserQuery();
		}

		async #createUserQuery() {
			let stopQueries = false;

			for (const user of this.#getUsersToQuery()) {
				if (stopQueries) {
					break;
				}

				stopQueries = this.#queryUser(user);
			}
		}

		#userQuery = `
        query ($username: String) {
            User(name: $username) {
                name
                avatar {
                    large
                }
                options {
                    profileColor
                }
            }
        }
    `;

		#queryUser(user) {
			const variables = {
				username: user.username,
			};

			const url = "https://graphql.anilist.co";
			const options = {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					Accept: "application/json",
				},
				body: JSON.stringify({
					query: this.#userQuery,
					variables,
				}),
			};

			let stopQueries = false;

			fetch(url, options)
				.then(this.#handleResponse)
				.then((data) => {
					const resultUser = data.User;
					this.settings.updateUserFromApi(user, resultUser);
				})
				.catch((err) => {
					console.error(err);
					stopQueries = true;
				});

			return stopQueries;
		}

		#getUsersToQuery() {
			if (
				this.settings.getOptionValue(
					this.settings.options.copyColorFromProfile
				) ||
				this.settings.getOptionValue(
					this.settings.options.quickAccessEnabled
				)
			) {
				return this.#filterUsersByLastFetch();
			}

			const users = this.settings.verifiedUsers.filter(
				(user) => user.copyColorFromProfile || user.quickAccessEnabled
			);

			return this.#filterUsersByLastFetch(users);
		}

		#handleResponse(response) {
			return response.json().then((json) => {
				return response.ok ? json.data : Promise.reject(json);
			});
		}

		#filterUsersByLastFetch(users = null) {
			const currentDate = new Date();
			if (users) {
				return users.filter(
					(user) =>
						!user.lastFetch ||
						currentDate - new Date(user.lastFetch) >
							this.apiQueryTimeout
				);
			}
			return this.settings.verifiedUsers.filter(
				(user) =>
					!user.lastFetch ||
					currentDate - new Date(user.lastFetch) > this.apiQueryTimeout
			);
		}
	}

	class Settings {
		localStorageUsers = "void-verified-users";
		localStorageSettings = "void-verified-settings";
		version = "1.2.0";

		verifiedUsers = [];

		options = defaultSettings;

		constructor() {
			this.verifiedUsers =
				JSON.parse(localStorage.getItem(this.localStorageUsers)) ?? [];

			const settingsInLocalStorage =
				JSON.parse(localStorage.getItem(this.localStorageSettings)) ?? {};

			for (const [key, value] of Object.entries(settingsInLocalStorage)) {
				if (!this.options[key]) {
					continue;
				}
				this.options[key].value = value.value;
			}
		}

		getOptionValue(object) {
			if (object.value === "") {
				return object.defaultValue;
			}
			return object.value ?? object.defaultValue;
		}

		verifyUser(username) {
			if (
				this.verifiedUsers.find(
					(user) => user.username.toLowerCase() === username.toLowerCase()
				)
			) {
				return;
			}

			this.verifiedUsers.push({ username });
			localStorage.setItem(
				this.localStorageUsers,
				JSON.stringify(this.verifiedUsers)
			);

			const anilistAPI = new AnilistAPI(this);
			anilistAPI.queryUserData();
		}

		updateUserOption(username, key, value) {
			this.verifiedUsers = this.verifiedUsers.map((u) =>
				u.username === username
					? {
							...u,
							[key]: value,
					  }
					: u
			);
			localStorage.setItem(
				this.localStorageUsers,
				JSON.stringify(this.verifiedUsers)
			);
		}

		updateUserFromApi(user, apiUser) {
			const newUser = this.#mapApiUser(user, apiUser);
			this.verifiedUsers = this.verifiedUsers.map((u) =>
				u.username.toLowerCase() === user.username.toLowerCase()
					? newUser
					: u
			);

			localStorage.setItem(
				this.localStorageUsers,
				JSON.stringify(this.verifiedUsers)
			);
		}

		#mapApiUser(user, apiUser) {
			let userObject = { ...user };

			userObject.color = this.#handleAnilistColor(
				apiUser.options.profileColor
			);

			userObject.username = apiUser.name;
			userObject.avatar = apiUser.avatar.large;
			userObject.lastFetch = new Date();

			return userObject;
		}

		removeUser(username) {
			this.verifiedUsers = this.verifiedUsers.filter(
				(user) => user.username !== username
			);
			localStorage.setItem(
				this.localStorageUsers,
				JSON.stringify(this.verifiedUsers)
			);
		}

		saveSettingToLocalStorage(key, value) {
			let localSettings = JSON.parse(
				localStorage.getItem(this.localStorageSettings)
			);

			this.options[key].value = value;

			if (localSettings === null) {
				const settings = {
					[key]: value,
				};
				localStorage.setItem(
					this.localStorageSettings,
					JSON.stringify(settings)
				);
				return;
			}

			localSettings[key] = { value };
			localStorage.setItem(
				this.localStorageSettings,
				JSON.stringify(localSettings)
			);
		}

		#defaultColors = [
			"gray",
			"blue",
			"purple",
			"green",
			"orange",
			"red",
			"pink",
		];

		#defaultColorRgb = {
			gray: "103, 123, 148",
			blue: "61, 180, 242",
			purple: "192, 99, 255",
			green: "76, 202, 81",
			orange: "239, 136, 26",
			red: "225, 51, 51",
			pink: "252, 157, 214",
		};

		#handleAnilistColor(color) {
			if (this.#defaultColors.includes(color)) {
				return this.#defaultColorRgb[color];
			}

			return ColorFunctions.hexToRgb(color);
		}
	}

	class StyleHandler {
		settings;
		usernameStyles = "";
		highlightStyles = "";
		otherStyles = "";

		profileLink = this.createStyleLink("", "profile");

		constructor(settings) {
			this.settings = settings;
		}

		refreshStyles() {
			this.createStyles();
			this.createStyleLink(this.usernameStyles, "username");
			this.createStyleLink(this.highlightStyles, "highlight");
			this.createStyleLink(this.otherStyles, "other");
		}

		createStyles() {
			this.usernameStyles = "";
			this.otherStyles = `a[href="/settings/developer" i]::after{content: " & Void"}`;

			for (const user of this.settings.verifiedUsers) {
				if (
					this.settings.getOptionValue(
						this.settings.options.enabledForUsername
					) ||
					user.enabledForUsername
				) {
					this.createUsernameCSS(user);
				}
			}

			if (
				this.settings.getOptionValue(
					this.settings.options.moveSubscribeButtons
				)
			) {
				this.otherStyles += `
                .has-label::before {
                top: -30px !important;
                left: unset !important;
                right: -10px;
                }
    
                .has-label[label="Unsubscribe"],
                .has-label[label="Subscribe"] {
                font-size: 0.875em !important;
                }
    
                .has-label[label="Unsubscribe"] {
                color: rgba(var(--color-green),.8);
                }
                `;
			}

			this.createHighlightStyles();

			if (this.settings.getOptionValue(this.settings.options.hideLikeCount)) {
				this.otherStyles += `
                    .like-wrap .count {
                        display: none;
                    }
                `;
			}
		}

		createHighlightStyles() {
			this.highlightStyles = "";
			for (const user of this.settings.verifiedUsers) {
				if (
					this.settings.getOptionValue(
						this.settings.options.highlightEnabled
					) ||
					user.highlightEnabled
				) {
					this.createHighlightCSS(
						user,
						`div.wrap:has( div.header > a.name[href*="${user.username}" i] )`
					);
					this.createHighlightCSS(
						user,
						`div.wrap:has( div.details > a.name[href*="${user.username}" i] )`
					);
				}

				if (
					this.settings.getOptionValue(
						this.settings.options.highlightEnabledForReplies
					) ||
					user.highlightEnabledForReplies
				) {
					this.createHighlightCSS(
						user,
						`div.reply:has( a.name[href*="${user.username}" i] )`
					);
				}
			}

			this.disableHighlightOnSmallCards();
		}

		createUsernameCSS(user) {
			this.usernameStyles += `
                a.name[href*="${user.username}" i]::after {
                    content: "${
						this.stringIsEmpty(user.sign) ??
						this.settings.getOptionValue(
							this.settings.options.defaultSign
						)
					}";
                    color: ${
						this.getUserColor(user) ?? "rgb(var(--color-blue))"
					}
                }
                `;
		}

		createHighlightCSS(user, selector) {
			this.highlightStyles += `
                ${selector} {
                    margin-right: -${this.settings.getOptionValue(
						this.settings.options.highlightSize
					)};
                    border-right: ${this.settings.getOptionValue(
						this.settings.options.highlightSize
					)} solid ${
			this.getUserColor(user) ?? this.getDefaultHighlightColor()
		};
                    border-radius: 5px;
                }
                `;
		}

		disableHighlightOnSmallCards() {
			this.highlightStyles += `
                div.wrap:has(div.small) {
                margin-right: 0px !important;
                border-right: 0px solid black !important;
                }
                `;
		}

		refreshHomePage() {
			if (
				!this.settings.getOptionValue(
					this.settings.options.highlightEnabled
				)
			) {
				return;
			}
			this.createHighlightStyles();
			this.createStyleLink(this.highlightStyles, "highlight");
		}

		clearProfileVerify() {
			this.profileLink.href =
				"data:text/css;charset=UTF-8," + encodeURIComponent("");
		}

		clearStyles(id) {
			const styles = document.getElementById(`void-verified-${id}-styles`);
			styles?.remove();
		}

		verifyProfile() {
			if (
				!this.settings.getOptionValue(
					this.settings.options.enabledForProfileName
				)
			) {
				return;
			}

			const usernameHeader = document.querySelector("h1.name");
			const username = usernameHeader.innerHTML.trim();

			const user = this.settings.verifiedUsers.find(
				(u) => u.username === username
			);
			if (!user) {
				this.clearProfileVerify();
				return;
			}

			const profileStyle = `
                    h1.name::after {
                    content: "${
						this.stringIsEmpty(user.sign) ??
						this.settings.getOptionValue(
							this.settings.options.defaultSign
						)
					}"
                    }
                `;
			this.profileLink.href =
				"data:text/css;charset=UTF-8," + encodeURIComponent(profileStyle);
		}

		copyUserColor() {
			const usernameHeader = document.querySelector("h1.name");
			const username = usernameHeader.innerHTML.trim();
			const user = this.settings.verifiedUsers.find(
				(u) => u.username === username
			);

			if (!user) {
				return;
			}

			if (
				!(
					user.copyColorFromProfile ||
					this.settings.getOptionValue(
						this.settings.options.copyColorFromProfile
					)
				)
			) {
				return;
			}

			const color =
				getComputedStyle(usernameHeader).getPropertyValue("--color-blue");

			this.settings.updateUserOption(user.username, "color", color);
		}

		getUserColor(user) {
			return (
				user.colorOverride ??
				(user.color &&
				(user.copyColorFromProfile ||
					this.settings.getOptionValue(
						this.settings.options.copyColorFromProfile
					))
					? `rgb(${user.color})`
					: undefined)
			);
		}

		getDefaultHighlightColor() {
			if (
				this.settings.getOptionValue(
					this.settings.options.useDefaultHighlightColor
				)
			) {
				return this.settings.getOptionValue(
					this.settings.options.defaultHighlightColor
				);
			}
			return "rgb(var(--color-blue))";
		}

		createStyleLink(styles, id) {
			const oldLink = document.getElementById(`void-verified-${id}-styles`);
			const link = document.createElement("link");
			link.setAttribute("id", `void-verified-${id}-styles`);
			link.setAttribute("rel", "stylesheet");
			link.setAttribute("type", "text/css");
			link.setAttribute(
				"href",
				"data:text/css;charset=UTF-8," + encodeURIComponent(styles)
			);
			document.head?.append(link);
			oldLink?.remove();
			return link;
		}

		stringIsEmpty(string) {
			if (!string || string.length === 0) {
				return undefined;
			}
			return string;
		}
	}

	class GlobalCSS {
		settings;
		styleHandler;

		styleId = "global-css";
		isCleared = false;

		cssInLocalStorage = "void-verified-global-css";
		constructor(settings) {
			this.settings = settings;
			this.styleHandler = new StyleHandler(settings);

			this.css = localStorage.getItem(this.cssInLocalStorage) ?? "";
		}

		createCss() {
			if (
				!this.settings.getOptionValue(
					this.settings.options.globalCssEnabled
				)
			) {
				this.styleHandler.clearStyles(this.styleId);
				return;
			}

			if (!this.shouldRender()) {
				return;
			}

			this.isCleared = false;
			this.styleHandler.createStyleLink(this.css, this.styleId);
		}

		updateCss(css) {
			this.css = css;
			this.createCss();
			localStorage.setItem(this.cssInLocalStorage, css);
		}

		clearCssForProfile() {
			if (this.isCleared) {
				return;
			}
			if (!this.shouldRender()) {
				this.styleHandler.clearStyles(this.styleId);
				this.isCleared = true;
			}
		}

		shouldRender() {
			if (window.location.pathname.startsWith("/settings")) {
				return false;
			}

			if (
				!this.settings.getOptionValue(
					this.settings.options.globalCssAutoDisable
				)
			) {
				return true;
			}

			const profileCustomCss = document.getElementById(
				"customCSS-automail-styles"
			);

			if (!profileCustomCss) {
				return true;
			}

			const shouldRender = profileCustomCss.innerHTML.trim().length === 0;
			return shouldRender;
		}
	}

	class ActivityHandler {
		settings;
		constructor(settings) {
			this.settings = settings;
		}

		moveAndDisplaySubscribeButton() {
			if (
				!this.settings.getOptionValue(
					this.settings.options.moveSubscribeButtons
				)
			) {
				return;
			}

			const subscribeButtons = document.querySelectorAll(
				"span[label='Unsubscribe'], span[label='Subscribe']"
			);
			for (const subscribeButton of subscribeButtons) {
				if (subscribeButton.parentNode.classList.contains("actions")) {
					continue;
				}

				const container = subscribeButton.parentNode.parentNode;
				const actions = container.querySelector(".actions");
				actions.append(subscribeButton);
			}
		}
	}

	class SettingsUserInterface {
		settings;
		styleHandler;
		globalCSS;
		AnilistBlue = "120, 180, 255";

		constructor(settings, styleHandler, globalCSS) {
			this.settings = settings;
			this.styleHandler = styleHandler;
			this.globalCSS = globalCSS;
		}

		renderSettingsUi() {
			const container = document.querySelector(
				".settings.container > .content"
			);
			const settingsContainer = document.createElement("div");
			settingsContainer.setAttribute("id", "voidverified-settings");
			this.#renderSettingsHeader(settingsContainer);

			const settingsListContainer = document.createElement("div");
			settingsListContainer.style.display = "flex";
			settingsListContainer.style.flexDirection = "column";
			settingsListContainer.style.gap = "5px";
			for (const [key, setting] of Object.entries(this.settings.options)) {
				this.#renderSetting(setting, settingsListContainer, key);
			}

			settingsContainer.append(settingsListContainer);

			this.#renderUserTable(settingsContainer);

			this.#renderCustomCssEditor(settingsContainer);

			container.append(settingsContainer);
		}

		removeSettingsUi() {
			const settings = document.querySelector("#voidverified-settings");
			settings?.remove();
		}

		#renderSettingsHeader(settingsContainer) {
			const headerContainer = document.createElement("div");
			const header = document.createElement("h1");
			header.style.marginTop = "30px";
			header.innerText = "VoidVerified settings";

			const versionInfo = document.createElement("p");
			versionInfo.append("version: ");
			const versionNumber = document.createElement("span");
			versionNumber.style.color = `rgb(${this.AnilistBlue})`;
			versionNumber.append(this.settings.version);

			versionInfo.append(versionNumber);

			headerContainer.append(header);
			headerContainer.append(versionInfo);
			settingsContainer.append(headerContainer);
		}

		#renderUserTable(settingsContainer) {
			const oldTableContainer = document.querySelector(
				"#void-verified-user-table"
			);
			const tableContainer =
				oldTableContainer ?? document.createElement("div");
			tableContainer.innerHTML = "";

			tableContainer.setAttribute("id", "void-verified-user-table");

			const table = document.createElement("table");
			const head = document.createElement("thead");
			const headrow = document.createElement("tr");
			headrow.append(this.#createCell("Username", "th"));
			headrow.append(this.#createCell("Sign", "th"));
			headrow.append(this.#createCell("Color", "th"));
			headrow.append(this.#createCell("Other", "th"));

			head.append(headrow);

			const body = document.createElement("tbody");

			for (const user of this.settings.verifiedUsers) {
				body.append(this.#createUserRow(user));
			}

			table.append(head);
			table.append(body);
			tableContainer.append(table);

			const inputForm = document.createElement("form");
			inputForm.addEventListener("submit", (event) =>
				this.#handleVerifyUserForm(event, this.settings)
			);
			const label = document.createElement("label");
			label.innerText = "Add user";
			inputForm.append(label);
			const textInput = document.createElement("input");
			textInput.setAttribute("id", "voidverified-add-user");

			inputForm.append(textInput);
			tableContainer.append(inputForm);

			oldTableContainer || settingsContainer.append(tableContainer);
		}

		#createUserRow(user) {
			const row = document.createElement("tr");
			const userLink = document.createElement("a");
			userLink.innerText = user.username;
			userLink.setAttribute(
				"href",
				`https://anilist.co/user/${user.username}/`
			);
			userLink.setAttribute("target", "_blank");
			row.append(this.#createCell(userLink));

			const signInput = document.createElement("input");
			signInput.value = user.sign ?? "";
			signInput.style.width = "100px";
			signInput.addEventListener("input", (event) =>
				this.#updateUserOption(user.username, "sign", event.target.value)
			);
			const signCell = this.#createCell(signInput);
			signCell.append(
				this.#createUserCheckbox(
					user.enabledForUsername,
					user.username,
					"enabledForUsername",
					this.settings.getOptionValue(
						this.settings.options.enabledForUsername
					)
				)
			);

			row.append(this.#createCell(signCell));

			const colorInput = document.createElement("input");
			colorInput.setAttribute("type", "color");
			colorInput.style.border = "0";
			colorInput.style.height = "24px";
			colorInput.style.width = "40px";
			colorInput.style.padding = "0";
			colorInput.style.backgroundColor = "unset";
			colorInput.value = this.#getUserColorPickerColor(user);
			colorInput.addEventListener(
				"change",
				(event) => this.#handleUserColorChange(event, user.username),
				false
			);

			const colorInputContainer = document.createElement("span");

			const colorCell = this.#createCell(colorInput);

			colorInputContainer.append(
				this.#createUserCheckbox(
					user.copyColorFromProfile,
					user.username,
					"copyColorFromProfile",
					this.settings.getOptionValue(
						this.settings.options.copyColorFromProfile
					)
				)
			);

			colorInputContainer.append(
				this.#createUserCheckbox(
					user.highlightEnabled,
					user.username,
					"highlightEnabled",
					this.settings.getOptionValue(
						this.settings.options.highlightEnabled
					)
				)
			);

			colorInputContainer.append(
				this.#createUserCheckbox(
					user.highlightEnabledForReplies,
					user.username,
					"highlightEnabledForReplies",
					this.settings.getOptionValue(
						this.settings.options.highlightEnabledForReplies
					)
				)
			);

			colorCell.append(colorInputContainer);

			const resetColorBtn = document.createElement("button");
			resetColorBtn.innerText = "🔄";
			resetColorBtn.addEventListener("click", () =>
				this.#handleUserColorReset(user.username)
			);

			colorCell.append(resetColorBtn);
			row.append(colorCell);

			const quickAccessCheckbox = this.#createUserCheckbox(
				user.quickAccessEnabled,
				user.username,
				"quickAccessEnabled",
				this.settings.getOptionValue(
					this.settings.options.quickAccessEnabled
				)
			);
			row.append(this.#createCell(quickAccessCheckbox));

			const deleteButton = document.createElement("button");
			deleteButton.innerText = "❌";
			deleteButton.addEventListener("click", () =>
				this.#removeUser(user.username)
			);
			row.append(this.#createCell(deleteButton));
			return row;
		}

		#getUserColorPickerColor(user) {
			if (user.colorOverride) {
				return user.colorOverride;
			}

			if (
				user.color &&
				(user.copyColorFromProfile ||
					this.settings.getOptionValue(
						this.settings.options.copyColorFromProfile
					))
			) {
				return ColorFunctions.rgbToHex(user.color);
			}

			if (
				this.settings.getOptionValue(
					this.settings.options.useDefaultHighlightColor
				)
			) {
				return this.settings.getOptionValue(
					this.settings.options.defaultHighlightColor
				);
			}

			return ColorFunctions.rgbToHex(this.AnilistBlue);
		}

		#createUserCheckbox(isChecked, username, settingKey, disabled) {
			const checkbox = document.createElement("input");
			if (disabled) {
				checkbox.setAttribute("disabled", "");
			}

			checkbox.setAttribute("type", "checkbox");
			checkbox.checked = isChecked;
			checkbox.addEventListener("change", (event) => {
				this.#updateUserOption(username, settingKey, event.target.checked);
				this.#refreshUserTable();
			});

			checkbox.style.marginLeft = "5px";

			checkbox.title = this.settings.options[settingKey].description;
			return checkbox;
		}

		#handleUserColorReset(username) {
			this.#updateUserOption(username, "colorOverride", undefined);
			this.#refreshUserTable();
		}

		#handleUserColorChange(event, username) {
			const color = event.target.value;
			this.#updateUserOption(username, "colorOverride", color);
		}

		#handleVerifyUserForm(event, settings) {
			event.preventDefault();

			const usernameInput = document.getElementById("voidverified-add-user");
			const username = usernameInput.value;
			settings.verifyUser(username);
			usernameInput.value = "";
			this.#refreshUserTable();
		}

		#refreshUserTable() {
			const container = document.querySelector(
				".settings.container > .content"
			);
			this.#renderUserTable(container);
		}

		#updateUserOption(username, key, value) {
			this.settings.updateUserOption(username, key, value);
			this.styleHandler.refreshStyles();
		}

		#removeUser(username) {
			this.settings.removeUser(username);
			this.#refreshUserTable();
			this.styleHandler.refreshStyles();
		}

		#createCell(content, elementType = "td") {
			const cell = document.createElement(elementType);
			cell.append(content);
			return cell;
		}

		#renderSetting(setting, settingsContainer, settingKey, disabled = false) {
			const value = this.settings.getOptionValue(setting);
			const type = typeof value;

			const container = document.createElement("div");
			const input = document.createElement("input");

			if (type === "boolean") {
				input.setAttribute("type", "checkbox");
			} else if (settingKey == "defaultHighlightColor") {
				input.setAttribute("type", "color");
				input.style.border = "0";
				input.style.height = "15px";
				input.style.width = "25px";
				input.style.padding = "0";
				input.style.backgroundColor = "unset";
			} else if (type === "string") {
				input.setAttribute("type", "text");
				input.style.width = "50px";
			}

			if (disabled) {
				input.setAttribute("disabled", "");
			}

			input.setAttribute("id", settingKey);
			input.addEventListener("change", (event) =>
				this.#handleOption(event, settingKey, type)
			);

			if (type === "boolean" && value) {
				input.setAttribute("checked", true);
			} else if (type === "string") {
				input.value = value;
			}

			container.append(input);

			const label = document.createElement("label");
			label.setAttribute("for", settingKey);
			label.innerText = setting.description;
			label.style.marginLeft = "5px";
			container.append(label);
			settingsContainer.append(container);
		}

		#handleOption(event, settingKey, type) {
			const value =
				type === "boolean" ? event.target.checked : event.target.value;
			this.settings.saveSettingToLocalStorage(settingKey, value);
			this.styleHandler.refreshStyles();
			this.#refreshUserTable();
		}

		#renderCustomCssEditor(settingsContainer) {
			const container = document.createElement("div");
			const label = document.createElement("label");
			label.innerText = "Custom Global CSS";
			label.setAttribute("for", "void-verified-global-css-editor");
			label.style.marginTop = "20px";
			label.style.fontSize = "2rem";
			label.style.display = "inline-block";
			container.append(label);

			const textarea = document.createElement("textarea");
			textarea.setAttribute("id", "void-verified-global-css-editor");

			textarea.value = this.globalCSS.css;
			textarea.style.width = "100%";
			textarea.style.height = "200px";
			textarea.style.resize = "vertical";
			textarea.style.background = "#14191f";
			textarea.style.color = "white";

			textarea.addEventListener("change", (event) => {
				this.#handleCustomCssEditor(event, this);
			});

			container.append(textarea);

			const notice = document.createElement("div");
			notice.innerText =
				"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";
			notice.style.fontSize = "11px";
			container.append(notice);

			settingsContainer.append(container);
		}

		#handleCustomCssEditor(event, settingsUi) {
			const value = event.target.value;
			settingsUi.globalCSS.updateCss(value);
		}
	}

	class QuickAccess {
		settings;
		#quickAccessId = "void-verified-quick-access";
		constructor(settings) {
			this.settings = settings;
		}

		renderQuickAccess() {
			if (this.#quickAccessRendered()) {
				return;
			}

			if (
				!this.settings.getOptionValue(
					this.settings.options.quickAccessEnabled
				) &&
				!this.settings.verifiedUsers.some((user) => user.quickAccessEnabled)
			) {
				return;
			}

			const quickAccessContainer = document.createElement("div");
			quickAccessContainer.setAttribute("id", this.#quickAccessId);

			const sectionHeader = document.createElement("div");
			sectionHeader.setAttribute("class", "section-header");
			const title = document.createElement("h2");
			title.append("Quick Access");
			sectionHeader.append(title);

			quickAccessContainer.append(sectionHeader);

			const quickAccessBody = document.createElement("div");
			quickAccessBody.style = `
            background: rgb(var(--color-foreground));
            display: grid;
            grid-template-columns: repeat(auto-fill, 60px);
            grid-template-rows: repeat(auto-fill, 80px);
            gap: 15px;
            padding: 20px;
            margin-bottom: 25px;
        `;

			for (const user of this.#getQuickAccessUsers()) {
				quickAccessBody.append(this.#createQuickAccessLink(user));
			}

			quickAccessContainer.append(quickAccessBody);

			const section = document.querySelector(
				".container > .home > div:nth-child(2)"
			);
			section.insertBefore(quickAccessContainer, section.firstChild);
		}

		#createQuickAccessLink(user) {
			const container = document.createElement("a");
			container.style.display = "inline-block";
			const link = document.createElement("a");
			container.setAttribute(
				"href",
				`https://anilist.co/user/${user.username}/`
			);

			const image = document.createElement("div");
			image.style = `
            background-image: url(${user.avatar});
            display: flex;
            background-size: contain;
            background-repeat: no-repeat;
            height: 60px;
            width: 60px;
        `;

			container.append(image);

			const username = document.createElement("div");
			username.append(user.username);

			username.style = `
            display: inline-block;
            text-align: center;
            bottom: -20px;
            width: 100%;
            word-break: break-all;
            font-size: 1.2rem;
        `;

			container.append(username);

			container.append(link);
			return container;
		}

		#quickAccessRendered() {
			const quickAccess = document.getElementById(this.#quickAccessId);
			return quickAccess !== null;
		}

		#getQuickAccessUsers() {
			if (
				this.settings.getOptionValue(
					this.settings.options.quickAccessEnabled
				)
			) {
				return this.settings.verifiedUsers;
			}

			return this.settings.verifiedUsers.filter(
				(user) => user.quickAccessEnabled
			);
		}
	}

	class IntervalScriptHandler {
		styleHandler;
		settingsUi;
		activityHandler;
		settings;
		globalCSS;
		quickAccess;
		constructor(settings) {
			this.settings = settings;

			this.styleHandler = new StyleHandler(settings);
			this.globalCSS = new GlobalCSS(settings);
			this.settingsUi = new SettingsUserInterface(
				settings,
				this.styleHandler,
				this.globalCSS
			);
			this.activityHandler = new ActivityHandler(settings);
			this.quickAccess = new QuickAccess(settings);
		}

		currentPath = "";
		evaluationIntervalInSeconds = 1;
		hasPathChanged(path) {
			if (path === this.currentPath) {
				return false;
			}
			this.currentPath = path;
			return true;
		}

		handleIntervalScripts(intervalScriptHandler) {
			const path = window.location.pathname;

			intervalScriptHandler.activityHandler.moveAndDisplaySubscribeButton();
			intervalScriptHandler.globalCSS.clearCssForProfile();

			if (path === "/home") {
				intervalScriptHandler.styleHandler.refreshHomePage();
				intervalScriptHandler.quickAccess.renderQuickAccess();
			}

			if (!path.startsWith("/settings/developer")) {
				intervalScriptHandler.settingsUi.removeSettingsUi();
			}

			if (!intervalScriptHandler.hasPathChanged(path)) {
				return;
			}

			intervalScriptHandler.styleHandler.clearProfileVerify();
			intervalScriptHandler.globalCSS.createCss();

			if (path.startsWith("/user/")) {
				intervalScriptHandler.styleHandler.verifyProfile();
				intervalScriptHandler.styleHandler.copyUserColor();
				return;
			}

			if (path.startsWith("/settings/developer")) {
				intervalScriptHandler.settingsUi.renderSettingsUi();
				return;
			}
		}

		enableScriptIntervalHandling() {
			setInterval(() => {
				this.handleIntervalScripts(this);
			}, this.evaluationIntervalInSeconds * 1000);
		}
	}

	const settings = new Settings();
	const styleHandler = new StyleHandler(settings);
	const intervalScriptHandler = new IntervalScriptHandler(settings);

	styleHandler.refreshStyles();
	intervalScriptHandler.enableScriptIntervalHandling();

	const anilistAPI = new AnilistAPI(settings);

	anilistAPI.queryUserData();

	console.log(`VoidVerified ${settings.version} loaded.`);

})();