VoidVerified

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

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

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 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.`);
})();