// ==UserScript==
// @name VoidVerified
// @version 1.4.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';
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,
},
pasteImagesToHostService: {
defaultValue: false,
description:
"Upload image from the clipboard to image host (configure below).",
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 += `
div.wrap:has( div.header > a.name[href*="/${
user.username
}/"]) a,
div.wrap:has( div.details > a.name[href*="/${
user.username
}/"]) a
{
color: ${this.getUserColor(user)};
}
`;
}
if (colorUserReplies) {
this.highlightStyles += `
.reply:has(a.name[href*="/${user.username}/"]) a,
.reply:has(a.name[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);
}
}
}
const imageHosts = {
imgbb: "imgbb",
};
const imageHostConfiguration = {
selectedHost: imageHosts.imgbb,
configurations: {
imgbb: {
name: "imgbb",
apiKey: "",
},
},
};
class ImageHostService {
#configuration;
#localStorage = "void-verified-image-host-config";
constructor() {
const config = JSON.parse(localStorage.getItem(this.#localStorage));
if (!config) {
localStorage.setItem(
this.#localStorage,
JSON.stringify(imageHostConfiguration)
);
}
this.#configuration = config ?? imageHostConfiguration;
}
getImageHostConfiguration(host) {
return this.#configuration.configurations[host];
}
getSelectedHost() {
return this.#configuration.selectedHost;
}
setSelectedHost(host) {
this.#configuration.selectedHost = host;
localStorage.setItem(
this.#localStorage,
JSON.stringify(this.#configuration)
);
}
setImageHostConfiguration(host, config) {
this.#configuration.configurations[host] = config;
localStorage.setItem(
this.#localStorage,
JSON.stringify(this.#configuration)
);
}
}
class ImageHostBase {
conventToBase64(image) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onloadend = function (e) {
resolve({
fileName: this.name,
result: e.target.result,
error: e.target.error,
});
};
reader.readAsDataURL(image);
});
}
}
class ImgbbAPI extends ImageHostBase {
#url = "https://api.imgbb.com/1/upload";
#configuration;
constructor(configuration) {
super();
this.#configuration = configuration;
}
async uploadImage(image) {
const file = await this.conventToBase64(image);
if (!file.result) {
return;
}
if (!this.#configuration.apiKey) {
return;
}
const base64 = file.result.split("base64,")[1];
const settings = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
body: "image=" + encodeURIComponent(base64),
};
try {
const response = await fetch(
`${this.#url}?key=${this.#configuration.apiKey}`,
settings
);
const data = await response.json();
return data;
} catch (error) {
console.error(error);
return error;
}
}
renderSettings() {
const container = document.createElement("div");
const apiKeyInput = document.createElement("input");
apiKeyInput.setAttribute("type", "text");
const label = document.createElement("label");
label.append("API key");
label.setAttribute("class", "void-api-label");
container.append(label);
apiKeyInput.setAttribute("value", this.#configuration.apiKey);
apiKeyInput.addEventListener("change", (event) =>
this.#updateApiKey(event, this.#configuration)
);
apiKeyInput.setAttribute("class", "void-api-key");
container.append(apiKeyInput);
const note = document.createElement("div");
note.append("You need to get the API key from the following link: ");
note.setAttribute("class", "void-notice");
const apiKeyLink = document.createElement("a");
apiKeyLink.setAttribute("href", "https://api.imgbb.com/");
apiKeyLink.append("api.imgbb.com");
apiKeyLink.setAttribute("target", "_blank");
note.append(apiKeyLink);
container.append(note);
return container;
}
#updateApiKey(event, configuration) {
const apiKey = event.target.value;
const config = {
...configuration,
apiKey,
};
new ImageHostService().setImageHostConfiguration(config.name, config);
}
}
class ImageApiFactory {
getImageHostInstance() {
const imageHostService = new ImageHostService();
switch (imageHostService.getSelectedHost()) {
case imageHosts.imgbb:
return new ImgbbAPI(
imageHostService.getImageHostConfiguration(imageHosts.imgbb)
);
}
}
}
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);
const imageHostContainer = document.createElement("div");
imageHostContainer.setAttribute("id", "void-verified-image-host");
settingsContainer.append(imageHostContainer);
this.#renderImageHostSettings(imageHostContainer);
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);
}
#renderImageHostSettings(cont) {
const container =
cont ?? document.getElementById("void-verified-image-host");
const title = document.createElement("label");
title.append("Image Host");
container.append(title);
const imageHostService = new ImageHostService();
const imageApiFactory = new ImageApiFactory();
const select = document.createElement("select");
select.append(undefined);
for (const imageHost of Object.values(imageHosts)) {
select.append(
this.#createOption(
imageHost,
imageHost === imageHostService.getSelectedHost()
)
);
}
container.append(select);
const hostSpecificSettings = document.createElement("div");
const imageHostApi = imageApiFactory.getImageHostInstance();
hostSpecificSettings.append(imageHostApi.renderSettings());
container.append(hostSpecificSettings);
}
#createOption(value, selected = false) {
const option = document.createElement("option");
if (selected) {
option.setAttribute("selected", true);
}
option.setAttribute("value", value);
option.append(value);
return option;
}
}
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;
#uploadInProgress = 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;
// }
async #handlePaste(event) {
if (event.target.tagName !== "TEXTAREA") {
return;
}
const clipboard = event.clipboardData.getData("text/plain").trim();
const file = event.clipboardData.items[0]?.getAsFile();
let result = [];
if (
file &&
file.type.startsWith("image/") &&
this.settings.options.pasteImagesToHostService.getValue()
) {
event.preventDefault();
if (this.#uploadInProgress) {
return;
}
this.#uploadInProgress = true;
document.body.setAttribute("id", "void-upload-in-progess");
try {
const imageApi = new ImageApiFactory().getImageHostInstance();
const response = await imageApi.uploadImage(file);
result.push(this.#handleRow(response.data.url));
} catch (error) {
console.error(error);
} finally {
this.#uploadInProgress = false;
document.body.removeAttribute("id");
}
} else if (this.settings.options.pasteEnabled.getValue()) {
event.preventDefault();
const rows = clipboard.split("\n");
for (let row of rows) {
result.push(this.#handleRow(row));
}
} else {
return;
}
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 = /* css */ `
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;
border-radius: 4px;
}
.void-quick-access-username {
display: inline-block;
text-align: center;
bottom: -20px;
width: 100%;
word-break: break-all;
font-size: 1.2rem;
}
.void-api-label {
margin-right: 5px;
}
.void-api-key {
width: 300px;
}
.void-notice {
font-size: 11px;
margin-top: 5px;
}
#void-upload-in-progess {
cursor: wait;
}
`;
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.`);
})();