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