Greasy Fork

VoidVerified

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

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

// ==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.`);
})();