VoidVerified

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

目前為 2023-11-10 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         VoidVerified
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Display a verified sign next to user's name in AniList.
// @author       voidnyan
// @match        https://anilist.co/*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
	"use strict";
	const version = "1.0.0";
	const evaluationIntervalInSeconds = 1;
	const localStorageSettings = "void-verified-settings";
	const localStorageUsers = "void-verified-users";
	const anilistBlue = "120, 180, 255";

	let voidVerifiedSettings = {
		copyColorFromProfile: {
			defaultValue: true,
			description: "Copy user color from their profile when visited.",
		},
		moveSubscribeButtons: {
			defaultValue: false,
			description:
				"Move activity subscribe button next to comments and likes.",
		},
		hideLikeCount: {
			defaultValue: false,
			description: "Hide activity and reply like counts.",
		},
		enabledForUsername: {
			defaultValue: true,
			description: "Display a verified sign next to usernames.",
		},
		enabledForProfileName: {
			defaultValue: false,
			description: "Display a verified sign next to a profile name.",
		},
		defaultSign: {
			defaultValue: "✔",
			description: "The default sign displayed next to a username.",
		},
		highlightEnabled: {
			defaultValue: true,
			description: "Highlight user activity with a border.",
		},
		highlightEnabledForReplies: {
			defaultValue: true,
			description: "Highlight replies with a border.",
		},
		highlightSize: {
			defaultValue: "5px",
			description: "Width of the highlight border.",
		},
		useDefaultHighlightColor: {
			defaultValue: false,
			description:
				"Use fallback highlight color when user color is not specified.",
		},
		defaultHighlightColor: {
			defaultValue: "#FFFFFF",
			description: "Fallback highlight color.",
		},
	};

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

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

	let verifiedUsers =
		JSON.parse(localStorage.getItem(localStorageUsers)) ?? [];

	let usernameStyles = "";
	let highlightStyles = "";
	let otherStyles = "";

	refreshStyles();

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

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

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

			if (
				getOptionValue(voidVerifiedSettings.highlightEnabled) ||
				user.highlightEnabled
			) {
				createHighlightCSS(
					user,
					`div.wrap:has( div.header > a.name[href*="${user.username}"] )`
				);
				createHighlightCSS(
					user,
					`div.wrap:has( div.details > a.name[href*="${user.username}"] )`
				);
			}

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

		disableHighlightOnSmallCards();

		if (getOptionValue(voidVerifiedSettings.moveSubscribeButtons)) {
			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);
            }
            `;
		}

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

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

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

	function moveAndDisplaySubscribeButton() {
		if (!getOptionValue(voidVerifiedSettings.moveSubscribeButtons)) {
			return;
		}

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

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

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

	const profileLink = createStyleLink("", "profile");

	function refreshHomePage() {
		if (!getOptionValue(voidVerifiedSettings.highlightEnabled)) {
			return;
		}

		createStyleLink(highlightStyles, "highlight");
	}

	function verifyProfile() {
		if (!getOptionValue(voidVerifiedSettings.enabledForProfileName)) {
			return;
		}

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

		const user = verifiedUsers.find((u) => u.username === username);
		if (!user) {
			profileLink.href =
				"data:text/css;charset=UTF-8," + encodeURIComponent("");
			return;
		}

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

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

		if (!user) {
			return;
		}

		if (
			!(
				user.copyColorFromProfile ||
				getOptionValue(voidVerifiedSettings.copyColorFromProfile)
			)
		) {
			return;
		}

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

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

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

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

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

		settingsContainer.append(settingsListContainer);

		renderUserTable(settingsContainer);

		container.append(settingsContainer);
	}

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

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

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

		versionInfo.append(versionNumber);

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

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

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

		head.append(headrow);

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

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

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

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

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

		settingsContainer.append(tableContainer);
		oldTableContainer?.remove();
	}

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

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

		row.append(createCell(signCell));

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

		const colorInputContainer = document.createElement("span");
		// colorInputContainer.append(colorInput);

		const colorCell = createCell(colorInput);

		colorInputContainer.append(
			createUserCheckbox(
				user.copyColorFromProfile,
				user.username,
				"copyColorFromProfile",
				getOptionValue(voidVerifiedSettings.copyColorFromProfile)
			)
		);

		colorInputContainer.append(
			createUserCheckbox(
				user.highlightEnabled,
				user.username,
				"highlightEnabled",
				getOptionValue(voidVerifiedSettings.highlightEnabled)
			)
		);

		colorInputContainer.append(
			createUserCheckbox(
				user.highlightEnabledForReplies,
				user.username,
				"highlightEnabledForReplies",
				getOptionValue(voidVerifiedSettings.highlightEnabledForReplies)
			)
		);

		colorCell.append(colorInputContainer);

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

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

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

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

		if (
			user.color &&
			(user.copyColorFromProfile ||
				getOptionValue(voidVerifiedSettings.copyColorFromProfile))
		) {
			return rgbToHex(user.color);
		}

		if (getOptionValue(voidVerifiedSettings.useDefaultHighlightColor)) {
			return getOptionValue(voidVerifiedSettings.defaultHighlightColor);
		}

		return rgbToHex(anilistBlue);
	}

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

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

		checkbox.style.marginLeft = "5px";

		checkbox.title = voidVerifiedSettings[settingKey].description;
		return checkbox;
	}

	function handleUserColorReset(username) {
		updateUserOption(username, "colorOverride", undefined);
		refreshUserTable();
	}

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

	function handleVerifyUserForm(event) {
		event.preventDefault();

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

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

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

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

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

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

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

	function renderSetting(
		setting,
		settingsContainer,
		settingKey,
		disabled = false
	) {
		const value = getOptionValue(setting);
		const type = typeof value;

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

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

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

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

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

		container.append(input);

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

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

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

	function getDefaultHighlightColor() {
		if (getOptionValue(voidVerifiedSettings.useDefaultHighlightColor)) {
			return getOptionValue(voidVerifiedSettings.defaultHighlightColor);
		}
		return "rgb(var(--color-blue))";
	}

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

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

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

		voidVerifiedSettings[key].value = value;

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

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

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

	let currentPath = "";
	function hasPathChanged(path) {
		if (path === currentPath) {
			return false;
		}
		currentPath = path;
		return true;
	}

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

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

		moveAndDisplaySubscribeButton();

		if (path === "/home") {
			refreshHomePage();
			return;
		}

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

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

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

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

	setInterval(handleIntervalScripts, evaluationIntervalInSeconds * 1000);

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