// ==UserScript==
// @name VoidVerified
// @version 1.1.1
// @namespace http://tampermonkey.net/
// @author voidnyan
// @description Display a verified sign next to user's name in 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';
class StyleHandler {
Settings;
usernameStyles = "";
highlightStyles = "";
otherStyles = "";
profileLink = this.createStyleLink("", "profile");
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 = `a[href="/settings/developer" i]::after{content: " & Void"}`;
for (const user of this.Settings.VerifiedUsers) {
if (
this.Settings.getOptionValue(
this.Settings.Options.enabledForUsername
) ||
user.enabledForUsername
) {
this.createUsernameCSS(user);
}
}
if (
this.Settings.getOptionValue(
this.Settings.Options.moveSubscribeButtons
)
) {
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.getOptionValue(this.Settings.Options.hideLikeCount)) {
this.otherStyles += `
.like-wrap .count {
display: none;
}
`;
}
}
createHighlightStyles() {
this.highlightStyles = "";
for (const user of this.Settings.VerifiedUsers) {
if (
this.Settings.getOptionValue(
this.Settings.Options.highlightEnabled
) ||
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.getOptionValue(
this.Settings.Options.highlightEnabledForReplies
) ||
user.highlightEnabledForReplies
) {
this.createHighlightCSS(
user,
`div.reply:has( a.name[href*="${user.username}" i] )`
);
}
}
this.disableHighlightOnSmallCards();
}
createUsernameCSS(user) {
this.usernameStyles += `
a.name[href*="${user.username}" i]::after {
content: "${
this.stringIsEmpty(user.sign) ??
this.Settings.getOptionValue(
this.Settings.Options.defaultSign
)
}";
color: ${
this.getUserColor(user) ?? "rgb(var(--color-blue))"
}
}
`;
}
createHighlightCSS(user, selector) {
this.highlightStyles += `
${selector} {
margin-right: -${this.Settings.getOptionValue(
this.Settings.Options.highlightSize
)};
border-right: ${this.Settings.getOptionValue(
this.Settings.Options.highlightSize
)} 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.getOptionValue(
this.Settings.Options.highlightEnabled
)
) {
return;
}
this.createHighlightStyles();
this.createStyleLink(this.highlightStyles, "highlight");
}
clearProfileVerify() {
this.profileLink.href =
"data:text/css;charset=UTF-8," + encodeURIComponent("");
}
clearStyles(id) {
const styles = document.getElementById(`void-verified-${id}-styles`);
styles?.remove();
}
verifyProfile() {
if (
!this.Settings.getOptionValue(
this.Settings.Options.enabledForProfileName
)
) {
return;
}
const usernameHeader = document.querySelector("h1.name");
const username = usernameHeader.innerHTML.trim();
const user = this.Settings.VerifiedUsers.find(
(u) => u.username === username
);
if (!user) {
this.clearProfileVerify();
return;
}
const profileStyle = `
h1.name::after {
content: "${
this.stringIsEmpty(user.sign) ??
this.Settings.getOptionValue(
this.Settings.Options.defaultSign
)
}"
}
`;
this.profileLink.href =
"data:text/css;charset=UTF-8," + encodeURIComponent(profileStyle);
}
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.getOptionValue(
this.Settings.Options.copyColorFromProfile
)
)
) {
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.getOptionValue(
this.Settings.Options.copyColorFromProfile
))
? `rgb(${user.color})`
: undefined)
);
}
getDefaultHighlightColor() {
if (
this.Settings.getOptionValue(
this.Settings.Options.useDefaultHighlightColor
)
) {
return this.Settings.getOptionValue(
this.Settings.Options.defaultHighlightColor
);
}
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.getOptionValue(
this.settings.Options.globalCssEnabled
)
) {
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.getOptionValue(
this.settings.Options.globalCssAutoDisable
)
) {
return true;
}
const profileCustomCss = document.getElementById(
"customCSS-automail-styles"
);
if (!profileCustomCss) {
return true;
}
const shouldRender = profileCustomCss.innerHTML.trim().length === 0;
return shouldRender;
}
}
class Settings {
LocalStorageUsers = "void-verified-users";
LocalStorageSettings = "void-verified-settings";
Version = "1.1.1";
Options = {
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.",
},
globalCssEnabled: {
defaultValue: false,
description: "Enable custom global CSS.",
},
globalCssAutoDisable: {
defaultValue: true,
description: "Disable global CSS when a profile has custom CSS.",
},
};
VerifiedUsers = [];
constructor() {
this.VerifiedUsers =
JSON.parse(localStorage.getItem(this.LocalStorageUsers)) ?? [];
const settingsInLocalStorage =
JSON.parse(localStorage.getItem(this.LocalStorageSettings)) ?? {};
for (const [key, value] of Object.entries(settingsInLocalStorage)) {
if (!this.Options[key]) {
continue;
}
this.Options[key].value = value.value;
}
}
getOptionValue(object) {
if (object.value === "") {
return object.defaultValue;
}
return object.value ?? object.defaultValue;
}
verifyUser(username) {
if (this.VerifiedUsers.find((user) => user.username === username)) {
return;
}
this.VerifiedUsers.push({ username });
localStorage.setItem(
this.LocalStorageUsers,
JSON.stringify(this.VerifiedUsers)
);
}
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)
);
}
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 ActivityHandler {
settings;
constructor(settings) {
this.settings = settings;
}
moveAndDisplaySubscribeButton() {
if (
!this.settings.getOptionValue(
this.settings.Options.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);
}
}
}
class SettingsUserInterface {
Settings;
StyleHandler;
globalCSS;
AnilistBlue = "120, 180, 255";
constructor(settings, styleHandler, globalCSS) {
this.Settings = settings;
this.StyleHandler = styleHandler;
this.globalCSS = globalCSS;
}
renderSettingsUi() {
const container = document.querySelector(
".settings.container > .content"
);
const settingsContainer = document.createElement("div");
settingsContainer.setAttribute("id", "voidverified-settings");
this.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(this.Settings.Options)) {
this.renderSetting(setting, settingsListContainer, key);
}
settingsContainer.append(settingsListContainer);
this.renderUserTable(settingsContainer);
this.renderCustomCssEditor(settingsContainer);
container.append(settingsContainer);
}
removeSettingsUi() {
const settings = document.querySelector("#voidverified-settings");
settings?.remove();
}
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(${this.AnilistBlue})`;
versionNumber.append(this.Settings.Version);
versionInfo.append(versionNumber);
headerContainer.append(header);
headerContainer.append(versionInfo);
settingsContainer.append(headerContainer);
}
renderUserTable(settingsContainer) {
const oldTableContainer = document.querySelector(
"#void-verified-user-table"
);
const tableContainer =
oldTableContainer ?? document.createElement("div");
tableContainer.innerHTML = "";
tableContainer.setAttribute("id", "void-verified-user-table");
const table = document.createElement("table");
const head = document.createElement("thead");
const headrow = document.createElement("tr");
headrow.append(this.createCell("Username", "th"));
headrow.append(this.createCell("Sign", "th"));
headrow.append(this.createCell("Color", "th"));
head.append(headrow);
const body = document.createElement("tbody");
for (const user of this.Settings.VerifiedUsers) {
body.append(this.createUserRow(user));
}
table.append(head);
table.append(body);
tableContainer.append(table);
const inputForm = document.createElement("form");
inputForm.addEventListener("submit", (event) =>
this.handleVerifyUserForm(event, this.Settings)
);
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);
oldTableContainer || settingsContainer.append(tableContainer);
}
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(this.createCell(userLink));
const signInput = document.createElement("input");
signInput.value = user.sign ?? "";
signInput.style.width = "100px";
signInput.addEventListener("input", (event) =>
this.updateUserOption(user.username, "sign", event.target.value)
);
const signCell = this.createCell(signInput);
signCell.append(
this.createUserCheckbox(
user.enabledForUsername,
user.username,
"enabledForUsername",
this.Settings.getOptionValue(
this.Settings.Options.enabledForUsername
)
)
);
row.append(this.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 = this.getUserColorPickerColor(user);
colorInput.addEventListener(
"change",
(event) => this.handleUserColorChange(event, user.username),
false
);
const colorInputContainer = document.createElement("span");
const colorCell = this.createCell(colorInput);
colorInputContainer.append(
this.createUserCheckbox(
user.copyColorFromProfile,
user.username,
"copyColorFromProfile",
this.Settings.getOptionValue(
this.Settings.Options.copyColorFromProfile
)
)
);
colorInputContainer.append(
this.createUserCheckbox(
user.highlightEnabled,
user.username,
"highlightEnabled",
this.Settings.getOptionValue(
this.Settings.Options.highlightEnabled
)
)
);
colorInputContainer.append(
this.createUserCheckbox(
user.highlightEnabledForReplies,
user.username,
"highlightEnabledForReplies",
this.Settings.getOptionValue(
this.Settings.Options.highlightEnabledForReplies
)
)
);
colorCell.append(colorInputContainer);
const resetColorBtn = document.createElement("button");
resetColorBtn.innerText = "🔄";
resetColorBtn.addEventListener("click", () =>
this.handleUserColorReset(user.username)
);
colorCell.append(resetColorBtn);
row.append(colorCell);
const deleteButton = document.createElement("button");
deleteButton.innerText = "❌";
deleteButton.addEventListener("click", () =>
this.removeUser(user.username)
);
row.append(this.createCell(deleteButton));
return row;
}
getUserColorPickerColor(user) {
if (user.colorOverride) {
return user.colorOverride;
}
if (
user.color &&
(user.copyColorFromProfile ||
this.Settings.getOptionValue(
this.Settings.Options.copyColorFromProfile
))
) {
return this.rgbToHex(user.color);
}
if (
this.Settings.getOptionValue(
this.Settings.Options.useDefaultHighlightColor
)
) {
return this.Settings.getOptionValue(
this.Settings.Options.defaultHighlightColor
);
}
return this.rgbToHex(this.AnilistBlue);
}
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) => {
this.updateUserOption(username, settingKey, event.target.checked);
this.refreshUserTable();
});
checkbox.style.marginLeft = "5px";
checkbox.title = this.Settings.Options[settingKey].description;
return checkbox;
}
handleUserColorReset(username) {
this.updateUserOption(username, "colorOverride", undefined);
this.refreshUserTable();
}
handleUserColorChange(event, username) {
const color = event.target.value;
this.updateUserOption(username, "colorOverride", color);
}
handleVerifyUserForm(event, settings) {
event.preventDefault();
const usernameInput = document.getElementById("voidverified-add-user");
const username = usernameInput.value;
settings.verifyUser(username);
usernameInput.value = "";
this.refreshUserTable();
}
refreshUserTable() {
const container = document.querySelector(
".settings.container > .content"
);
this.renderUserTable(container);
}
updateUserOption(username, key, value) {
this.Settings.updateUserOption(username, key, value);
this.StyleHandler.refreshStyles();
}
removeUser(username) {
this.Settings.removeUser(username);
this.refreshUserTable();
this.StyleHandler.refreshStyles();
}
verifyUser(username) {
this.Settings.verifyUser(username);
this.StyleHandler.refreshStyles();
}
createCell(content, elementType = "td") {
const cell = document.createElement(elementType);
cell.append(content);
return cell;
}
renderSetting(setting, settingsContainer, settingKey, disabled = false) {
const value = this.Settings.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) =>
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 = document.createElement("label");
label.setAttribute("for", settingKey);
label.innerText = setting.description;
label.style.marginLeft = "5px";
container.append(label);
settingsContainer.append(container);
}
handleOption(event, settingKey, type) {
const value =
type === "boolean" ? event.target.checked : event.target.value;
this.Settings.saveSettingToLocalStorage(settingKey, value);
this.StyleHandler.refreshStyles();
this.refreshUserTable();
}
renderCustomCssEditor(settingsContainer) {
const container = document.createElement("div");
const label = document.createElement("label");
label.innerText = "Custom Global CSS";
label.setAttribute("for", "void-verified-global-css-editor");
label.style.marginTop = "20px";
label.style.fontSize = "2rem";
label.style.display = "inline-block";
container.append(label);
const textarea = document.createElement("textarea");
textarea.setAttribute("id", "void-verified-global-css-editor");
textarea.value = this.globalCSS.css;
textarea.style.width = "100%";
textarea.style.height = "200px";
textarea.style.resize = "vertical";
textarea.style.background = "#14191f";
textarea.style.color = "white";
textarea.addEventListener("change", (event) => {
this.handleCustomCssEditor(event, this);
});
container.append(textarea);
const notice = document.createElement("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);
settingsContainer.append(container);
}
handleCustomCssEditor(event, settingsUi) {
const value = event.target.value;
settingsUi.globalCSS.updateCss(value);
}
rgbToHex(rgb) {
const [r, g, b] = rgb.split(",");
const hex = this.generateHex(r, g, b);
return hex;
}
generateHex(r, g, b) {
return (
"#" +
[r, g, b]
.map((x) => {
const hex = Number(x).toString(16);
return hex.length === 1 ? "0" + hex : hex;
})
.join("")
);
}
}
class IntervalScriptHandler {
styleHandler;
settingsUi;
activityHandler;
settings;
globalCSS;
constructor(settings) {
this.settings = settings;
this.styleHandler = new StyleHandler(settings);
this.globalCSS = new GlobalCSS(settings);
this.settingsUi = new SettingsUserInterface(
settings,
this.styleHandler,
this.globalCSS
);
this.activityHandler = new ActivityHandler(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();
if (path === "/home") {
intervalScriptHandler.styleHandler.refreshHomePage();
}
if (!path.startsWith("/settings/developer")) {
intervalScriptHandler.settingsUi.removeSettingsUi();
}
if (!intervalScriptHandler.hasPathChanged(path)) {
return;
}
intervalScriptHandler.styleHandler.clearProfileVerify();
intervalScriptHandler.globalCSS.createCss();
if (path.startsWith("/user/")) {
intervalScriptHandler.styleHandler.verifyProfile();
intervalScriptHandler.styleHandler.copyUserColor();
return;
}
if (path.startsWith("/settings/developer")) {
intervalScriptHandler.settingsUi.renderSettingsUi();
return;
}
}
enableScriptIntervalHandling() {
setInterval(() => {
this.handleIntervalScripts(this);
}, this.evaluationIntervalInSeconds * 1000);
}
}
const settings = new Settings();
const styleHandler = new StyleHandler(settings);
const intervalScriptHandler = new IntervalScriptHandler(settings);
styleHandler.refreshStyles();
intervalScriptHandler.enableScriptIntervalHandling();
console.log(`VoidVerified ${settings.Version} loaded.`);
})();