// ==UserScript== // @name VoidVerified // @version 1.3.0 // @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 // @downloadURL none // ==/UserScript== (function () { 'use strict'; const categories = { users: "users", paste: "paste", misc: "misc", }; const defaultSettings = { copyColorFromProfile: { defaultValue: true, description: "Copy user color from their profile.", category: categories.users, }, moveSubscribeButtons: { defaultValue: false, description: "Move activity subscribe button next to comments and likes.", category: categories.misc, }, hideLikeCount: { defaultValue: false, description: "Hide activity and reply like counts.", category: categories.misc, }, enabledForUsername: { defaultValue: true, description: "Display a verified sign next to usernames.", category: categories.users, }, enabledForProfileName: { defaultValue: false, description: "Display a verified sign next to a profile name.", category: categories.users, }, defaultSign: { defaultValue: "✔", description: "The default sign displayed next to a username.", category: categories.users, }, highlightEnabled: { defaultValue: true, description: "Highlight user activity with a border.", category: categories.users, }, highlightEnabledForReplies: { defaultValue: true, description: "Highlight replies with a border.", category: categories.users, }, highlightSize: { defaultValue: "5px", description: "Width of the highlight border.", category: categories.users, }, colorUserActivity: { defaultValue: false, description: "Color user activity links with user color.", category: categories.users, }, colorUserReplies: { defaultValue: false, description: "Color user reply links with user color.", category: categories.users, }, useDefaultHighlightColor: { defaultValue: false, description: "Use fallback highlight color when user color is not specified.", category: categories.users, }, defaultHighlightColor: { defaultValue: "#FFFFFF", description: "Fallback highlight color.", category: categories.users, }, globalCssEnabled: { defaultValue: false, description: "Enable custom global CSS.", category: categories.misc, }, globalCssAutoDisable: { defaultValue: true, description: "Disable global CSS when a profile has custom CSS.", category: categories.misc, }, quickAccessEnabled: { defaultValue: false, description: "Display quick access of users in home page.", category: categories.users, }, pasteEnabled: { defaultValue: false, description: "Automatically wrap pasted links and images with link and image tags.", category: categories.paste, }, pasteWrapImagesWithLink: { defaultValue: false, description: "Wrap images with a link tag.", category: categories.paste, }, pasteRequireKeyPress: { defaultValue: true, description: "Require an additional key to be pressed while pasting.", category: categories.paste, }, pasteKeybind: { defaultValue: "Shift", description: "The key to be pressed while pasting.", category: categories.paste, }, pasteImageWidth: { defaultValue: "420", description: "Width used when pasting images.", category: categories.paste, }, }; class ColorFunctions { static hexToRgb(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `${r}, ${g}, ${b}`; } static rgbToHex(rgb) { const [r, g, b] = rgb.split(","); const hex = this.generateHex(r, g, b); return hex; } static 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 AnilistAPI { apiQueryTimeoutInMinutes = 30; apiQueryTimeout = this.apiQueryTimeoutInMinutes * 60 * 1000; settings; constructor(settings) { this.settings = settings; } queryUserData() { this.#createUserQuery(); } async #createUserQuery() { let stopQueries = false; for (const user of this.#getUsersToQuery()) { if (stopQueries) { break; } stopQueries = this.#queryUser(user); } } #userQuery = ` query ($username: String) { User(name: $username) { name avatar { large } options { profileColor } } } `; #queryUser(user) { const variables = { username: user.username, }; const url = "https://graphql.anilist.co"; const options = { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ query: this.#userQuery, variables, }), }; let stopQueries = false; fetch(url, options) .then(this.#handleResponse) .then((data) => { const resultUser = data.User; this.settings.updateUserFromApi(user, resultUser); }) .catch((err) => { console.error(err); stopQueries = true; }); return stopQueries; } #getUsersToQuery() { if ( this.settings.options.copyColorFromProfile.getValue() || this.settings.options.quickAccessEnabled.getValue() ) { return this.#filterUsersByLastFetch(); } const users = this.settings.verifiedUsers.filter( (user) => user.copyColorFromProfile || user.quickAccessEnabled ); return this.#filterUsersByLastFetch(users); } #handleResponse(response) { return response.json().then((json) => { return response.ok ? json.data : Promise.reject(json); }); } #filterUsersByLastFetch(users = null) { const currentDate = new Date(); if (users) { return users.filter( (user) => !user.lastFetch || currentDate - new Date(user.lastFetch) > this.apiQueryTimeout ); } return this.settings.verifiedUsers.filter( (user) => !user.lastFetch || currentDate - new Date(user.lastFetch) > this.apiQueryTimeout ); } } class Option { value; defaultValue; description; category; constructor(option) { this.defaultValue = option.defaultValue; this.description = option.description; this.category = option.category; } getValue() { if (this.value === "") { return this.defaultValue; } return this.value ?? this.defaultValue; } } class Settings { localStorageUsers = "void-verified-users"; localStorageSettings = "void-verified-settings"; version = GM_info.script.version; verifiedUsers = []; options = {}; constructor() { this.verifiedUsers = JSON.parse(localStorage.getItem(this.localStorageUsers)) ?? []; const settingsInLocalStorage = JSON.parse(localStorage.getItem(this.localStorageSettings)) ?? {}; for (const [key, value] of Object.entries(defaultSettings)) { this.options[key] = new Option(value); } for (const [key, value] of Object.entries(settingsInLocalStorage)) { if (!this.options[key]) { continue; } this.options[key].value = value.value; } } verifyUser(username) { if ( this.verifiedUsers.find( (user) => user.username.toLowerCase() === username.toLowerCase() ) ) { return; } this.verifiedUsers.push({ username }); localStorage.setItem( this.localStorageUsers, JSON.stringify(this.verifiedUsers) ); const anilistAPI = new AnilistAPI(this); anilistAPI.queryUserData(); } 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) ); } updateUserFromApi(user, apiUser) { const newUser = this.#mapApiUser(user, apiUser); this.verifiedUsers = this.verifiedUsers.map((u) => u.username.toLowerCase() === user.username.toLowerCase() ? newUser : u ); localStorage.setItem( this.localStorageUsers, JSON.stringify(this.verifiedUsers) ); } #mapApiUser(user, apiUser) { let userObject = { ...user }; userObject.color = this.#handleAnilistColor( apiUser.options.profileColor ); userObject.username = apiUser.name; userObject.avatar = apiUser.avatar.large; userObject.lastFetch = new Date(); return userObject; } 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) ); } #defaultColors = [ "gray", "blue", "purple", "green", "orange", "red", "pink", ]; #defaultColorRgb = { gray: "103, 123, 148", blue: "61, 180, 242", purple: "192, 99, 255", green: "76, 202, 81", orange: "239, 136, 26", red: "225, 51, 51", pink: "252, 157, 214", }; #handleAnilistColor(color) { if (this.#defaultColors.includes(color)) { return this.#defaultColorRgb[color]; } return ColorFunctions.hexToRgb(color); } } 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 = ""; for (const user of this.settings.verifiedUsers) { if ( this.settings.options.enabledForUsername.getValue() || user.enabledForUsername ) { this.createUsernameCSS(user); } } if (this.settings.options.moveSubscribeButtons.getValue()) { 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.options.hideLikeCount.getValue()) { this.otherStyles += ` .like-wrap .count { display: none; } `; } } createHighlightStyles() { this.highlightStyles = ""; for (const user of this.settings.verifiedUsers) { if ( this.settings.options.highlightEnabled.getValue() || 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.options.highlightEnabledForReplies.getValue() || user.highlightEnabledForReplies ) { this.createHighlightCSS( user, `div.reply:has( a.name[href*="/${user.username}/" i] )` ); } this.#createActivityCss(user); } this.disableHighlightOnSmallCards(); } #createActivityCss(user) { const colorUserActivity = this.settings.options.colorUserActivity.getValue() ?? user.colorUserActivity; const colorUserReplies = this.settings.options.colorUserReplies.getValue() ?? user.colorUserReplies; if (colorUserActivity) { this.highlightStyles += ` .activity-entry :is(.details, .wrap):has(a[href*="/${ user.username }/"]) a { color: ${this.getUserColor(user)}; } `; } if (colorUserReplies) { this.highlightStyles += ` .reply:has(a[href*="/${user.username}/"]) a, .reply:has(a[href*="/${ user.username }/"]) .markdown-spoiler::before { color: ${this.getUserColor(user)}; } `; } } createUsernameCSS(user) { this.usernameStyles += ` a.name[href*="/${user.username}/" i]::after { content: "${ this.stringIsEmpty(user.sign) ?? this.settings.options.defaultSign.getValue() }"; color: ${this.getUserColor(user) ?? "rgb(var(--color-blue))"} } `; } createHighlightCSS(user, selector) { this.highlightStyles += ` ${selector} { margin-right: -${this.settings.options.highlightSize.getValue()}; border-right: ${this.settings.options.highlightSize.getValue()} 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.options.highlightEnabled.getValue()) { 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.options.enabledForProfileName.getValue()) { return; } const usernameHeader = document.querySelector("h1.name"); const username = usernameHeader.innerHTML.trim(); const user = this.settings.verifiedUsers.find( (u) => u.username.toLowerCase() === username.toLowerCase() ); if (!user) { this.clearProfileVerify(); return; } const profileStyle = ` .name-wrapper h1.name::after { content: "${ this.stringIsEmpty(user.sign) ?? this.settings.options.defaultSign.getValue() }" } `; this.profileLink = this.createStyleLink(profileStyle, "profile"); } 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.options.copyColorFromProfile.getValue() ) ) { 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.options.copyColorFromProfile.getValue()) ? `rgb(${user.color})` : undefined) ); } getDefaultHighlightColor() { if (this.settings.options.useDefaultHighlightColor.getValue()) { return this.settings.options.defaultHighlightColor.getValue(); } 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.options.globalCssEnabled.getValue()) { 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.options.globalCssAutoDisable.getValue()) { return true; } if (!window.location.pathname.startsWith("/user/")) { return true; } const profileCustomCss = document.getElementById( "customCSS-automail-styles" ); if (!profileCustomCss) { return true; } const shouldRender = profileCustomCss.innerHTML.trim().length === 0; return shouldRender; } } class ActivityHandler { settings; constructor(settings) { this.settings = settings; } moveAndDisplaySubscribeButton() { if (!this.settings.options.moveSubscribeButtons.getValue()) { 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"; #activeCategory = "all"; 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"); settingsContainer.setAttribute("class", "void-settings"); this.#renderSettingsHeader(settingsContainer); this.#renderCategories(settingsContainer); this.#renderOptions(settingsContainer); this.#renderUserTable(settingsContainer); this.#renderCustomCssEditor(settingsContainer); container.append(settingsContainer); } #renderOptions(settingsContainer) { const oldSettingsListContainer = document.getElementById("void-settings-list"); const settingsListContainer = oldSettingsListContainer ?? document.createElement("div"); settingsListContainer.innerHTML = ""; settingsListContainer.setAttribute("id", "void-settings-list"); settingsListContainer.setAttribute("class", "void-settings-list"); for (const [key, setting] of Object.entries(this.settings.options)) { if ( setting.category !== this.#activeCategory && this.#activeCategory !== "all" ) { continue; } this.#renderSetting(setting, settingsListContainer, key); } oldSettingsListContainer ?? settingsContainer.append(settingsListContainer); } removeSettingsUi() { const settings = document.querySelector("#voidverified-settings"); settings?.remove(); } #renderSettingsHeader(settingsContainer) { const headerContainer = document.createElement("div"); headerContainer.setAttribute("class", "void-settings-header"); const header = document.createElement("h1"); header.innerText = "VoidVerified settings"; const versionInfo = document.createElement("p"); versionInfo.append("Version: "); const versionNumber = document.createElement("span"); versionNumber.append(this.settings.version); versionInfo.append(versionNumber); headerContainer.append(header); headerContainer.append(versionInfo); settingsContainer.append(headerContainer); } #renderCategories(settingsContainer) { const oldNav = document.querySelector(".void-nav"); const nav = oldNav ?? document.createElement("nav"); nav.innerHTML = ""; nav.setAttribute("class", "void-nav"); const list = document.createElement("ol"); list.append(this.#createNavBtn("all")); for (const category of Object.values(categories)) { list.append(this.#createNavBtn(category)); } nav.append(list); oldNav ?? settingsContainer.append(nav); } #createNavBtn(category) { const li = document.createElement("li"); li.append(category); if (category === this.#activeCategory) { li.setAttribute("class", "void-active"); } li.addEventListener("click", () => { this.#activeCategory = category; this.#renderCategories(); this.#renderOptions(); }); return li; } #renderUserTable(settingsContainer) { const oldTableContainer = document.querySelector( "#void-verified-user-table" ); const tableContainer = oldTableContainer ?? document.createElement("div"); tableContainer.innerHTML = ""; tableContainer.setAttribute("class", "void-table"); tableContainer.setAttribute("id", "void-verified-user-table"); tableContainer.style = ` margin-top: 25px; `; 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")); headrow.append(this.#createCell("Other", "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.setAttribute("type", "text"); signInput.value = user.sign ?? ""; 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.options.enabledForUsername.getValue() ) ); row.append(this.#createCell(signCell)); const colorInputContainer = document.createElement("div"); const colorInput = document.createElement("input"); colorInput.setAttribute("type", "color"); colorInput.value = this.#getUserColorPickerColor(user); colorInput.addEventListener( "change", (event) => this.#handleUserColorChange(event, user.username), false ); colorInputContainer.append(colorInput); const resetColorBtn = document.createElement("button"); resetColorBtn.innerText = "🔄"; resetColorBtn.addEventListener("click", () => this.#handleUserColorReset(user.username) ); colorInputContainer.append(resetColorBtn); colorInputContainer.append( this.#createUserCheckbox( user.copyColorFromProfile, user.username, "copyColorFromProfile", this.settings.options.copyColorFromProfile.getValue() ) ); colorInputContainer.append( this.#createUserCheckbox( user.highlightEnabled, user.username, "highlightEnabled", this.settings.options.highlightEnabled.getValue() ) ); colorInputContainer.append( this.#createUserCheckbox( user.highlightEnabledForReplies, user.username, "highlightEnabledForReplies", this.settings.options.highlightEnabledForReplies.getValue() ) ); colorInputContainer.append( this.#createUserCheckbox( user.colorUserActivity, user.username, "colorUserActivity", this.settings.options.colorUserActivity.getValue() ) ); colorInputContainer.append( this.#createUserCheckbox( user.colorUserReplies, user.username, "colorUserReplies", this.settings.options.colorUserReplies.getValue() ) ); const colorCell = this.#createCell(colorInputContainer); row.append(colorCell); const quickAccessCheckbox = this.#createUserCheckbox( user.quickAccessEnabled, user.username, "quickAccessEnabled", this.settings.options.quickAccessEnabled.getValue() ); row.append(this.#createCell(quickAccessCheckbox)); 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.options.copyColorFromProfile.getValue()) ) { return ColorFunctions.rgbToHex(user.color); } if (this.settings.options.useDefaultHighlightColor.getValue()) { return this.settings.options.defaultHighlightColor.getValue(); } return ColorFunctions.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.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(); } #createCell(content, elementType = "td") { const cell = document.createElement(elementType); cell.append(content); return cell; } #renderSetting(setting, settingsContainer, settingKey, disabled = false) { const value = setting.getValue(); 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"); } else if (type === "string") { input.setAttribute("type", "text"); } if (disabled) { input.setAttribute("disabled", ""); } input.setAttribute("id", settingKey); if (settingKey === "pasteKeybind") { input.style.width = "80px"; input.addEventListener("keydown", (event) => this.#handleKeybind(event, settingKey, input) ); } else { 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; container.append(label); settingsContainer.append(container); } #handleKeybind(event, settingKey, input) { event.preventDefault(); const keybind = event.key; this.settings.saveSettingToLocalStorage(settingKey, keybind); input.value = keybind; } #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"); container.setAttribute("class", "void-css-editor"); const label = document.createElement("label"); label.innerText = "Custom Global CSS"; label.setAttribute("for", "void-verified-global-css-editor"); container.append(label); const textarea = document.createElement("textarea"); textarea.setAttribute("id", "void-verified-global-css-editor"); textarea.value = this.globalCSS.css; 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); } } class QuickAccess { settings; #quickAccessId = "void-verified-quick-access"; constructor(settings) { this.settings = settings; } renderQuickAccess() { if (this.#quickAccessRendered()) { return; } if ( !this.settings.options.quickAccessEnabled.getValue() && !this.settings.verifiedUsers.some((user) => user.quickAccessEnabled) ) { return; } const quickAccessContainer = document.createElement("div"); quickAccessContainer.setAttribute("class", "void-quick-access"); quickAccessContainer.setAttribute("id", this.#quickAccessId); const sectionHeader = document.createElement("div"); sectionHeader.setAttribute("class", "section-header"); const title = document.createElement("h2"); title.append("Quick Access"); sectionHeader.append(title); quickAccessContainer.append(sectionHeader); const quickAccessBody = document.createElement("div"); quickAccessBody.setAttribute("class", "void-quick-access-wrap"); for (const user of this.#getQuickAccessUsers()) { quickAccessBody.append(this.#createQuickAccessLink(user)); } quickAccessContainer.append(quickAccessBody); const section = document.querySelector( ".container > .home > div:nth-child(2)" ); section.insertBefore(quickAccessContainer, section.firstChild); } #createQuickAccessLink(user) { const container = document.createElement("a"); container.setAttribute("class", "void-quick-access-item"); const link = document.createElement("a"); container.setAttribute( "href", `https://anilist.co/user/${user.username}/` ); const image = document.createElement("div"); image.style.backgroundImage = `url(${user.avatar})`; image.setAttribute("class", "void-quick-access-pfp"); container.append(image); const username = document.createElement("div"); username.append(user.username); username.setAttribute("class", "void-quick-access-username"); container.append(username); container.append(link); return container; } #quickAccessRendered() { const quickAccess = document.getElementById(this.#quickAccessId); return quickAccess !== null; } #getQuickAccessUsers() { if (this.settings.options.quickAccessEnabled.getValue()) { return this.settings.verifiedUsers; } return this.settings.verifiedUsers.filter( (user) => user.quickAccessEnabled ); } } class IntervalScriptHandler { styleHandler; settingsUi; activityHandler; settings; globalCSS; quickAccess; 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); this.quickAccess = new QuickAccess(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(); intervalScriptHandler.quickAccess.renderQuickAccess(); } if (!path.startsWith("/settings/developer")) { intervalScriptHandler.settingsUi.removeSettingsUi(); } if (!intervalScriptHandler.hasPathChanged(path)) { return; } if (path.startsWith("/user/")) { intervalScriptHandler.styleHandler.verifyProfile(); intervalScriptHandler.styleHandler.copyUserColor(); } else { intervalScriptHandler.styleHandler.clearProfileVerify(); } if (path.startsWith("/settings/developer")) { intervalScriptHandler.settingsUi.renderSettingsUi(); } intervalScriptHandler.globalCSS.createCss(); } enableScriptIntervalHandling() { setInterval(() => { this.handleIntervalScripts(this); }, this.evaluationIntervalInSeconds * 1000); } } class PasteHandler { settings; #imageFormats = [ "jpg", "png", "gif", "webp", "apng", "avif", "jpeg", "svg", ]; #isKeyPressed = false; constructor(settings) { this.settings = settings; } setup() { window.addEventListener("keydown", (event) => { this.#handleKeybind(event); }); window.addEventListener("keyup", (event) => { this.#handleKeybind(event, false); }); window.addEventListener("paste", (event) => { this.#handlePaste(event); }); } #handleKeybind(event, isKeyDown = true) { if (this.settings.options.pasteKeybind.getValue() !== event.key) { return; } this.#isKeyPressed = isKeyDown; } #handlePaste(event) { const clipboard = event.clipboardData.getData("text/plain").trim(); if (!this.settings.options.pasteEnabled.getValue()) { return; } if ( this.settings.options.pasteRequireKeyPress.getValue() && !this.#isKeyPressed ) { return; } event.preventDefault(); const rows = clipboard.split("\n"); let result = []; for (let row of rows) { result.push(this.#handleRow(row)); } const transformedClipboard = result.join("\n\n"); window.document.execCommand("insertText", false, transformedClipboard); } #handleRow(row) { row = row.trim(); if ( this.#imageFormats.some((format) => row.toLowerCase().endsWith(format) ) ) { return this.#handleImg(row); } else if (row.toLowerCase().startsWith("http")) { return `[](${row})`; } else { return row; } } #handleImg(row) { const img = `img${this.settings.options.pasteImageWidth.getValue()}(${row})`; let result = img; if (this.settings.options.pasteWrapImagesWithLink.getValue()) { result = `[ ${img} ](${row})`; } return result; } } const styles = ` a[href="/settings/developer" i]::after{content: " & Void"} .void-settings .void-nav ol { display: flex; margin: 8px 0px; padding: 0; } .void-settings .void-nav li { list-style: none; display: block; color: white; padding: 3px 8px; text-transform: capitalize; background: black; cursor: pointer; min-width: 50px; text-align: center; font-size: 1.3rem; } .void-settings .void-nav li.void-active { background: rgb(var(--color-blue)); } .void-settings .void-nav li:first-child { border-radius: 4px 0px 0px 4px; } .void-settings .void-nav li:last-child { border-radius: 0px 4px 4px 0px; } .void-settings .void-nav li:hover { background: rgb(var(--color-blue)); } .void-settings .void-settings-header { margin-top: 30px; } .void-settings .void-table input[type="text"] { width: 100px; } .void-settings .void-table input[type="color"] { border: 0; height: 24px; width: 40px; padding: 0; background-color: unset; cursor: pointer; } .void-settings .void-table input[type="checkbox"] { margin-left: 3px; margin-right: 3px; } .void-settings .void-table button { background: unset; border: none; cursor: pointer; padding: 0; } .void-settings .void-table form { padding: 8px; display: flex; align-items: center; gap: 8px; } .void-settings .void-settings-header span { color: rgb(var(--color-blue)); } .void-settings .void-settings-list { display: flex; flex-direction: column; gap: 5px; } .void-settings .void-settings-list input[type="color"] { border: 0; height: 20px; width: 25px; padding: 0; background-color: unset; cursor: pointer; } .void-settings .void-settings-list input[type="text"] { width: 50px; } .void-settings .void-settings-list label { margin-left: 5px; } .void-settings .void-css-editor label { margin-top: 20px; fontSize: 2rem; display: inline-block; } .void-settings .void-css-editor textarea { width: 100%; height: 200px; resize: vertical; background: rgb(var(--color-foreground)); color: rgb(var(--color-text)); } .void-quick-access .void-quick-access-wrap { background: rgb(var(--color-foreground)); display: grid; grid-template-columns: repeat(auto-fill, 60px); grid-template-rows: repeat(auto-fill, 80px); gap: 15px; padding: 15px; margin-bottom: 25px; } .void-quick-access-item { display: inline-block; } .void-quick-access-pfp { background-size: contain; background-repeat: no-repeat; height: 60px; width: 60px; } .void-quick-access-username { display: inline-block; text-align: center; bottom: -20px; width: 100%; word-break: break-all; font-size: 1.2rem; } `; const settings = new Settings(); const styleHandler = new StyleHandler(settings); const intervalScriptHandler = new IntervalScriptHandler(settings); const anilistAPI = new AnilistAPI(settings); const pasteHandler = new PasteHandler(settings); styleHandler.refreshStyles(); intervalScriptHandler.enableScriptIntervalHandling(); anilistAPI.queryUserData(); pasteHandler.setup(); styleHandler.createStyleLink(styles, "script"); console.log(`VoidVerified ${settings.version} loaded.`); })();