Greasy Fork

VoidVerified

Social enhancements for AniList.

目前为 2024-02-09 提交的版本。查看 最新版本

// ==UserScript==
// @name          VoidVerified
// @version       1.7.0
// @namespace     http://tampermonkey.net/
// @author        voidnyan
// @description   Social enhancements for 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",
		css: "css",
		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.css,
		},
		globalCssAutoDisable: {
			defaultValue: true,
			description: "Disable global CSS when a profile has custom CSS.",
			category: categories.css,
		},
		profileCssEnabled: {
			defaultValue: false,
			description: "Load user's custom CSS when viewing their profile.",
			category: categories.css,
			authRequired: true,
		},
		activityCssEnabled: {
			defaultValue: false,
			description:
				"Load user's custom CSS when viewing their activity (direct link).",
			category: categories.css,
			authRequired: true,
		},
		onlyLoadCssFromVerifiedUser: {
			defaultValue: false,
			description: "Only load custom CSS from verified users.",
			category: categories.css,
		},
		layoutDesignerEnabled: {
			defaultValue: false,
			description: "Enable Layout Designer in the settings tab.",
			category: categories.misc,
			authRequired: true,
		},
		quickAccessEnabled: {
			defaultValue: false,
			description: "Display quick access of users in home page.",
			category: categories.users,
		},
		quickAccessBadge: {
			defaultValue: false,
			description:
				"Display a badge on quick access when changes are detected on user's layout.",
			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,
		},
		pasteImagesToHostService: {
			defaultValue: false,
			description:
				"Upload image from the clipboard to image host (configure below).",
			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("")
			);
		}

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

		static 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",
		};

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

			return this.hexToRgb(color);
		}
	}

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

		settings;
		#userId;
		#url = "https://graphql.anilist.co";
		#lastFetchedLocalStorage = "void-verified-last-fetched";
		constructor(settings) {
			this.settings = settings;
			this.#userId = Number(JSON.parse(localStorage.getItem("auth")).id);
		}

		async getActivityCss(activityId) {
			const query = `query ($activityId: Int) {
            Activity(id: $activityId) {
                ... on ListActivity {
                    user {
                        name
                        about
                        options {
                            profileColor
                        }
                }}
                ... on TextActivity {
                    user {
                        name
                        about
                        options {
                            profileColor
                        }
                    }
                }
                ... on MessageActivity {
                    recipient {
                        name
                        about
                        options {
                            profileColor
                        }
                    }
                }
            }
        }`;

			const variables = { activityId };
			const options = this.#getQueryOptions(query, variables);
			try {
				const response = await fetch(this.#url, options);
				const result = await response.json();
				return result;
			} catch (error) {
				return error;
			}
		}

		async getUserAbout(username) {
			const query = `query ($username: String) {
            User(name: $username) {
                about
            }
        }`;

			const variables = { username };
			const options = this.#getQueryOptions(query, variables);
			try {
				const response = await fetch(this.#url, options);
				const result = await response.json();
				return result.data;
			} catch (error) {
				return error;
			}
		}

		async saveUserAbout(about) {
			const query = `mutation ($about: String) {
            UpdateUser(about: $about) {
                about
            }
        }`;
			const variables = { about };
			const options = this.#getMutationOptions(query, variables);
			try {
				const response = await fetch(this.#url, options);
				const result = await response.json();
				return result.data;
			} catch (error) {
				return error;
			}
		}

		async saveUserColor(color) {
			const query = `mutation ($color: String) {
            UpdateUser(profileColor: $color) {
                options {
                    profileColor
                }
            }
        }`;

			const variables = { color };
			const options = this.#getMutationOptions(query, variables);
			try {
				const response = await fetch(this.#url, options);
				const result = await response.json();
				return result.data;
			} catch (error) {
				throw new Error("Failed to publish profile color", error);
			}
		}

		async saveDonatorBadge(text) {
			const query = `mutation ($text: String) {
            UpdateUser(donatorBadge: $text) {
                donatorBadge
            }
        }`;

			const variables = { text };
			const options = this.#getMutationOptions(query, variables);
			try {
				const response = await fetch(this.#url, options);
				const result = await response.json();
				return result.data;
			} catch (error) {
				throw new Error("Failed to publish donator badge", error);
			}
		}

		queryUserData() {
			const lastFetched = new Date(
				localStorage.getItem(this.#lastFetchedLocalStorage)
			);
			const currentTime = new Date();

			if (!lastFetched || currentTime - lastFetched > this.apiQueryTimeout) {
				this.#querySelf();
				this.#queryUsers(1);
				localStorage.setItem(this.#lastFetchedLocalStorage, new Date());
			}
		}

		#querySelf() {
			const variables = { userId: this.#userId };
			const query = `query ($userId: Int!) {
                User(id: $userId) {
                  name
                  avatar {
                    large
                  }
                  bannerImage
                  options {
                    profileColor
                  }
              }
            }
        `;

			const options = this.#getQueryOptions(query, variables);

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

		#queryUsers(page) {
			const variables = { page, userId: this.#userId };
			const query = `query ($page: Int, $userId: Int!) {
            Page(page: $page) {
                following(userId: $userId) {
                  name
                  avatar {
                    large
                  }
                  bannerImage
                  options {
                    profileColor
                  }
                }, 
                pageInfo {
                  total
                  perPage
                  currentPage
                  lastPage
                  hasNextPage
                }
              }
            }
        `;

			const options = this.#getQueryOptions(query, variables);

			fetch(this.#url, options)
				.then(this.#handleResponse)
				.then((data) => {
					this.#handleQueriedUsers(data.Page.following);
					const pageInfo = data.Page.pageInfo;
					if (pageInfo.hasNextPage) {
						this.#queryUsers(pageInfo.currentPage + 1);
					}
				})
				.catch((err) => {
					console.error(err);
				});
		}

		#handleQueriedUsers(users) {
			for (const user of users) {
				this.settings.updateUserFromApi(user);
			}
		}

		#getQueryOptions(query, variables) {
			const options = {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					Accept: "application/json",
				},
				body: JSON.stringify({
					query,
					variables,
				}),
			};

			if (this.settings.auth?.token) {
				options.headers.Authorization = `Bearer ${this.settings.auth.token}`;
			}

			return options;
		}

		#getMutationOptions(query, variables) {
			if (!this.settings.auth.token) {
				console.error("VoidVerified is not authenticated.");
				return;
			}
			let queryOptions = this.#getQueryOptions(query, variables);
			return queryOptions;
		}

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

	class Option {
		value;
		defaultValue;
		description;
		category;
		authRequired;
		constructor(option) {
			this.defaultValue = option.defaultValue;
			this.description = option.description;
			this.category = option.category;
			this.authRequired = option.authRequired;
		}

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

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

		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;
			}

			this.auth =
				JSON.parse(localStorage.getItem(this.localStorageAuth)) ?? null;

			const auth = JSON.parse(localStorage.getItem("auth"));
			this.anilistUser = auth?.name;
		}

		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();
		}

		getUser(username) {
			return this.verifiedUsers.find((user) => user.username === username);
		}

		isVerified(username) {
			return this.verifiedUsers.some((user) => user.username === username);
		}

		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(apiUser) {
			const user = this.verifiedUsers.find(
				(u) => u.username === apiUser.name
			);
			if (!user) {
				return;
			}

			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 = ColorFunctions.handleAnilistColor(
				apiUser.options.profileColor
			);

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

			if (this.options.quickAccessBadge.getValue() || user.quickAccessBadge) {
				if (
					(user.avatar && user.avatar !== userObject.avatar) ||
					(user.color && user.color !== userObject.color) ||
					(user.banner && user.banner !== userObject.banner)
				) {
					userObject.quickAccessBadgeDisplay = true;
				}
			}

			return userObject;
		}

		saveAuthToken(tokenObject) {
			this.auth = tokenObject;
			localStorage.setItem(
				this.localStorageAuth,
				JSON.stringify(tokenObject)
			);
		}

		removeAuthToken() {
			this.auth = null;
			localStorage.removeItem(this.localStorageAuth);
		}

		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)
			);
		}
	}

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

		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 += `
                div.wrap:has( div.header > a.name[href*="/${
					user.username
				}/"]) a,
                div.wrap:has( div.details > a.name[href*="/${
					user.username
				}/"]) a
                {
                    color: ${this.getUserColor(user)};
                }
            `;
			}
			if (colorUserReplies) {
				this.highlightStyles += `
                .reply:has(a.name[href*="/${user.username}/"]) a,
                .reply:has(a.name[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");
		}

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

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

			const username =
				window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];

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

			if (!user) {
				this.clearStyles("profile");
				return;
			}

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

		getStyleLink(id) {
			return document.getElementById(`void-verified-${id}-styles`);
		}

		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/") &&
				!window.location.pathname.startsWith("/activity/")
			) {
				return true;
			}

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

			const styleHandler = new StyleHandler(this.settings);
			const voidActivityStyles = styleHandler.getStyleLink("activity-css");
			const voidUserStyles = styleHandler.getStyleLink("user-css");

			if (voidActivityStyles || voidUserStyles) {
				return false;
			}

			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);
			}
		}
	}

	const imageHosts = {
		imgbb: "imgbb",
		imgur: "imgur",
	};

	const imageHostConfiguration = {
		selectedHost: imageHosts.imgbb,
		configurations: {
			imgbb: {
				name: "imgbb",
				apiKey: "",
			},
			imgur: {
				name: "imgur",
				clientId: "",
				clientSecret: "",
				expires: null,
				refreshToken: null,
				authToken: null,
			},
		},
	};

	class ImageHostService {
		#configuration;
		#localStorage = "void-verified-image-host-config";
		constructor() {
			const config = JSON.parse(localStorage.getItem(this.#localStorage));
			if (!config) {
				localStorage.setItem(
					this.#localStorage,
					JSON.stringify(imageHostConfiguration)
				);
			} else {
				for (const key of Object.keys(
					imageHostConfiguration.configurations
				)) {
					if (config.configurations[key]) {
						continue;
					}
					config.configurations[key] =
						imageHostConfiguration.configurations[key];
				}
				localStorage.setItem(this.#localStorage, JSON.stringify(config));
			}

			this.#configuration = config ?? imageHostConfiguration;
		}

		getImageHostConfiguration(host) {
			return this.#configuration.configurations[host];
		}

		getSelectedHost() {
			return this.#configuration.selectedHost;
		}

		setSelectedHost(host) {
			this.#configuration.selectedHost = host;
			localStorage.setItem(
				this.#localStorage,
				JSON.stringify(this.#configuration)
			);
		}

		setImageHostConfiguration(host, config) {
			this.#configuration.configurations[host] = config;
			localStorage.setItem(
				this.#localStorage,
				JSON.stringify(this.#configuration)
			);
		}
	}

	class ImageHostBase {
		conventToBase64(image) {
			return new Promise(function (resolve, reject) {
				var reader = new FileReader();
				reader.onloadend = function (e) {
					resolve({
						fileName: this.name,
						result: e.target.result,
						error: e.target.error,
					});
				};
				reader.readAsDataURL(image);
			});
		}
	}

	class ImgbbAPI extends ImageHostBase {
		#url = "https://api.imgbb.com/1/upload";
		#configuration;
		constructor(configuration) {
			super();
			this.#configuration = configuration;
		}

		async uploadImage(image) {
			const file = await this.conventToBase64(image);
			if (!file.result) {
				return;
			}

			if (!this.#configuration.apiKey) {
				return;
			}

			const base64 = file.result.split("base64,")[1];

			const settings = {
				method: "POST",
				headers: {
					Accept: "application/json",
					"Content-Type": "application/x-www-form-urlencoded",
				},
				body:
					"image=" +
					encodeURIComponent(base64) +
					"&name=" +
					image.name.split(".")[0],
			};

			try {
				const response = await fetch(
					`${this.#url}?key=${this.#configuration.apiKey}`,
					settings
				);
				const data = await response.json();
				return data.data.url;
			} catch (error) {
				console.error(error);
				return null;
			}
		}

		renderSettings() {
			const container = document.createElement("div");
			const apiKeyInput = document.createElement("input");
			apiKeyInput.setAttribute("type", "text");

			const label = document.createElement("label");
			label.append("API key");
			label.setAttribute("class", "void-api-label");
			container.append(label);

			apiKeyInput.setAttribute("value", this.#configuration.apiKey);
			apiKeyInput.addEventListener("change", (event) =>
				this.#updateApiKey(event, this.#configuration)
			);
			apiKeyInput.setAttribute("class", "void-api-key");
			container.append(apiKeyInput);

			const note = document.createElement("div");
			note.append("You need to get the API key from the following link: ");

			note.setAttribute("class", "void-notice");

			const apiKeyLink = document.createElement("a");
			apiKeyLink.setAttribute("href", "https://api.imgbb.com/");
			apiKeyLink.append("api.imgbb.com");
			apiKeyLink.setAttribute("target", "_blank");
			note.append(apiKeyLink);

			container.append(note);

			return container;
		}

		#updateApiKey(event, configuration) {
			const apiKey = event.target.value;
			const config = {
				...configuration,
				apiKey,
			};
			new ImageHostService().setImageHostConfiguration(config.name, config);
		}
	}

	class DOM {
		static create(element, classes = null, children = null) {
			const el = document.createElement(element);
			if (classes !== null) {
				for (const className of classes?.split(" ")) {
					if (className.startsWith("#")) {
						el.setAttribute("id", `void-${className.slice(1)}`);
						continue;
					}
					el.classList.add(`void-${className}`);
				}
			}

			if (children) {
				if (Array.isArray(children)) {
					el.append(...children);
				} else {
					el.append(children);
				}
			}

			return el;
		}

		static getOrCreate(element, classes) {
			const id = classes
				.split(" ")
				.find((className) => className.startsWith("#"));
			return this.get(id) ?? this.create(element, classes);
		}

		static get(selector) {
			return document.querySelector(selector);
		}

		static getAll(selector) {
			return document.querySelectorAll(selector);
		}
	}

	class ImgurAPI extends ImageHostBase {
		#url = "https://api.imgur.com/3/image";
		#configuration;
		constructor(configuration) {
			super();
			this.#configuration = configuration;
		}

		async uploadImage(image) {
			const file = await this.conventToBase64(image);
			if (!file.result) {
				return;
			}

			if (!this.#configuration.clientId) {
				return;
			}

			const base64 = file.result.split("base64,")[1];

			const formData = new FormData();
			formData.append("image", base64);
			formData.append("title", image.name.split(".")[0]);

			const settings = {
				method: "POST",
				headers: {
					Authorization: this.#configuration.authToken
						? `Bearer ${this.#configuration.authToken}`
						: `Client-ID ${this.#configuration.clientId}`,
				},
				body: formData,
			};

			try {
				const response = await fetch(this.#url, settings);
				const data = await response.json();
				return data.data.link;
			} catch (error) {
				console.error(error);
				return null;
			}
		}

		renderSettings(settingsUi) {
			const container = DOM.create("div");

			const idInput = DOM.create("input");
			idInput.addEventListener("change", (event) => {
				this.#updateConfig(event, this.#configuration);
				settingsUi.renderSettingsUi();
			});

			idInput.setAttribute("name", "clientId");
			idInput.value = this.#configuration?.clientId ?? "";

			const idLabel = DOM.create("label", "api-label", [
				"Client ID",
				idInput,
			]);

			const secretInput = DOM.create("input");
			secretInput.addEventListener("change", (event) => {
				this.#updateConfig(event, this.#configuration);
				settingsUi.renderSettingsUi();
			});

			secretInput.setAttribute("name", "clientSecret");
			secretInput.value = this.#configuration?.clientSecret ?? "";

			const secretLabel = DOM.create("label", "api-label", [
				"Client Secret",
				secretInput,
			]);

			container.append(DOM.create("div", null, idLabel));
			container.append(DOM.create("div", null, secretLabel));

			if (
				this.#configuration.clientId &&
				this.#configuration.clientSecret &&
				!this.#configuration.authToken
			) {
				const authLink = DOM.create("a", null, "Authorize Imgur");
				authLink.classList.add("button");
				authLink.setAttribute(
					"href",
					`https://api.imgur.com/oauth2/authorize?client_id=${
					this.#configuration.clientId
				}&response_type=token`
				);
				container.append(authLink);
			}

			if (this.#configuration.authToken) {
				const revokeAuthButton = DOM.create(
					"button",
					null,
					"Clear Authorization"
				);
				revokeAuthButton.classList.add("button");
				revokeAuthButton.addEventListener("click", () => {
					this.#revokeAuth();
					settingsUi.renderSettingsUi();
				});
				container.append(revokeAuthButton);
			}

			this.#renderNote(container);
			return container;
		}

		handleAuth() {
			const hash = window.location.hash.substring(1);
			if (!hash) {
				return;
			}

			const [path, token, expires, _, refreshToken] = hash.split("&");

			if (path !== "void_imgur") {
				return;
			}

			let config = { ...this.#configuration };
			config.authToken = token.split("=")[1];
			config.refreshToken = refreshToken.split("=")[1];

			config.expires = new Date(
				new Date().getTime() + Number(expires.split("=")[1])
			);

			new ImageHostService().setImageHostConfiguration(
				imageHosts.imgur,
				config
			);

			window.history.replaceState(
				null,
				"",
				"https://anilist.co/settings/developer"
			);
		}

		async refreshAuthToken() {
			if (
				!this.#configuration.refreshToken ||
				!this.#configuration.clientSecret ||
				!this.#configuration.clientId
			) {
				return;
			}

			const expires = new Date(this.#configuration.expires);

			if (new Date() < expires) {
				return;
			}

			const formData = new FormData();
			formData.append("refresh_token", this.#configuration.refreshToken);
			formData.append("client_id", this.#configuration.clientId);
			formData.append("client_secret", this.#configuration.clientSecret);
			formData.append("grant_type", "refresh_token");

			try {
				const response = await fetch("https://api.imgur.com/oauth2/token", {
					method: "POST",
					body: formData,
				});
				if (!response.status === 200) {
					console.error("Failed to reauthorize Imgur");
					return;
				}
				const data = await response.json();
				window.alert(
					"voidnyan, your new imgur token is in console, please check it out!"
				);
				const config = {
					...this.#configuration,
					authToken: data.access_token,
					expires: new Date(new Date().getTime() + data.expires_in),
				};
				new ImageHostService().setImageHostConfiguration(
					imageHosts.imgur,
					config
				);
			} catch (error) {
				console.error(error);
			}
		}

		#renderNote(container) {
			const note = DOM.create(
				"div",
				"notice",
				"How to setup Imgur integration"
			);

			const registerLink = DOM.create("a", null, "api.imgur.com");
			registerLink.setAttribute(
				"href",
				"https://api.imgur.com/oauth2/addclient"
			);
			registerLink.setAttribute("target", "_blank");

			const stepList = DOM.create("ol", null, [
				DOM.create("li", null, [
					"Register your application: ",
					registerLink,
					". Use 'https://anilist.co/settings/developer#void_imgur' as callback URL.",
				]),
				DOM.create(
					"li",
					null,
					"Fill the client id and secret fields with the value Imgur provided."
				),
				DOM.create(
					"li",
					null,
					"Click on authorize (you can skip this step if you don't want images tied to your account)."
				),
			]);

			note.append(stepList);

			note.append(
				"Hitting Imgur API limits might get your API access blocked."
			);

			note.append(
				"Token expires at " +
					new Date(this.#configuration.expires).toISOString()
			);

			container.append(note);
		}

		#revokeAuth() {
			const config = {
				...this.#configuration,
				authToken: null,
				refreshToken: null,
			};

			new ImageHostService().setImageHostConfiguration(
				imageHosts.imgur,
				config
			);
		}

		#updateConfig(event, configuration) {
			const value = event.target.value;
			const key = event.target.name;
			const config = {
				...configuration,
				[key]: value,
			};
			new ImageHostService().setImageHostConfiguration(
				imageHosts.imgur,
				config
			);
		}
	}

	class ImageApiFactory {
		getImageHostInstance() {
			const imageHostService = new ImageHostService();
			switch (imageHostService.getSelectedHost()) {
				case imageHosts.imgbb:
					return new ImgbbAPI(
						imageHostService.getImageHostConfiguration(imageHosts.imgbb)
					);
				case imageHosts.imgur:
					return new ImgurAPI(
						imageHostService.getImageHostConfiguration(imageHosts.imgur)
					);
			}
		}
	}

	const ColorPicker = (value, onChange) => {
		const container = DOM.create("div", "color-picker-container");
		const colorPicker = DOM.create("input", "color-picker");
		colorPicker.setAttribute("type", "color");
		colorPicker.value = value;
		colorPicker.addEventListener("change", (event) => {
			onChange(event);
		});

		const inputField = DOM.create("input", "color-picker-input");
		inputField.value = value;
		inputField.addEventListener("change", (event) => {
			onChange(event);
		});

		container.append(colorPicker, inputField);
		return container;
	};

	const InputField = (value, onChange, classes) => {
		const inputField = DOM.create("input", transformClasses("input", classes));
		inputField.value = value;
		inputField.addEventListener("change", (event) => {
			onChange(event);
		});
		return inputField;
	};

	const Button = (text, onClick) => {
		const button = DOM.create("button", "button", text);
		button.addEventListener("click", (event) => {
			onClick(event);
		});
		return button;
	};

	const Note = (text) => {
		const note = DOM.create("div", "notice", text);
		return note;
	};

	const Link = (text, href, target = "_blank", classes) => {
		const link = DOM.create("a", transformClasses("link", classes), text);
		link.setAttribute("href", href);
		link.setAttribute("target", target);
		return link;
	};

	const TextArea = (text, onChange, classes) => {
		const textArea = DOM.create(
			"textarea",
			transformClasses("textarea", classes),
			text
		);
		textArea.addEventListener("change", (event) => {
			onChange(event);
		});

		return textArea;
	};

	const transformClasses = (base, additional) => {
		let classes = base;
		if (additional) {
			classes += ` ${additional}`;
		}
		return classes;
	};

	const subCategories = {
		users: "users",
		authorization: "authorization",
		imageHost: "image host",
		layout: "layout & CSS",
		globalCss: "global CSS",
	};

	class SettingsUserInterface {
		settings;
		styleHandler;
		globalCSS;
		userCSS;
		layoutDesigner;
		AnilistBlue = "120, 180, 255";
		#activeCategory = "all";
		#activeSubCategory = "users";

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

		renderSettingsUi() {
			this.#checkAuthFromUrl();
			const container = DOM.get(".settings.container > .content");
			const settingsContainerExists =
				DOM.get("#void-verified-settings") !== null;
			if (!settingsContainerExists) {
				const settingsContainer = DOM.create(
					"div",
					"#verified-settings settings"
				);
				container.append(settingsContainer);
			}

			this.renderSettingsUiContent();
		}

		renderSettingsUiContent() {
			const settingsContainer = DOM.get("#void-verified-settings");
			const innerContainer = DOM.create("div");
			this.#renderSettingsHeader(innerContainer);

			this.#renderCategories(innerContainer);
			this.#renderOptions(innerContainer);
			this.#renderSubCategories(innerContainer);

			switch (this.#activeSubCategory) {
				case subCategories.users:
					this.#renderUserTable(innerContainer);
					break;
				case subCategories.authorization:
					this.#creatAuthenticationSection(innerContainer);
					break;
				case subCategories.imageHost:
					this.#renderImageHostSettings(innerContainer);
					break;
				case subCategories.layout:
					innerContainer.append(this.layoutDesigner.renderSettings(this));
					if (
						this.settings.auth?.token &&
						(this.settings.options.profileCssEnabled.getValue() ||
							this.settings.options.activityCssEnabled.getValue())
					) {
						this.#renderCustomCssEditor(innerContainer, this.userCSS);
					}
					break;
				case subCategories.globalCss:
					if (this.settings.options.globalCssEnabled.getValue()) {
						this.#renderCustomCssEditor(innerContainer, this.globalCSS);
					}
					break;
			}

			settingsContainer.replaceChildren(innerContainer);
		}

		#renderOptions(settingsContainer) {
			const settingsListContainer = DOM.create("div", "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);
			}

			settingsContainer.append(settingsListContainer);
		}

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

		#renderSettingsHeader(settingsContainer) {
			const headerContainer = DOM.create("div", "settings-header");
			const header = DOM.create("h1", null, "VoidVerified Settings");

			const versionInfo = DOM.create("p", null, [
				"Version: ",
				DOM.create("span", null, this.settings.version),
			]);

			headerContainer.append(header);
			headerContainer.append(versionInfo);
			headerContainer.append(
				DOM.create("p", null, [
					"Author: ",
					Link("voidnyan", "https://anilist.co/user/voidnyan/"),
				])
			);
			settingsContainer.append(headerContainer);
		}

		#renderCategories(settingsContainer) {
			const nav = DOM.create("nav", "nav");
			const list = DOM.create("ol");

			const onClick = (_category) => {
				this.#activeCategory = _category;
				this.renderSettingsUiContent();
			};

			list.append(
				this.#createNavBtn("all", "all" === this.#activeCategory, () => {
					onClick("all");
				})
			);

			for (const category of Object.values(categories)) {
				list.append(
					this.#createNavBtn(
						category,
						category === this.#activeCategory,
						() => {
							onClick(category);
						}
					)
				);
			}

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

		#renderSubCategories(settingsContainer) {
			const nav = DOM.create("nav", "nav");
			const list = DOM.create("ol");

			for (const subCategory of Object.values(subCategories)) {
				if (!this.#shouldDisplaySubCategory(subCategory)) {
					continue;
				}
				list.append(
					this.#createNavBtn(
						subCategory,
						this.#activeSubCategory === subCategory,
						() => {
							this.#activeSubCategory = subCategory;
							this.renderSettingsUiContent();
						}
					)
				);
			}

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

		#shouldDisplaySubCategory(subCategory) {
			switch (subCategory) {
				case subCategories.users:
					return true;
				case subCategories.authorization:
					return true;
				case subCategories.imageHost:
					return this.settings.options.pasteImagesToHostService.getValue();
				case subCategories.layout:
					return (
						this.settings.auth?.token &&
						(this.settings.options.profileCssEnabled.getValue() ||
							this.settings.options.activityCssEnabled.getValue() ||
							this.settings.options.layoutDesignerEnabled.getValue())
					);
				case subCategories.globalCss:
					return this.settings.options.globalCssEnabled.getValue();
			}
		}

		#createNavBtn(category, isActive, onClick) {
			const className = isActive ? "active" : null;
			const li = DOM.create("li", className, category);

			li.addEventListener("click", () => {
				onClick();
			});

			return li;
		}

		#renderUserTable(settingsContainer) {
			const tableContainer = DOM.create("div", "table #verified-user-table");

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

			const table = DOM.create("table");
			const head = DOM.create("thead");
			const headrow = DOM.create("tr", null, [
				DOM.create("th", null, "Username"),
				DOM.create("th", null, "Sign"),
				DOM.create("th", null, "Color"),
				DOM.create("th", null, "Other"),
			]);

			head.append(headrow);

			const body = DOM.create("tbody");

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

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

			const inputForm = DOM.create("form", null, null);

			inputForm.addEventListener("submit", (event) => {
				this.#handleVerifyUserForm(event, this.settings);
			});

			const inputFormLabel = DOM.create("label", null, "Add user");
			inputFormLabel.setAttribute("for", "void-verified-add-user");

			inputForm.append(inputFormLabel);
			inputForm.append(InputField("", () => {}, "#verified-add-user"));
			tableContainer.append(inputForm);

			settingsContainer.append(tableContainer);
		}

		#createUserRow(user) {
			const row = DOM.create("tr");
			const userLink = DOM.create("a", null, user.username);
			userLink.setAttribute(
				"href",
				`https://anilist.co/user/${user.username}/`
			);
			userLink.setAttribute("target", "_blank");
			row.append(DOM.create("td", null, userLink));

			const signInput = InputField(
				user.sign ?? "",
				(event) => {
					this.#updateUserOption(
						user.username,
						"sign",
						event.target.value
					);
				},
				"sign"
			);

			const signCell = DOM.create("td", null, signInput);
			signCell.append(
				this.#createUserCheckbox(
					user.enabledForUsername,
					user.username,
					"enabledForUsername",
					this.settings.options.enabledForUsername.getValue()
				)
			);

			row.append(DOM.create("th", null, signCell));

			const colorInputContainer = DOM.create("div");

			const colorInput = DOM.create("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 = DOM.create("button", null, "🔄");
			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 = DOM.create("td", null, colorInputContainer);
			row.append(colorCell);

			const quickAccessCheckbox = this.#createUserCheckbox(
				user.quickAccessEnabled,
				user.username,
				"quickAccessEnabled",
				this.settings.options.quickAccessEnabled.getValue()
			);

			const otherCell = DOM.create("td", null, quickAccessCheckbox);

			const cssEnabledCheckbox = this.#createUserCheckbox(
				user.onlyLoadCssFromVerifiedUser,
				user.username,
				"onlyLoadCssFromVerifiedUser",
				this.settings.options.onlyLoadCssFromVerifiedUser.getValue()
			);

			otherCell.append(cssEnabledCheckbox);

			row.append(otherCell);

			const deleteButton = DOM.create("button", null, "❌");
			deleteButton.addEventListener("click", () =>
				this.#removeUser(user.username)
			);
			row.append(DOM.create("th", null, 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 = DOM.create("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.renderSettingsUiContent();
			});

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

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

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

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

			const usernameInput = DOM.get("#void-verified-add-user");
			const username = usernameInput.value;
			settings.verifyUser(username);
			usernameInput.value = "";
			this.renderSettingsUiContent();
		}

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

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

		#renderSetting(setting, settingsContainer, settingKey, disabled = false) {
			if (setting.category === categories.hidden) {
				return;
			}
			const value = setting.getValue();
			const type = typeof value;

			const container = DOM.create("div");
			const input = DOM.create("input");
			input.setAttribute("id", settingKey);

			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 = DOM.create("label", null, setting.description);
			label.setAttribute("for", settingKey);
			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();

			if (!this.#shouldDisplaySubCategory(this.#activeSubCategory)) {
				this.#activeSubCategory = subCategories.users;
			}

			this.renderSettingsUiContent();
		}

		#renderCustomCssEditor(settingsContainer, cssHandler) {
			const cssName = cssHandler instanceof GlobalCSS ? "global" : "user";
			const container = DOM.create("div", "css-editor");
			const label = DOM.create("label", null, `Custom ${cssName} CSS`);
			label.setAttribute("for", `void-verified-${cssName}-css-editor`);
			container.append(label);

			const textarea = TextArea(cssHandler.css, (event) => {
				this.#handleCustomCssEditor(event, cssHandler);
			});
			container.append(textarea);

			if (cssName === "global") {
				const notice = DOM.create("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);
			} else {
				const publishButton = DOM.create("button", null, "Publish");
				publishButton.classList.add("button");
				publishButton.addEventListener("click", (event) =>
					this.#handlePublishCss(event, cssHandler)
				);

				const previewButton = DOM.create(
					"button",
					null,
					cssHandler.preview ? "Disable Preview" : "Enable Preview"
				);
				previewButton.classList.add("button");
				previewButton.addEventListener("click", () => {
					cssHandler.togglePreview();
					previewButton.innerText = cssHandler.preview
						? "Disable Preview"
						: "Enable Preview";
				});

				const resetButton = DOM.create("button", null, "Reset");
				resetButton.classList.add("button");
				resetButton.addEventListener("click", () => {
					if (window.confirm("Your changes will be lost.")) {
						cssHandler.getAuthUserCss().then(() => {
							textarea.value = cssHandler.css;
						});
					}
				});

				container.append(publishButton);
				container.append(previewButton);
				container.append(resetButton);
			}

			settingsContainer.append(container);
		}

		#handlePublishCss(event, cssHandler) {
			const btn = event.target;
			btn.innerText = "Publishing...";
			cssHandler.publishUserCss().then(() => {
				btn.innerText = "Publish";
			});
		}

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

		#renderImageHostSettings(settingsContainer) {
			const container = DOM.create("div");
			const header = DOM.create("label", null, "Image Host");
			header.setAttribute("for", "void-image-host-select");
			container.append(header);

			const imageHostService = new ImageHostService();
			const imageApiFactory = new ImageApiFactory();

			const select = DOM.create("select", "#image-host-select");
			for (const imageHost of Object.values(imageHosts)) {
				select.append(
					this.#createOption(
						imageHost,
						imageHost === imageHostService.getSelectedHost()
					)
				);
			}
			select.value = imageHostService.getSelectedHost();
			select.addEventListener("change", (event) => {
				imageHostService.setSelectedHost(event.target.value);
				this.renderSettingsUi();
			});
			container.append(select);

			const hostSpecificSettings = DOM.create("div");
			const imageHostApi = imageApiFactory.getImageHostInstance();
			hostSpecificSettings.append(imageHostApi.renderSettings(this));

			container.append(hostSpecificSettings);
			settingsContainer.append(container);
		}

		#createOption(value, selected = false) {
			const option = DOM.create("option", null, value);
			if (selected) {
				option.setAttribute("selected", true);
			}
			option.setAttribute("value", value);
			return option;
		}

		#creatAuthenticationSection(settingsContainer) {
			const isAuthenticated =
				this.settings.auth !== null &&
				new Date(this.settings.auth?.expires) > new Date();

			const clientId = 15519;

			const authenticationContainer = DOM.create("div");

			const header = DOM.create("h3", null, "Authorize VoidVerified");
			const description = DOM.create(
				"p",
				null,
				"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."
			);

			const list = DOM.create("ul");
			for (const option of Object.values(this.settings.options).filter(
				(o) => o.authRequired
			)) {
				list.append(DOM.create("li", null, option.description));
			}

			const authLink = DOM.create("a", "button", "Authenticate VoidVerified");
			authLink.setAttribute(
				"href",
				`https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`
			);

			const removeAuthButton = DOM.create(
				"button",
				null,
				"Revoke auth token"
			);
			removeAuthButton.classList.add("button");
			removeAuthButton.addEventListener("click", () => {
				this.settings.removeAuthToken();
				this.renderSettingsUiContent();
			});

			authenticationContainer.append(header);
			authenticationContainer.append(description);
			authenticationContainer.append(list);
			authenticationContainer.append(
				!isAuthenticated ? authLink : removeAuthButton
			);

			settingsContainer.append(authenticationContainer);
		}

		#checkAuthFromUrl() {
			const hash = window.location.hash.substring(1);
			if (!hash) {
				return;
			}

			const [path, token, type, expiress] = hash.split("&");

			if (path === "void_imgur") {
				const imgurConfig =
					new ImageHostService().getImageHostConfiguration(
						imageHosts.imgur
					);
				new ImgurAPI(imgurConfig).handleAuth();
			}
			if (path !== "void_auth") {
				return;
			}

			const expiresDate = new Date(
				new Date().getTime() + Number(expiress.split("=")[1]) * 1000
			);

			this.settings.saveAuthToken({
				token: token.split("=")[1],
				expires: expiresDate,
			});

			window.history.replaceState(
				null,
				"",
				"https://anilist.co/settings/developer"
			);
		}
	}

	class QuickAccess {
		settings;
		#quickAccessId = "void-verified-quick-access";
		#lastFetched;
		constructor(settings) {
			this.settings = settings;
			this.#lastFetched = new Date(
				localStorage.getItem("void-verified-last-fetched")
			);
		}

		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");
			title.setAttribute(
				"title",
				`Last updated at ${this.#lastFetched.toLocaleTimeString()}`
			);
			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);
		}

		clearBadge() {
			const username =
				window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];
			this.settings.updateUserOption(
				username,
				"quickAccessBadgeDisplay",
				false
			);
		}

		#createQuickAccessLink(user) {
			const container = document.createElement("a");
			container.setAttribute("class", "void-quick-access-item");
			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");

			if (
				(this.settings.options.quickAccessBadge.getValue() ||
					user.quickAccessBadge) &&
				user.quickAccessBadgeDisplay
			) {
				container.classList.add("void-quick-access-badge");
			}

			container.append(username);
			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
			);
		}
	}

	// Copyright (c) 2013 Pieroxy <[email protected]>
	// This work is free. You can redistribute it and/or modify it
	// under the terms of the WTFPL, Version 2
	// For more information see LICENSE.txt or http://www.wtfpl.net/
	//
	// For more information, the home page:
	// http://pieroxy.net/blog/pages/lz-string/testing.html
	//
	// LZ-based compression algorithm, version 1.4.4
	var LZString = (function () {
		// private property
		var f = String.fromCharCode;
		var keyStrBase64 =
			"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
		var keyStrUriSafe =
			"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
		var baseReverseDic = {};

		function getBaseValue(alphabet, character) {
			if (!baseReverseDic[alphabet]) {
				baseReverseDic[alphabet] = {};
				for (var i = 0; i < alphabet.length; i++) {
					baseReverseDic[alphabet][alphabet.charAt(i)] = i;
				}
			}
			return baseReverseDic[alphabet][character];
		}

		var LZString = {
			compressToBase64: function (input) {
				if (input == null) return "";
				var res = LZString._compress(input, 6, function (a) {
					return keyStrBase64.charAt(a);
				});
				switch (
					res.length % 4 // To produce valid Base64
				) {
					default: // When could this happen ?
					case 0:
						return res;
					case 1:
						return res + "===";
					case 2:
						return res + "==";
					case 3:
						return res + "=";
				}
			},

			decompressFromBase64: function (input) {
				if (input == null) return "";
				if (input == "") return null;
				return LZString._decompress(input.length, 32, function (index) {
					return getBaseValue(keyStrBase64, input.charAt(index));
				});
			},

			compressToUTF16: function (input) {
				if (input == null) return "";
				return (
					LZString._compress(input, 15, function (a) {
						return f(a + 32);
					}) + " "
				);
			},

			decompressFromUTF16: function (compressed) {
				if (compressed == null) return "";
				if (compressed == "") return null;
				return LZString._decompress(
					compressed.length,
					16384,
					function (index) {
						return compressed.charCodeAt(index) - 32;
					}
				);
			},

			//compress into uint8array (UCS-2 big endian format)
			compressToUint8Array: function (uncompressed) {
				var compressed = LZString.compress(uncompressed);
				var buf = new Uint8Array(compressed.length * 2); // 2 bytes per character

				for (var i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
					var current_value = compressed.charCodeAt(i);
					buf[i * 2] = current_value >>> 8;
					buf[i * 2 + 1] = current_value % 256;
				}
				return buf;
			},

			//decompress from uint8array (UCS-2 big endian format)
			decompressFromUint8Array: function (compressed) {
				if (compressed === null || compressed === undefined) {
					return LZString.decompress(compressed);
				} else {
					var buf = new Array(compressed.length / 2); // 2 bytes per character
					for (var i = 0, TotalLen = buf.length; i < TotalLen; i++) {
						buf[i] = compressed[i * 2] * 256 + compressed[i * 2 + 1];
					}

					var result = [];
					buf.forEach(function (c) {
						result.push(f(c));
					});
					return LZString.decompress(result.join(""));
				}
			},

			//compress into a string that is already URI encoded
			compressToEncodedURIComponent: function (input) {
				if (input == null) return "";
				return LZString._compress(input, 6, function (a) {
					return keyStrUriSafe.charAt(a);
				});
			},

			//decompress from an output of compressToEncodedURIComponent
			decompressFromEncodedURIComponent: function (input) {
				if (input == null) return "";
				if (input == "") return null;
				input = input.replace(/ /g, "+");
				return LZString._decompress(input.length, 32, function (index) {
					return getBaseValue(keyStrUriSafe, input.charAt(index));
				});
			},

			compress: function (uncompressed) {
				return LZString._compress(uncompressed, 16, function (a) {
					return f(a);
				});
			},
			_compress: function (uncompressed, bitsPerChar, getCharFromInt) {
				if (uncompressed == null) return "";
				var i,
					value,
					context_dictionary = {},
					context_dictionaryToCreate = {},
					context_c = "",
					context_wc = "",
					context_w = "",
					context_enlargeIn = 2, // Compensate for the first entry which should not count
					context_dictSize = 3,
					context_numBits = 2,
					context_data = [],
					context_data_val = 0,
					context_data_position = 0,
					ii;

				for (ii = 0; ii < uncompressed.length; ii += 1) {
					context_c = uncompressed.charAt(ii);
					if (
						!Object.prototype.hasOwnProperty.call(
							context_dictionary,
							context_c
						)
					) {
						context_dictionary[context_c] = context_dictSize++;
						context_dictionaryToCreate[context_c] = true;
					}

					context_wc = context_w + context_c;
					if (
						Object.prototype.hasOwnProperty.call(
							context_dictionary,
							context_wc
						)
					) {
						context_w = context_wc;
					} else {
						if (
							Object.prototype.hasOwnProperty.call(
								context_dictionaryToCreate,
								context_w
							)
						) {
							if (context_w.charCodeAt(0) < 256) {
								for (i = 0; i < context_numBits; i++) {
									context_data_val = context_data_val << 1;
									if (context_data_position == bitsPerChar - 1) {
										context_data_position = 0;
										context_data.push(
											getCharFromInt(context_data_val)
										);
										context_data_val = 0;
									} else {
										context_data_position++;
									}
								}
								value = context_w.charCodeAt(0);
								for (i = 0; i < 8; i++) {
									context_data_val =
										(context_data_val << 1) | (value & 1);
									if (context_data_position == bitsPerChar - 1) {
										context_data_position = 0;
										context_data.push(
											getCharFromInt(context_data_val)
										);
										context_data_val = 0;
									} else {
										context_data_position++;
									}
									value = value >> 1;
								}
							} else {
								value = 1;
								for (i = 0; i < context_numBits; i++) {
									context_data_val =
										(context_data_val << 1) | value;
									if (context_data_position == bitsPerChar - 1) {
										context_data_position = 0;
										context_data.push(
											getCharFromInt(context_data_val)
										);
										context_data_val = 0;
									} else {
										context_data_position++;
									}
									value = 0;
								}
								value = context_w.charCodeAt(0);
								for (i = 0; i < 16; i++) {
									context_data_val =
										(context_data_val << 1) | (value & 1);
									if (context_data_position == bitsPerChar - 1) {
										context_data_position = 0;
										context_data.push(
											getCharFromInt(context_data_val)
										);
										context_data_val = 0;
									} else {
										context_data_position++;
									}
									value = value >> 1;
								}
							}
							context_enlargeIn--;
							if (context_enlargeIn == 0) {
								context_enlargeIn = Math.pow(2, context_numBits);
								context_numBits++;
							}
							delete context_dictionaryToCreate[context_w];
						} else {
							value = context_dictionary[context_w];
							for (i = 0; i < context_numBits; i++) {
								context_data_val =
									(context_data_val << 1) | (value & 1);
								if (context_data_position == bitsPerChar - 1) {
									context_data_position = 0;
									context_data.push(
										getCharFromInt(context_data_val)
									);
									context_data_val = 0;
								} else {
									context_data_position++;
								}
								value = value >> 1;
							}
						}
						context_enlargeIn--;
						if (context_enlargeIn == 0) {
							context_enlargeIn = Math.pow(2, context_numBits);
							context_numBits++;
						}
						// Add wc to the dictionary.
						context_dictionary[context_wc] = context_dictSize++;
						context_w = String(context_c);
					}
				}

				// Output the code for w.
				if (context_w !== "") {
					if (
						Object.prototype.hasOwnProperty.call(
							context_dictionaryToCreate,
							context_w
						)
					) {
						if (context_w.charCodeAt(0) < 256) {
							for (i = 0; i < context_numBits; i++) {
								context_data_val = context_data_val << 1;
								if (context_data_position == bitsPerChar - 1) {
									context_data_position = 0;
									context_data.push(
										getCharFromInt(context_data_val)
									);
									context_data_val = 0;
								} else {
									context_data_position++;
								}
							}
							value = context_w.charCodeAt(0);
							for (i = 0; i < 8; i++) {
								context_data_val =
									(context_data_val << 1) | (value & 1);
								if (context_data_position == bitsPerChar - 1) {
									context_data_position = 0;
									context_data.push(
										getCharFromInt(context_data_val)
									);
									context_data_val = 0;
								} else {
									context_data_position++;
								}
								value = value >> 1;
							}
						} else {
							value = 1;
							for (i = 0; i < context_numBits; i++) {
								context_data_val = (context_data_val << 1) | value;
								if (context_data_position == bitsPerChar - 1) {
									context_data_position = 0;
									context_data.push(
										getCharFromInt(context_data_val)
									);
									context_data_val = 0;
								} else {
									context_data_position++;
								}
								value = 0;
							}
							value = context_w.charCodeAt(0);
							for (i = 0; i < 16; i++) {
								context_data_val =
									(context_data_val << 1) | (value & 1);
								if (context_data_position == bitsPerChar - 1) {
									context_data_position = 0;
									context_data.push(
										getCharFromInt(context_data_val)
									);
									context_data_val = 0;
								} else {
									context_data_position++;
								}
								value = value >> 1;
							}
						}
						context_enlargeIn--;
						if (context_enlargeIn == 0) {
							context_enlargeIn = Math.pow(2, context_numBits);
							context_numBits++;
						}
						delete context_dictionaryToCreate[context_w];
					} else {
						value = context_dictionary[context_w];
						for (i = 0; i < context_numBits; i++) {
							context_data_val =
								(context_data_val << 1) | (value & 1);
							if (context_data_position == bitsPerChar - 1) {
								context_data_position = 0;
								context_data.push(getCharFromInt(context_data_val));
								context_data_val = 0;
							} else {
								context_data_position++;
							}
							value = value >> 1;
						}
					}
					context_enlargeIn--;
					if (context_enlargeIn == 0) {
						context_enlargeIn = Math.pow(2, context_numBits);
						context_numBits++;
					}
				}

				// Mark the end of the stream
				value = 2;
				for (i = 0; i < context_numBits; i++) {
					context_data_val = (context_data_val << 1) | (value & 1);
					if (context_data_position == bitsPerChar - 1) {
						context_data_position = 0;
						context_data.push(getCharFromInt(context_data_val));
						context_data_val = 0;
					} else {
						context_data_position++;
					}
					value = value >> 1;
				}

				// Flush the last char
				while (true) {
					context_data_val = context_data_val << 1;
					if (context_data_position == bitsPerChar - 1) {
						context_data.push(getCharFromInt(context_data_val));
						break;
					} else context_data_position++;
				}
				return context_data.join("");
			},

			decompress: function (compressed) {
				if (compressed == null) return "";
				if (compressed == "") return null;
				return LZString._decompress(
					compressed.length,
					32768,
					function (index) {
						return compressed.charCodeAt(index);
					}
				);
			},

			_decompress: function (length, resetValue, getNextValue) {
				var dictionary = [],
					enlargeIn = 4,
					dictSize = 4,
					numBits = 3,
					entry = "",
					result = [],
					i,
					w,
					bits,
					resb,
					maxpower,
					power,
					c,
					data = { val: getNextValue(0), position: resetValue, index: 1 };

				for (i = 0; i < 3; i += 1) {
					dictionary[i] = i;
				}

				bits = 0;
				maxpower = Math.pow(2, 2);
				power = 1;
				while (power != maxpower) {
					resb = data.val & data.position;
					data.position >>= 1;
					if (data.position == 0) {
						data.position = resetValue;
						data.val = getNextValue(data.index++);
					}
					bits |= (resb > 0 ? 1 : 0) * power;
					power <<= 1;
				}

				switch ((bits)) {
					case 0:
						bits = 0;
						maxpower = Math.pow(2, 8);
						power = 1;
						while (power != maxpower) {
							resb = data.val & data.position;
							data.position >>= 1;
							if (data.position == 0) {
								data.position = resetValue;
								data.val = getNextValue(data.index++);
							}
							bits |= (resb > 0 ? 1 : 0) * power;
							power <<= 1;
						}
						c = f(bits);
						break;
					case 1:
						bits = 0;
						maxpower = Math.pow(2, 16);
						power = 1;
						while (power != maxpower) {
							resb = data.val & data.position;
							data.position >>= 1;
							if (data.position == 0) {
								data.position = resetValue;
								data.val = getNextValue(data.index++);
							}
							bits |= (resb > 0 ? 1 : 0) * power;
							power <<= 1;
						}
						c = f(bits);
						break;
					case 2:
						return "";
				}
				dictionary[3] = c;
				w = c;
				result.push(c);
				while (true) {
					if (data.index > length) {
						return "";
					}

					bits = 0;
					maxpower = Math.pow(2, numBits);
					power = 1;
					while (power != maxpower) {
						resb = data.val & data.position;
						data.position >>= 1;
						if (data.position == 0) {
							data.position = resetValue;
							data.val = getNextValue(data.index++);
						}
						bits |= (resb > 0 ? 1 : 0) * power;
						power <<= 1;
					}

					switch ((c = bits)) {
						case 0:
							bits = 0;
							maxpower = Math.pow(2, 8);
							power = 1;
							while (power != maxpower) {
								resb = data.val & data.position;
								data.position >>= 1;
								if (data.position == 0) {
									data.position = resetValue;
									data.val = getNextValue(data.index++);
								}
								bits |= (resb > 0 ? 1 : 0) * power;
								power <<= 1;
							}

							dictionary[dictSize++] = f(bits);
							c = dictSize - 1;
							enlargeIn--;
							break;
						case 1:
							bits = 0;
							maxpower = Math.pow(2, 16);
							power = 1;
							while (power != maxpower) {
								resb = data.val & data.position;
								data.position >>= 1;
								if (data.position == 0) {
									data.position = resetValue;
									data.val = getNextValue(data.index++);
								}
								bits |= (resb > 0 ? 1 : 0) * power;
								power <<= 1;
							}
							dictionary[dictSize++] = f(bits);
							c = dictSize - 1;
							enlargeIn--;
							break;
						case 2:
							return result.join("");
					}

					if (enlargeIn == 0) {
						enlargeIn = Math.pow(2, numBits);
						numBits++;
					}

					if (dictionary[c]) {
						entry = dictionary[c];
					} else {
						if (c === dictSize) {
							entry = w + w.charAt(0);
						} else {
							return null;
						}
					}
					result.push(entry);

					// Add w+entry[0] to the dictionary.
					dictionary[dictSize++] = w + entry.charAt(0);
					enlargeIn--;

					w = entry;

					if (enlargeIn == 0) {
						enlargeIn = Math.pow(2, numBits);
						numBits++;
					}
				}
			},
		};
		return LZString;
	})();

	var LZString$1 = LZString;

	class UserCSS {
		#settings;
		#currentActivity;
		#currentUser;
		css = "";
		preview = false;
		cssInLocalStorage = "void-verified-user-css";
		broadcastChannel;

		constructor(settings) {
			this.#settings = settings;
			if (
				this.#settings.auth?.token &&
				this.#settings.options.profileCssEnabled.getValue()
			) {
				const cssInLocalStorage = JSON.parse(
					localStorage.getItem(this.cssInLocalStorage)
				);
				if (cssInLocalStorage) {
					this.css = cssInLocalStorage.css;
					this.preview = cssInLocalStorage.preview;
				} else {
					this.getAuthUserCss();
				}
			}

			this.broadcastChannel = new BroadcastChannel("user-css");
			this.broadcastChannel.addEventListener("message", (event) =>
				this.#handleBroadcastMessage(event, this.#settings)
			);
		}

		async checkActivityCss() {
			if (
				!this.#settings.options.activityCssEnabled.getValue() ||
				!window.location.pathname.startsWith("/activity/")
			) {
				return;
			}

			const activityId = window.location.pathname.match(
				/^\/activity\/([^/]*)\/?/
			)[1];

			if (this.#currentActivity === activityId) {
				return;
			}

			this.#currentActivity = activityId;
			const anilistAPI = new AnilistAPI(this.#settings);
			const result = await anilistAPI.getActivityCss(activityId);
			const username =
				result.data.Activity.user?.name ??
				result.data.Activity.recipient?.name;

			const userColor =
				result.data.Activity.user?.options.profileColor ??
				result.data.Activity.recipient?.options.profileColor;
			const rgb = ColorFunctions.handleAnilistColor(userColor);

			const activityEntry = document.querySelector(
				".container > .activity-entry"
			);

			activityEntry.style.setProperty("--color-blue", rgb);
			activityEntry.style.setProperty("--color-blue-dim", rgb);

			if (username === this.#settings.anilistUser && this.preview) {
				this.#renderCss(this.css, "user-css");
				return;
			}

			if (username === this.#currentUser) {
				this.#clearGlobalCss();
				return;
			}
			new StyleHandler(this.#settings).clearStyles("user-css");

			if (!this.#shouldRenderCss(username)) {
				return;
			}

			const about =
				result.data.Activity.user?.about ??
				result.data.Activity.recipient?.about;

			const css = this.#decodeAbout(about)?.customCSS;
			if (css) {
				this.#renderCss(css, "user-css");
			}

			this.#currentUser = username;
		}

		resetCurrentActivity() {
			this.#currentActivity = null;
		}

		async checkUserCss() {
			if (
				!this.#settings.options.profileCssEnabled.getValue() ||
				!window.location.pathname.startsWith("/user/")
			) {
				return;
			}

			const username =
				window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];

			if (username === this.#currentUser) {
				return;
			}

			if (username === this.#settings.anilistUser && this.preview) {
				this.#renderCss(this.css, "user-css");
				return;
			}

			if (!this.#shouldRenderCss(username)) {
				new StyleHandler(this.#settings).clearStyles("user-css");
				return;
			}

			this.#currentUser = username;

			const anilistAPI = new AnilistAPI(this.#settings);
			const result = await anilistAPI.getUserAbout(username);
			const about = result.User.about;
			const css = this.#decodeAbout(about)?.customCSS;
			if (!css) {
				new StyleHandler(this.#settings).clearStyles("user-css");
			}
			this.#renderCss(css, "user-css");
		}

		resetCurrentUser() {
			this.#currentUser = null;
		}

		updateCss(css) {
			this.css = css;
			if (this.preview) {
				this.broadcastChannel.postMessage({ type: "css", css });
			}
			this.#saveToLocalStorage();
		}

		async publishUserCss() {
			const username = this.#settings.anilistUser;
			if (!username) {
				return;
			}

			const anilistAPI = new AnilistAPI(this.#settings);
			const result = await anilistAPI.getUserAbout(username);
			let about = result.User.about;
			let aboutJson = this.#decodeAbout(about);
			aboutJson.customCSS = this.css;
			const compressedAbout = LZString$1.compressToBase64(
				JSON.stringify(aboutJson)
			);

			const target = about.match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/)[1];
			if (target) {
				about = about.replace(target, compressedAbout);
			} else {
				about = `[](json${compressedAbout})` + about;
			}
			const mutationResult = await anilistAPI.saveUserAbout(about);
			return mutationResult?.errors === undefined;
		}

		togglePreview() {
			this.preview = !this.preview;
			this.broadcastChannel.postMessage({
				type: "preview",
				preview: this.preview,
			});
			this.#saveToLocalStorage();
		}

		async getAuthUserCss() {
			const anilistAPI = new AnilistAPI(this.#settings);
			const username = this.#settings.anilistUser;
			if (!username) {
				return;
			}
			const response = await anilistAPI.getUserAbout(username);
			const about = response.User.about;
			const css = this.#decodeAbout(about).customCSS;
			this.css = css;
			this.#saveToLocalStorage();
			return css;
		}

		#handleBroadcastMessage(event, settings) {
			switch (event.data.type) {
				case "css":
					this.#handlePreviewCssMessage(event.data.css, settings);
					break;
				case "preview":
					this.#handlePreviewToggleMessage(event.data.preview);
					break;
			}
		}

		#handlePreviewCssMessage(css, settings) {
			this.css = css;
			const hasUserCss = document.getElementById(
				"void-verified-user-css-styles"
			);
			if (hasUserCss) {
				new StyleHandler(settings).createStyleLink(css, "user-css");
			}
		}

		#handlePreviewToggleMessage(preview) {
			this.preview = preview;
			const hasUserCss = document.getElementById(
				"void-verified-user-css-styles"
			);
			if (!hasUserCss) {
				return;
			}

			this.resetCurrentUser();
			this.resetCurrentActivity();

			this.checkUserCss();
			this.checkActivityCss();
		}

		#saveToLocalStorage() {
			localStorage.setItem(
				this.cssInLocalStorage,
				JSON.stringify({
					css: this.css,
					preview: this.preview,
				})
			);
		}

		#shouldRenderCss(username) {
			const user = this.#settings.getUser(username);
			if (
				this.#settings.options.onlyLoadCssFromVerifiedUser.getValue() &&
				!this.#settings.isVerified(username)
			) {
				return false;
			}
			if (user?.onlyLoadCssFromVerifiedUser) {
				return true;
			}
			return !this.#userSpecificRenderingExists();
		}

		#userSpecificRenderingExists() {
			return this.#settings.verifiedUsers.some(
				(user) => user.onlyLoadCssFromVerifiedUser
			);
		}

		#renderCss(css, id) {
			if (!css) {
				return;
			}

			const styleHandler = new StyleHandler(this.#settings);
			styleHandler.createStyleLink(css, id);
			this.#clearGlobalCss();
		}

		#clearGlobalCss() {
			if (this.#settings.options.globalCssAutoDisable.getValue()) {
				new StyleHandler(this.#settings).clearStyles("global-css");
			}
		}

		#decodeAbout(about) {
			let json = (about || "").match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/);
			if (!json) {
				return null;
			}

			let jsonData;
			try {
				jsonData = JSON.parse(atob(json[1]));
			} catch (e) {
				jsonData = JSON.parse(LZString$1.decompressFromBase64(json[1]));
			}
			return jsonData;
		}
	}

	const markdownRegex = [
		{
			regex: /^##### (.*$)/gim,
			format: "<h5>$1</h5>",
		},
		{
			regex: /^#### (.*$)/gim,
			format: "<h4>$1</h4>",
		},
		{
			regex: /^### (.*$)/gim,
			format: "<h3>$1</h3>",
		},
		{
			regex: /^## (.*$)/gim,
			format: "<h2>$1</h2>",
		},
		{
			regex: /^# (.*$)/gim,
			format: "<h1>$1</h1>",
		},
		{
			regex: /\_\_(.*)\_\_/gim,
			format: "<strong>$1</strong>",
		},
		{
			regex: /\_(.*)\_/gim,
			format: "<em>$1</em>",
		},
		{
			regex: /(?:\r\n|\r|\n)/g,
			format: "<br>",
		},
		{
			regex: /\~~~(.*)\~~~/gim,
			format: "<center>$1</center>",
		},
		{
			regex: /\[([^\]]*)\]\(([^\)]+)\)/gi,
			format: "<a href='$2' target='_blank'>$1</a>",
		},
		{
			regex: /\~\!(.*)\!\~/gi,
			format: "<span class='markdown-spoiler'><span>$1</span></span>",
		},
		{
			regex: /img([0-9]+%?)\(([^\)]+)\)/g,
			format: "<img src='$2' width='$1' >",
		},
	];

	class Markdown {
		static parse(markdown) {
			let html = markdown;
			for (const parser of markdownRegex) {
				html = html.replace(parser.regex, parser.format);
			}

			return html;
		}
	}

	class Layout {
		avatar;
		banner;
		bio;
		color;
		donatorBadge;

		constructor(layout) {
			this.avatar = layout?.avatar ?? "";
			this.banner = layout?.banner ?? "";
			this.bio = layout?.bio ?? "";
			this.color = layout?.color ?? "";
			this.donatorBadge = layout?.donatorBadge ?? "";
		}
	}

	class LayoutDesigner {
		#settings;
		#layoutsInLocalStorage = "void-verified-layouts";
		#originalHtml;
		#broadcastChannel;
		#donatorTier = 0;
		#anilistSettings;
		#layout;
		#layouts = {
			selectedLayout: 0,
			preview: false,
			disableCss: false,
			layoutsList: [new Layout()],
		};

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

			this.#broadcastChannel = new BroadcastChannel("void-layouts");
			this.#broadcastChannel.addEventListener("message", (event) =>
				this.#handleBroadcastMessage(event)
			);

			const layouts = JSON.parse(
				localStorage.getItem(this.#layoutsInLocalStorage)
			);
			if (layouts) {
				this.#layouts = layouts;
				this.#layouts.layoutsList = layouts.layoutsList.map(
					(layout) => new Layout(layout)
				);
			}

			this.#anilistSettings = JSON.parse(localStorage.getItem("auth"));

			this.#donatorTier = this.#anilistSettings?.donatorTier;
			this.#layout = this.#getSelectedLayout();
		}

		renderLayoutPreview() {
			if (!this.#settings.options.layoutDesignerEnabled.getValue()) {
				return;
			}

			if (!window.location.pathname.startsWith("/user/")) {
				return;
			}
			const username =
				window.location.pathname.match(/^\/user\/([^/]*)\/?/)[1];

			if (username !== this.#settings.anilistUser || !this.#layouts.preview) {
				return;
			}

			this.#handleAvatar(this.#layout.avatar);
			this.#handleBanner(this.#layout.banner);
			this.#handleColor(this.#layout.color);
			this.#handleDonatorBadge(this.#layout.donatorBadge);
			this.#handleCss();
			this.#handleAbout(Markdown.parse(this.#layout.bio ?? ""));
		}

		#handleBroadcastMessage(event) {
			switch (event.data.type) {
				case "preview":
					this.#handlePreviewToggleMessage(event.data.preview);
					break;
				case "layout":
					this.#handleLayoutMessage(event.data.layout);
					break;
				case "css":
					this.#handleCssMessage(event.data.disableCss);
					break;
			}
		}

		#handlePreviewToggleMessage(preview) {
			this.#layouts.preview = preview;
			if (preview) {
				return;
			}

			this.#handleAvatar(this.#anilistSettings?.avatar?.large);
			this.#handleBanner(this.#anilistSettings?.bannerImage);
			this.#handleColor(this.#anilistSettings.options.profileColor);
			this.#handleDonatorBadge(this.#anilistSettings.donatorBadge);
			this.#layouts.disableCss = false;
			this.#handleCss();
			this.#handleAbout(this.#originalHtml);
		}

		#handleLayoutMessage(layout) {
			this.#layout = layout;
		}

		#handleCssMessage(disableCss) {
			this.#layouts.disableCss = disableCss;
		}

		#handleAvatar(avatar) {
			if (avatar === "") {
				return;
			}

			const avatarElement = DOM.get("img.avatar");
			avatarElement.src = avatar;

			const avatarLinks = DOM.getAll(
				`a.avatar[href*="${this.#settings.anilistUser}"]`
			);
			for (const avatarLink of avatarLinks) {
				avatarLink.style = `background-image: url(${avatar})`;
			}
		}

		#handleBanner(banner) {
			if (banner === "") {
				return;
			}

			const bannerElement = DOM.get(".banner");
			bannerElement.style = `background-image: url(${banner})`;
		}

		#handleColor(value) {
			let color;
			try {
				color = ColorFunctions.handleAnilistColor(value);
			} catch (err) {
				return;
			}

			const pageContent = DOM.get(".page-content > .user");
			pageContent.style.setProperty("--color-blue", color);
			pageContent.style.setProperty("--color-blue-dim", color);
		}

		#handleDonatorBadge(donatorText) {
			if (this.#donatorTier < 3 || donatorText === "") {
				return;
			}

			const donatorBadge = DOM.get(".donator-badge");
			donatorBadge.innerText = donatorText;
		}

		#handleCss() {
			if (this.#layouts.disableCss) {
				DOM.get("#void-verified-user-css-styles")?.setAttribute(
					"disabled",
					true
				);
			} else {
				DOM.get("#void-verified-user-css-styles")?.removeAttribute(
					"disabled"
				);
			}
		}

		#handleAbout(about) {
			const aboutContainer = DOM.get(".about .markdown");

			if (!this.#originalHtml) {
				this.#originalHtml = aboutContainer.innerHTML;
			}

			aboutContainer.innerHTML = about !== "" ? about : this.#originalHtml;
		}

		renderSettings(settingsUi) {
			if (!this.#settings.options.layoutDesignerEnabled.getValue()) {
				return "";
			}
			const container = DOM.create("div", "layout-designer-container");

			const header = DOM.create("h3", null, "Layout Designer");

			const imageSection = DOM.create("div");

			imageSection.append(
				this.#createImageField("avatar", this.#layout.avatar, settingsUi)
			);

			imageSection.append(
				this.#createImageField("banner", this.#layout.banner, settingsUi)
			);

			const imageUploadNote = Note(
				"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. "
			);

			imageUploadNote.append(
				DOM.create("br"),
				"Unfortunately AniList API does not support third parties uploading new avatars or banners. You have to upload them separately."
			);

			const colorSelection = this.#createColorSelection(settingsUi);

			const previewButton = Button(
				this.#layouts.preview ? "Disable Preview" : "Enable Preview",
				() => {
					this.#togglePreview(settingsUi);
				}
			);

			const cssButton = Button(
				this.#layouts.disableCss ? "Enable Css" : "Disable Css",
				() => {
					this.#toggleCss();
					cssButton.innerText = this.#layouts.disableCss
						? "Enable Css"
						: "Disable Css";
				}
			);

			const getAboutButton = Button("Reset About", () => {
				this.#getUserAbout(settingsUi);
			});

			container.append(header, imageSection, imageUploadNote, colorSelection);

			if (this.#donatorTier >= 3) {
				container.append(this.#createDonatorBadgeField(settingsUi));
			}

			container.append(this.#createAboutSection(settingsUi), getAboutButton);

			if (this.#settings.auth?.token) {
				const saveAboutButton = Button("Publish About", (event) => {
					this.#publishAbout(event, settingsUi);
				});
				container.append(saveAboutButton);
			}

			container.append(previewButton);

			if (this.#layouts.preview) {
				container.append(cssButton);
			}
			return container;
		}

		#createInputField(field, value, settingsUi) {
			const input = InputField(value, (event) => {
				this.#updateOption(field, event.target.value, settingsUi);
			});
			return input;
		}

		#createImageField(field, value, settingsUi) {
			const container = DOM.create("div", "layout-image-container");
			const header = DOM.create("h5", "layout-header", field);
			const display = DOM.create("div", `layout-image-display ${field}`);
			display.style.backgroundImage = `url(${value})`;
			const input = this.#createInputField(field, value, settingsUi);

			container.append(header, display, input);
			return container;
		}

		#createDonatorBadgeField(settingsUi) {
			const container = DOM.create("div", "layout-donator-badge-container");
			const donatorHeader = DOM.create(
				"h5",
				"layout-header",
				"Donator Badge"
			);
			const donatorInput = InputField(this.#layout.donatorBadge, (event) => {
				this.#updateOption("donatorBadge", event.target.value, settingsUi);
			});
			donatorInput.setAttribute("maxlength", 24);

			container.append(donatorHeader, donatorInput);

			if (
				this.#layout.donatorBadge !== this.#anilistSettings.donatorBadge &&
				this.#layout.donatorBadge !== "" &&
				this.#settings.auth?.token
			) {
				const publishButton = Button("Publish Donator Badge", (event) => {
					this.#publishDonatorText(event, settingsUi);
				});
				container.append(DOM.create("div", null, publishButton));
			}

			return container;
		}

		#createColorSelection(settingsUi) {
			const container = DOM.create("div", "layout-color-selection");

			const header = DOM.create("h5", "layout-header", "Color");
			container.append(header);

			for (const anilistColor of ColorFunctions.defaultColors) {
				container.append(this.#createColorButton(anilistColor, settingsUi));
			}

			if (this.#donatorTier >= 2) {
				const isDefaultColor = ColorFunctions.defaultColors.some(
					(color) => color === this.#layout.color
				);

				const colorInput = ColorPicker(
					isDefaultColor ? "" : this.#layout.color,
					(event) => {
						this.#updateOption("color", event.target.value, settingsUi);
					}
				);
				if (!isDefaultColor && this.#layout.color !== "") {
					colorInput.classList.add("active");
				}
				container.append(colorInput);
			}

			if (
				this.#settings.auth?.token &&
				this.#layout.color.toLocaleLowerCase() !==
					this.#anilistSettings?.options?.profileColor?.toLocaleLowerCase() &&
				this.#layout.color !== ""
			) {
				const publishButton = Button("Publish Color", (event) => {
					this.#publishColor(event, settingsUi);
				});
				container.append(DOM.create("div", null, publishButton));
			}

			return container;
		}

		#createAboutSection(settingsUi) {
			const container = DOM.create("div");
			const aboutHeader = DOM.create("h5", "layout-header", "About");
			const aboutInput = TextArea(this.#layout.bio, (event) => {
				this.#updateOption("bio", event.target.value, settingsUi);
			});
			const note = Note(
				"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."
			);

			container.append(aboutHeader, aboutInput, note);
			return container;
		}

		async #publishAbout(event, settingsUi) {
			const button = event.target;
			button.innerText = "Publishing...";

			try {
				const anilistAPI = new AnilistAPI(this.#settings);
				const result = await anilistAPI.getUserAbout(
					this.#settings.anilistUser
				);
				const currentAbout = result.User?.about;
				const about = this.#transformAbout(currentAbout, this.#layout.bio);

				await anilistAPI.saveUserAbout(about);
				settingsUi.renderSettingsUi();
			} catch {
				console.error("Failed to publish about");
			}
		}

		#transformAbout(currentAbout, newAbout) {
			const json = currentAbout.match(/^\[\]\(json([A-Za-z0-9+/=]+)\)/)[1];

			const about = `[](json${json})` + newAbout;
			return about;
		}

		async #publishColor(event, settingsUi) {
			const button = event.target;
			const color = this.#layout.color;
			button.innerText = "Publishing...";

			try {
				const anilistAPI = new AnilistAPI(this.#settings);
				const result = await anilistAPI.saveUserColor(color);
				const profileColor = result.UpdateUser?.options?.profileColor;
				this.#anilistSettings.options.profileColor = profileColor;
			} catch {
			} finally {
				settingsUi.renderSettingsUi();
			}
		}

		async #publishDonatorText(event, settingsUi) {
			const button = event.target;
			const donatorText = this.#layout.donatorBadge;
			button.innerText = "Publishing...";

			try {
				const anilistAPI = new AnilistAPI(this.#settings);
				const result = await anilistAPI.saveDonatorBadge(donatorText);
				const donatorBadge = result.UpdateUser?.donatorBadge;
				this.#anilistSettings.donatorBadge = donatorBadge;
			} catch {
			} finally {
				settingsUi.renderSettingsUi();
			}
		}

		async #getUserAbout(settingsUi) {
			if (
				this.#layout.bio !== "" &&
				!window.confirm(
					"Are you sure you want to reset about? Any changes will be lost."
				)
			) {
				return;
			}

			try {
				const anilistAPI = new AnilistAPI(this.#settings);
				const result = await anilistAPI.getUserAbout(
					this.#settings.anilistUser
				);
				const about = result.User.about;
				const clearedAbout = this.#removeJson(about);

				this.#updateOption("bio", clearedAbout, settingsUi);
			} catch (error) {
				console.error(error);
			}
		}

		#removeJson(about) {
			return about.replace(/^\[\]\(json([A-Za-z0-9+/=]+)\)/, "");
		}

		#createColorButton(anilistColor, settingsUi) {
			const button = DOM.create("div", "color-button");
			button.style.backgroundColor = `rgb(${ColorFunctions.handleAnilistColor(
			anilistColor
		)})`;

			button.addEventListener("click", () => {
				this.#updateOption("color", anilistColor, settingsUi);
			});

			if (this.#layout.color === anilistColor) {
				button.classList.add("active");
			}

			return button;
		}

		#updateOption(field, value, settingsUi) {
			this.#layout[field] = value;
			this.#updateLayout(this.#layout);
			settingsUi.renderSettingsUi();
		}

		#togglePreview(settingsUi) {
			this.#layouts.preview = !this.#layouts.preview;
			if (!this.#layouts.preview) {
				this.#layouts.disableCss = false;
			}
			this.#broadcastChannel.postMessage({
				type: "preview",
				preview: this.#layouts.preview,
			});
			this.#saveToLocalStorage();
			settingsUi.renderSettingsUi();
		}

		#toggleCss() {
			this.#layouts.disableCss = !this.#layouts.disableCss;
			this.#broadcastChannel.postMessage({
				type: "css",
				disableCss: this.#layouts.disableCss,
			});
			this.#saveToLocalStorage();
		}

		#getSelectedLayout() {
			return this.#layouts.layoutsList[this.#layouts.selectedLayout];
		}

		#updateLayout(layout) {
			this.#layouts.layoutsList[this.#layouts.selectedLayout] = layout;
			this.#saveToLocalStorage();
			this.#broadcastChannel.postMessage({
				type: "layout",
				layout: this.#layout,
			});
		}

		#saveToLocalStorage() {
			localStorage.setItem(
				this.#layoutsInLocalStorage,
				JSON.stringify(this.#layouts)
			);
		}
	}

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

			this.styleHandler = new StyleHandler(settings);
			this.globalCSS = new GlobalCSS(settings);
			this.userCSS = new UserCSS(settings);
			this.layoutDesigner = new LayoutDesigner(settings);

			this.settingsUi = new SettingsUserInterface(
				settings,
				this.styleHandler,
				this.globalCSS,
				this.userCSS,
				this.layoutDesigner
			);
			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();
			intervalScriptHandler.layoutDesigner.renderLayoutPreview();

			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.userCSS.checkUserCss();
				intervalScriptHandler.quickAccess.clearBadge();
				intervalScriptHandler.styleHandler.verifyProfile();
			} else {
				intervalScriptHandler.styleHandler.clearStyles("profile");
			}

			if (path.startsWith("/activity/")) {
				intervalScriptHandler.userCSS.checkActivityCss();
			}

			if (!path.startsWith("/activity/") && !path.startsWith("/user/")) {
				intervalScriptHandler.userCSS.resetCurrentActivity();
				intervalScriptHandler.userCSS.resetCurrentUser();
				intervalScriptHandler.styleHandler.clearStyles("user-css");
			}

			intervalScriptHandler.globalCSS.createCss();

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

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

	class PasteHandler {
		settings;

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

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

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

		async #handlePaste(event) {
			if (
				event.target.tagName !== "TEXTAREA" &&
				event.target.tagName !== "INPUT"
			) {
				return;
			}

			const clipboard = event.clipboardData.getData("text/plain").trim();
			let result = [];

			const file = event.clipboardData.items[0]?.getAsFile();
			if (file && this.settings.options.pasteImagesToHostService.getValue()) {
				event.preventDefault();
				result = await this.#handleImages(event);
			} else if (this.settings.options.pasteEnabled.getValue()) {
				event.preventDefault();
				const rows = clipboard.split("\n");

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

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

		async #handleImages(event) {
			const _files = event.clipboardData.items;
			if (this.#uploadInProgress) {
				return;
			}
			this.#uploadInProgress = true;
			document.body.classList.add("void-upload-in-progress");

			const imageApi = new ImageApiFactory().getImageHostInstance();

			const files = Object.values(_files).map((file) => file.getAsFile());
			const images = files.filter((file) => file.type.startsWith("image/"));

			try {
				const results = await Promise.all(
					images.map((image) => imageApi.uploadImage(image))
				);
				return results
					.filter((url) => url !== null)
					.map((url) => this.#handleRow(url, event));
			} catch (error) {
				console.error(error);
				return [];
			} finally {
				this.#uploadInProgress = false;
				document.body.classList.remove("void-upload-in-progress");
			}
		}

		#handleRow(row, event) {
			if (
				event.target.parentElement.classList.contains("void-css-editor") ||
				event.target.tagName === "INPUT"
			) {
				return 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 = /* css */ `
    a[href="/settings/developer" i]::after{content: " & Void"}
    .void-settings .void-nav ol {
        display: flex;
        margin: 8px 0px;
        padding: 0;
    }

    .void-nav {
        margin-top: 3rem;
    }

    .void-settings .void-nav li {
        list-style: none;
        display: block;
        color: rgb(var(--color-text));
        padding: 4px 8px;
        text-transform: capitalize;
        background: rgb(var(--color-foreground-blue));
        cursor: pointer;
        min-width: 50px;
        text-align: center;
        font-size: 1.4rem;
    }

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

    .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-settings-header {
        margin-top: 30px;
    }

    .void-settings .void-table table {
        border-collapse: collapse;
    }

    .void-settings .void-table :is(th, td) {
        padding: 2px 6px !important;
    }

    .void-settings .void-table :is(th, td):first-child {
        border-radius: 4px 0px 0px 4px;
    }

    .void-settings .void-table :is(th, td):last-child {
        border-radius: 0px 4px 4px 0px;
    }

    .void-settings .void-table tbody tr:hover {
        background-color: rgba(var(--color-foreground-blue), .7);
    }

    .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-textarea {
        width: 100%;
        height: 300px;
        min-height: 200px;
        resize: vertical;
        background: rgb(var(--color-foreground-blue));
        color: rgb(var(--color-text));
        padding: 4px;
        border-radius: 4px;
        border: 2px solid transparent;
        outline: none !important;
    }

    .void-textarea:focus {
        border: 2px solid rgb(var(--color-blue)) !important;
    }

    .void-layout-image-container {
        padding: 4px;
        display: inline-block;
    }

    .void-layout-image-container:first-child {
        width: 35%;
    }

    .void-layout-image-container:last-child {
        width: 65%;
    }

    .void-layout-header {
        text-transform: uppercase;
        margin-top: 2.2em;
        margin-bottom: .8em;
    }

    .void-layout-image-display {
        height: 140px;
        background-repeat: no-repeat;
        margin: auto;
        margin-bottom: 6px;
        border-radius: 4px;
    }



    .void-layout-image-display.void-banner {
        width: 100%;
        background-size: cover;
        background-position: 50% 50%;
        background-size: 
    }

    .void-layout-image-display.void-avatar {
        background-size: contain;
        width: 140px;
    }

    .void-layout-image-container input {
        width: 100%;
    }

    .void-layout-color-selection {
        margin-top: 10px;
        margin-bottom: 10px;
    }

    .void-layout-color-selection .void-color-button {
        width: 50px;
        height: 50px;
        display: inline-flex;
        border-radius: 4px;
        margin-right: 10px;
    }

    .void-layout-color-selection .void-color-button.active {
        border: 4px solid rgb(var(--color-text));
    }

    .void-layout-color-selection .void-color-picker-container.active {
        border: 2px solid rgb(var(--color-text));
    }

    .void-color-picker-container {
        display: inline-block;
        vertical-align: top;
        width: 75px;
        height: 50px;
        border: 2px solid transparent;
        border-radius: 4px;
        box-sizing: border-box;
    }

    .void-color-picker-container:has(:focus) {
        border: 2px solid rgb(var(--color-text));
    }

    .void-color-picker-input {
        width: 100%;
        height: 20px;
        background-color: rgba(var(--color-background), .6);
        padding: 1px;
        font-size: 11px;
        color: rgb(var(--color-text));
        outline: none;
        appearance: none;
        -webkit-appearance: none;
        text-align: center;
        border: unset;
        border-radius: 0px 0px 4px 4px;
    }

    .void-color-picker {
        /* width: 100%;;
        height: 50px; */
        block-size: 30px;
        border-width: 0px;
        padding: 0px;
        background-color: unset;
        inline-size: 100%;
        border-radius: 4px;
        appearance: none;
        vertical-align: top;
        padding-block: 0px;
        padding-inline: 0px;
        outline: none;
    }

    .void-color-picker::-webkit-color-swatch,
    .void-color-picker::-moz-color-swatch {
        border: none;
        border-radius: 4px;
    }

    .void-color-picker::-webkit-color-swatch-wrapper,
    .void-color-picker::-webkit-color-swatch-wrapper {
        padding: 0px;
        border-radius: 4px;
    }

    .void-input {
        background-color: rgba(var(--color-background), .6);
        padding: 4px 6px;
        color: rgb(var(--color-text));
        outline: none;
        appearance: none;
        -webkit-appearance: none;
        border: 2px solid transparent;
        border-radius: 4px;
        box-sizing: border-box;
    }

    a.void-link {
        color: rgb(var(--color-blue)) !important;
    }

    .void-input.void-sign {
        width: 75px;
        text-align: center;
        height: 20px;
        font-size: 14px;
    }

    .void-input:focus {
        border: 2px solid rgb(var(--color-blue));
    }

    .void-button {
        align-items: center;
        background: rgb(var(--color-blue));
        border-radius: 4px;
        color: rgb(var(--color-text-bright));
        cursor: pointer;
        display: inline-flex;
        font-size: 1.3rem;
        padding: 10px 15px;
        outline: none;
        appearance: none;
        -webkit-appearance: none;
        border: 0px solid rgb(var(--color-background));
        vertical-align: top;
        margin-top: 15px;
        margin-right: 10px;
    }
    
    .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;
        border-radius: 4px;
    }

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

    .void-quick-access-badge {
        position: relative;
    }

    .void-quick-access-badge::after {
        content: "New";
        background: rgb(var(--color-blue));
        border-radius: 10px;
        padding: 2px 4px;
        font-size: 9px;
        position: absolute;
        top: 2px;
        right: -10px;
        color: white;
    }

    .void-api-label {
        margin-right: 5px;
    }

    .void-api-key {
        width: 300px;
    }

    .void-notice {
        font-size: 11px;
        margin-top: 5px;
    }

    .void-upload-in-progress {
        cursor: wait;
    }
`;

	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();

	new ImgurAPI(
		new ImageHostService().getImageHostConfiguration(imageHosts.imgur)
	).refreshAuthToken();

	styleHandler.createStyleLink(styles, "script");

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

})();