Greasy Fork

VoidVerified

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

目前为 2023-11-20 提交的版本。查看 最新版本

// ==UserScript==
// @name          VoidVerified
// @version       1.3.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 categories = {
		users: "users",
		paste: "paste",
		misc: "misc",
	};

	const defaultSettings = {
		copyColorFromProfile: {
			defaultValue: true,
			description: "Copy user color from their profile.",
			category: categories.users,
		},
		moveSubscribeButtons: {
			defaultValue: false,
			description:
				"Move activity subscribe button next to comments and likes.",
			category: categories.misc,
		},
		hideLikeCount: {
			defaultValue: false,
			description: "Hide activity and reply like counts.",
			category: categories.misc,
		},
		enabledForUsername: {
			defaultValue: true,
			description: "Display a verified sign next to usernames.",
			category: categories.users,
		},
		enabledForProfileName: {
			defaultValue: false,
			description: "Display a verified sign next to a profile name.",
			category: categories.users,
		},
		defaultSign: {
			defaultValue: "✔",
			description: "The default sign displayed next to a username.",
			category: categories.users,
		},
		highlightEnabled: {
			defaultValue: true,
			description: "Highlight user activity with a border.",
			category: categories.users,
		},
		highlightEnabledForReplies: {
			defaultValue: true,
			description: "Highlight replies with a border.",
			category: categories.users,
		},
		highlightSize: {
			defaultValue: "5px",
			description: "Width of the highlight border.",
			category: categories.users,
		},
		colorUserActivity: {
			defaultValue: false,
			description: "Color user activity links with user color.",
			category: categories.users,
		},
		colorUserReplies: {
			defaultValue: false,
			description: "Color user reply links with user color.",
			category: categories.users,
		},
		useDefaultHighlightColor: {
			defaultValue: false,
			description:
				"Use fallback highlight color when user color is not specified.",
			category: categories.users,
		},
		defaultHighlightColor: {
			defaultValue: "#FFFFFF",
			description: "Fallback highlight color.",
			category: categories.users,
		},
		globalCssEnabled: {
			defaultValue: false,
			description: "Enable custom global CSS.",
			category: categories.misc,
		},
		globalCssAutoDisable: {
			defaultValue: true,
			description: "Disable global CSS when a profile has custom CSS.",
			category: categories.misc,
		},
		quickAccessEnabled: {
			defaultValue: false,
			description: "Display quick access of users in home page.",
			category: categories.users,
		},
		pasteEnabled: {
			defaultValue: false,
			description:
				"Automatically wrap pasted links and images with link and image tags.",
			category: categories.paste,
		},
		pasteWrapImagesWithLink: {
			defaultValue: false,
			description: "Wrap images with a link tag.",
			category: categories.paste,
		},
		pasteRequireKeyPress: {
			defaultValue: true,
			description: "Require an additional key to be pressed while pasting.",
			category: categories.paste,
		},
		pasteKeybind: {
			defaultValue: "Shift",
			description: "The key to be pressed while pasting.",
			category: categories.paste,
		},
		pasteImageWidth: {
			defaultValue: "420",
			description: "Width used when pasting images.",
			category: categories.paste,
		},
	};

	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.options.copyColorFromProfile.getValue() ||
				this.settings.options.quickAccessEnabled.getValue()
			) {
				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 Option {
		value;
		defaultValue;
		description;
		category;
		constructor(option) {
			this.defaultValue = option.defaultValue;
			this.description = option.description;
			this.category = option.category;
		}

		getValue() {
			if (this.value === "") {
				return this.defaultValue;
			}
			return this.value ?? this.defaultValue;
		}
	}

	class Settings {
		localStorageUsers = "void-verified-users";
		localStorageSettings = "void-verified-settings";
		version = GM_info.script.version;

		verifiedUsers = [];

		options = {};

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

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

			for (const [key, value] of Object.entries(defaultSettings)) {
				this.options[key] = new Option(value);
			}

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

		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 = "";

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

			if (this.settings.options.moveSubscribeButtons.getValue()) {
				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.options.hideLikeCount.getValue()) {
				this.otherStyles += `
                    .like-wrap .count {
                        display: none;
                    }
                `;
			}
		}

		createHighlightStyles() {
			this.highlightStyles = "";
			for (const user of this.settings.verifiedUsers) {
				if (
					this.settings.options.highlightEnabled.getValue() ||
					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.options.highlightEnabledForReplies.getValue() ||
					user.highlightEnabledForReplies
				) {
					this.createHighlightCSS(
						user,
						`div.reply:has( a.name[href*="/${user.username}/" i] )`
					);
				}

				this.#createActivityCss(user);
			}

			this.disableHighlightOnSmallCards();
		}

		#createActivityCss(user) {
			const colorUserActivity =
				this.settings.options.colorUserActivity.getValue() ??
				user.colorUserActivity;
			const colorUserReplies =
				this.settings.options.colorUserReplies.getValue() ??
				user.colorUserReplies;

			if (colorUserActivity) {
				this.highlightStyles += `
                .activity-entry :is(.details, .wrap):has(a[href*="/${
					user.username
				}/"]) a
                {
                    color: ${this.getUserColor(user)};
                }
            `;
			}
			if (colorUserReplies) {
				this.highlightStyles += `
                .reply:has(a[href*="/${user.username}/"]) a,
                .reply:has(a[href*="/${
					user.username
				}/"]) .markdown-spoiler::before
                {
                    color: ${this.getUserColor(user)};
                }
            `;
			}
		}

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

		createHighlightCSS(user, selector) {
			this.highlightStyles += `
                ${selector} {
                    margin-right: -${this.settings.options.highlightSize.getValue()};
                    border-right: ${this.settings.options.highlightSize.getValue()} 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.options.highlightEnabled.getValue()) {
				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.options.enabledForProfileName.getValue()) {
				return;
			}

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

			const user = this.settings.verifiedUsers.find(
				(u) => u.username.toLowerCase() === username.toLowerCase()
			);

			if (!user) {
				this.clearProfileVerify();
				return;
			}

			const profileStyle = `
                    .name-wrapper h1.name::after {
                    content: "${
						this.stringIsEmpty(user.sign) ??
						this.settings.options.defaultSign.getValue()
					}"
                    }
                `;
			this.profileLink = this.createStyleLink(profileStyle, "profile");
		}

		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.options.copyColorFromProfile.getValue()
				)
			) {
				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.options.copyColorFromProfile.getValue())
					? `rgb(${user.color})`
					: undefined)
			);
		}

		getDefaultHighlightColor() {
			if (this.settings.options.useDefaultHighlightColor.getValue()) {
				return this.settings.options.defaultHighlightColor.getValue();
			}
			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.options.globalCssEnabled.getValue()) {
				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.options.globalCssAutoDisable.getValue()) {
				return true;
			}

			if (!window.location.pathname.startsWith("/user/")) {
				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.options.moveSubscribeButtons.getValue()) {
				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";
		#activeCategory = "all";

		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");
			settingsContainer.setAttribute("class", "void-settings");
			this.#renderSettingsHeader(settingsContainer);

			this.#renderCategories(settingsContainer);
			this.#renderOptions(settingsContainer);
			this.#renderUserTable(settingsContainer);
			this.#renderCustomCssEditor(settingsContainer);

			container.append(settingsContainer);
		}

		#renderOptions(settingsContainer) {
			const oldSettingsListContainer =
				document.getElementById("void-settings-list");
			const settingsListContainer =
				oldSettingsListContainer ?? document.createElement("div");
			settingsListContainer.innerHTML = "";
			settingsListContainer.setAttribute("id", "void-settings-list");
			settingsListContainer.setAttribute("class", "void-settings-list");

			for (const [key, setting] of Object.entries(this.settings.options)) {
				if (
					setting.category !== this.#activeCategory &&
					this.#activeCategory !== "all"
				) {
					continue;
				}
				this.#renderSetting(setting, settingsListContainer, key);
			}

			oldSettingsListContainer ??
				settingsContainer.append(settingsListContainer);
		}

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

		#renderSettingsHeader(settingsContainer) {
			const headerContainer = document.createElement("div");
			headerContainer.setAttribute("class", "void-settings-header");
			const header = document.createElement("h1");
			header.innerText = "VoidVerified settings";

			const versionInfo = document.createElement("p");
			versionInfo.append("Version: ");
			const versionNumber = document.createElement("span");
			versionNumber.append(this.settings.version);

			versionInfo.append(versionNumber);

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

		#renderCategories(settingsContainer) {
			const oldNav = document.querySelector(".void-nav");
			const nav = oldNav ?? document.createElement("nav");

			nav.innerHTML = "";

			nav.setAttribute("class", "void-nav");
			const list = document.createElement("ol");

			list.append(this.#createNavBtn("all"));

			for (const category of Object.values(categories)) {
				list.append(this.#createNavBtn(category));
			}

			nav.append(list);
			oldNav ?? settingsContainer.append(nav);
		}

		#createNavBtn(category) {
			const li = document.createElement("li");
			li.append(category);
			if (category === this.#activeCategory) {
				li.setAttribute("class", "void-active");
			}

			li.addEventListener("click", () => {
				this.#activeCategory = category;
				this.#renderCategories();
				this.#renderOptions();
			});

			return li;
		}

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

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

			tableContainer.style = `
            margin-top: 25px;
        `;

			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.setAttribute("type", "text");
			signInput.value = user.sign ?? "";
			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.options.enabledForUsername.getValue()
				)
			);

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

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

			const colorInput = document.createElement("input");
			colorInput.setAttribute("type", "color");
			colorInput.value = this.#getUserColorPickerColor(user);
			colorInput.addEventListener(
				"change",
				(event) => this.#handleUserColorChange(event, user.username),
				false
			);

			colorInputContainer.append(colorInput);

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

			colorInputContainer.append(resetColorBtn);

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

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

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

			colorInputContainer.append(
				this.#createUserCheckbox(
					user.colorUserActivity,
					user.username,
					"colorUserActivity",
					this.settings.options.colorUserActivity.getValue()
				)
			);

			colorInputContainer.append(
				this.#createUserCheckbox(
					user.colorUserReplies,
					user.username,
					"colorUserReplies",
					this.settings.options.colorUserReplies.getValue()
				)
			);

			const colorCell = this.#createCell(colorInputContainer);
			row.append(colorCell);

			const quickAccessCheckbox = this.#createUserCheckbox(
				user.quickAccessEnabled,
				user.username,
				"quickAccessEnabled",
				this.settings.options.quickAccessEnabled.getValue()
			);
			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.options.copyColorFromProfile.getValue())
			) {
				return ColorFunctions.rgbToHex(user.color);
			}

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

			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.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 = setting.getValue();
			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");
			} else if (type === "string") {
				input.setAttribute("type", "text");
			}

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

			input.setAttribute("id", settingKey);

			if (settingKey === "pasteKeybind") {
				input.style.width = "80px";
				input.addEventListener("keydown", (event) =>
					this.#handleKeybind(event, settingKey, input)
				);
			} else {
				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;
			container.append(label);
			settingsContainer.append(container);
		}

		#handleKeybind(event, settingKey, input) {
			event.preventDefault();
			const keybind = event.key;
			this.settings.saveSettingToLocalStorage(settingKey, keybind);
			input.value = keybind;
		}

		#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");
			container.setAttribute("class", "void-css-editor");
			const label = document.createElement("label");
			label.innerText = "Custom Global CSS";
			label.setAttribute("for", "void-verified-global-css-editor");
			container.append(label);

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

			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.options.quickAccessEnabled.getValue() &&
				!this.settings.verifiedUsers.some((user) => user.quickAccessEnabled)
			) {
				return;
			}

			const quickAccessContainer = document.createElement("div");
			quickAccessContainer.setAttribute("class", "void-quick-access");
			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.setAttribute("class", "void-quick-access-wrap");

			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.setAttribute("class", "void-quick-access-item");
			const link = document.createElement("a");
			container.setAttribute(
				"href",
				`https://anilist.co/user/${user.username}/`
			);

			const image = document.createElement("div");
			image.style.backgroundImage = `url(${user.avatar})`;
			image.setAttribute("class", "void-quick-access-pfp");
			container.append(image);

			const username = document.createElement("div");
			username.append(user.username);
			username.setAttribute("class", "void-quick-access-username");

			container.append(username);
			container.append(link);
			return container;
		}

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

		#getQuickAccessUsers() {
			if (this.settings.options.quickAccessEnabled.getValue()) {
				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;
			}

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

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

			intervalScriptHandler.globalCSS.createCss();
		}

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

	class PasteHandler {
		settings;

		#imageFormats = [
			"jpg",
			"png",
			"gif",
			"webp",
			"apng",
			"avif",
			"jpeg",
			"svg",
		];

		#isKeyPressed = false;
		constructor(settings) {
			this.settings = settings;
		}

		setup() {
			window.addEventListener("keydown", (event) => {
				this.#handleKeybind(event);
			});
			window.addEventListener("keyup", (event) => {
				this.#handleKeybind(event, false);
			});
			window.addEventListener("paste", (event) => {
				this.#handlePaste(event);
			});
		}

		#handleKeybind(event, isKeyDown = true) {
			if (this.settings.options.pasteKeybind.getValue() !== event.key) {
				return;
			}
			this.#isKeyPressed = isKeyDown;
		}

		#handlePaste(event) {
			const clipboard = event.clipboardData.getData("text/plain").trim();

			if (!this.settings.options.pasteEnabled.getValue()) {
				return;
			}

			if (
				this.settings.options.pasteRequireKeyPress.getValue() &&
				!this.#isKeyPressed
			) {
				return;
			}

			event.preventDefault();
			const rows = clipboard.split("\n");
			let result = [];

			for (let row of rows) {
				result.push(this.#handleRow(row));
			}

			const transformedClipboard = result.join("\n\n");
			window.document.execCommand("insertText", false, transformedClipboard);
		}

		#handleRow(row) {
			row = row.trim();
			if (
				this.#imageFormats.some((format) =>
					row.toLowerCase().endsWith(format)
				)
			) {
				return this.#handleImg(row);
			} else if (row.toLowerCase().startsWith("http")) {
				return `[](${row})`;
			} else {
				return row;
			}
		}

		#handleImg(row) {
			const img = `img${this.settings.options.pasteImageWidth.getValue()}(${row})`;
			let result = img;
			if (this.settings.options.pasteWrapImagesWithLink.getValue()) {
				result = `[ ${img} ](${row})`;
			}
			return result;
		}
	}

	const styles = `
    a[href="/settings/developer" i]::after{content: " & Void"}
    .void-settings .void-nav ol {
        display: flex;
        margin: 8px 0px;
        padding: 0;
    }

    .void-settings .void-nav li {
        list-style: none;
        display: block;
        color: white;
        padding: 3px 8px;
        text-transform: capitalize;
        background: black;
        cursor: pointer;
        min-width: 50px;
        text-align: center;
        font-size: 1.3rem;
    }

    .void-settings .void-nav li.void-active {
        background: rgb(var(--color-blue));
    }

    .void-settings .void-nav li:first-child {
        border-radius: 4px 0px 0px 4px;
    }

    .void-settings .void-nav li:last-child {
        border-radius: 0px 4px 4px 0px;
    }
    
    .void-settings .void-nav li:hover {
        background: rgb(var(--color-blue));
    }

    .void-settings .void-settings-header {
        margin-top: 30px;
    }

    .void-settings .void-table input[type="text"] {
        width: 100px;
    }
    .void-settings .void-table input[type="color"] {
        border: 0;
        height: 24px;
        width: 40px;
        padding: 0;
        background-color: unset;
        cursor: pointer;
    }

    .void-settings .void-table input[type="checkbox"] {
        margin-left: 3px;
        margin-right: 3px;
    }

    .void-settings .void-table button {
        background: unset;
        border: none;
        cursor: pointer;
        padding: 0;
    }

    .void-settings .void-table form {
        padding: 8px;
        display: flex;
        align-items: center;
        gap: 8px;
    }

    .void-settings .void-settings-header span {
        color: rgb(var(--color-blue));
    }

    .void-settings .void-settings-list {
        display: flex;
        flex-direction: column;
        gap: 5px;
    }

    .void-settings .void-settings-list input[type="color"] {
        border: 0;
        height: 20px;
        width: 25px;
        padding: 0;
        background-color: unset;
        cursor: pointer;
    }

    .void-settings .void-settings-list input[type="text"] {
        width: 50px;
    }

    .void-settings .void-settings-list label {
        margin-left: 5px;
    }

    .void-settings .void-css-editor label {
        margin-top: 20px;
        fontSize: 2rem;
        display: inline-block;
    }

    .void-settings .void-css-editor textarea {
        width: 100%;
        height: 200px;
        resize: vertical;
        background: rgb(var(--color-foreground));
        color: rgb(var(--color-text));
    }
    
    .void-quick-access .void-quick-access-wrap {
        background: rgb(var(--color-foreground));
        display: grid;
        grid-template-columns: repeat(auto-fill, 60px);
        grid-template-rows: repeat(auto-fill, 80px);
        gap: 15px;
        padding: 15px;
        margin-bottom: 25px;
    }

    .void-quick-access-item {
        display: inline-block;
    }

    .void-quick-access-pfp {
        background-size: contain;
        background-repeat: no-repeat;
        height: 60px;
        width: 60px;
    }

    .void-quick-access-username {
        display: inline-block;
        text-align: center;
        bottom: -20px;
        width: 100%;
        word-break: break-all;
        font-size: 1.2rem;
    }
`;

	const settings = new Settings();
	const styleHandler = new StyleHandler(settings);
	const intervalScriptHandler = new IntervalScriptHandler(settings);
	const anilistAPI = new AnilistAPI(settings);
	const pasteHandler = new PasteHandler(settings);

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

	anilistAPI.queryUserData();
	pasteHandler.setup();

	styleHandler.createStyleLink(styles, "script");

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

})();