您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
try to take over the world!
// ==UserScript== // @name Luogu Alias And Customize Tags // @namespace http://tampermonkey.net/ // @version 2025-01-23 15:17 // @description try to take over the world! // @author normalpcer // @match https://www.luogu.com.cn/* // @match https://www.luogu.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=luogu.com.cn // @grant none // @license MIT // ==/UserScript== /** * 自定义类名、LocalStorage 项的前缀。 * 为了避免与其他插件重名。 */ const Prefix = "normalpcer-alias-tags-"; const Cooldown = 1000; // 两次修改的冷却时间(毫秒) const Colors = new Map(Object.entries({ purple: "#9d3dcf", red: "#fe4c61", orange: "#f39c11", green: "#52c41a", blue: "#3498db", gray: "#bfbfbf", })); /** * 用于确定一个用户 */ class UserIdentifier { uid; // 0 为无效项 username; constructor(uid, username) { this.uid = uid; this.username = username; } static fromUid(uid) { console.log(`UserIdentifier::fromUid(${uid})`); let res = uidToIdentifier.get(uid); if (res !== undefined) { return new Promise((resolve, _) => { resolve(res); }); } // 否则,直接通过 API 爬取 const APIBase = "/api/user/search?keyword="; let api = APIBase + uid.toString(); console.log("api: " + api); let xml = new XMLHttpRequest(); xml.open("GET", api); return new Promise((resolve, reject) => { xml.addEventListener("loadend", () => { console.log("status: " + xml.status); console.log("response: " + xml.responseText); if (xml.status === 200) { let json = JSON.parse(xml.responseText); let users = json["users"]; if (users.length !== 1) { reject(); } else { let uid = users[0]["uid"]; let username = users[0]["name"]; let identifier = new UserIdentifier(uid, username); uidToIdentifier.set(uid, identifier); usernameToIdentifier.set(username, identifier); resolve(identifier); } } else { reject(); } }); xml.send(); }); } static fromUsername(username) { console.log(`UserIdentifier::fromUsername(${username})`); let res = usernameToIdentifier.get(username); if (res !== undefined) { return new Promise((resolve) => { resolve(res); }); } const APIBase = "/api/user/search?keyword="; let api = APIBase + username; let xml = new XMLHttpRequest(); xml.open("GET", api); return new Promise((resolve, reject) => { xml.addEventListener("loadend", () => { console.log("response: ", xml.responseText); if (xml.status === 200) { let json = JSON.parse(xml.responseText); let users = json["users"]; if (users.length !== 1) { reject(); } else { let uid = users[0]["uid"]; let username = users[0]["name"]; let identifier = new UserIdentifier(uid, username); uidToIdentifier.set(uid, identifier); usernameToIdentifier.set(username, identifier); resolve(identifier); } } else { reject(); } }); xml.send(); }); } /** * 通过用户给定的字符串,自动判断类型并创建 UserIdentifier 对象。 * @param s 新创建的 UserIdentifier 对象 */ static fromAny(s) { // 保证:UID 一定为数字 // 忽略首尾空格,如果是一段完整数字,视为 UID if (s.trim().match(/^\d+$/)) { return UserIdentifier.fromUid(parseInt(s)); } else { return UserIdentifier.fromUsername(s); } } dump() { return { uid: this.uid, username: this.username }; } } let uidToIdentifier = new Map(); let usernameToIdentifier = new Map(); class UsernameAlias { id; newName; constructor(id, newName) { this.id = id; this.newName = newName; } /** * 在当前文档中应用别名。 * 当前采用直接 dfs 全文替换的方式。 */ apply() { function dfs(p, alias) { // 进行一些特判。 /** * 如果当前为私信页面,那么位于顶栏的用户名直接替换会出现问题。 * 在原名的后面用括号标注别名,并且在修改时删除别名 */ if (window.location.href.includes("/chat")) { if (p.classList.contains("title")) { let a_list = p.querySelectorAll(`a[href*='/user/${alias.id.uid}']`); if (a_list.length === 1) { let a = a_list[0]; if (a.children.length !== 1) return; let span = a.children[0]; if (!(span instanceof HTMLSpanElement)) return; if (span.innerText.includes(alias.id.username)) { if (span.getElementsByClassName(Prefix + "alias").length !== 0) return; // 尝试在里面添加一个 span 标注别名 let alias_span = document.createElement("span"); alias_span.classList.add(Prefix + "alias"); alias_span.innerText = `(${alias.newName})`; span.appendChild(alias_span); // 在真实名称修改时删除别名 let observer = new MutationObserver(() => { span.removeChild(alias_span); observer.disconnect(); }); observer.observe(span, { characterData: true, childList: true, subtree: true, attributes: true, }); } } return; } } if (p.children.length == 0) { // 到达叶子节点,进行替换 if (!p.innerText.includes(alias.id.username)) { return; // 尽量不做修改 } p.innerText = p.innerText.replaceAll(alias.id.username, alias.newName); } else { for (let element of p.children) { if (element instanceof HTMLElement) { dfs(element, alias); } } } } dfs(document.body, this); } dump() { return { uid: this.id.uid, newName: this.newName }; } } let aliases = new Map(); let cache = new Map(); // 每个 UID 的缓存 class SettingBoxItem { } class SettingBoxItemText { element = null; placeholder; constructor(placeholder) { this.placeholder = placeholder; } createElement() { if (this.element !== null) { throw "SettingBoxItemText::createElement(): this.element is not null."; } let new_element = document.createElement("input"); new_element.placeholder = this.placeholder; this.element = new_element; return new_element; } getValue() { if (this.element instanceof HTMLInputElement) { return this.element.value; } else { throw "SettingBoxItemText::getValue(): this.element is not HTMLInputElement."; } } } /** * 位于主页的设置块 */ class SettingBox { title; items = []; placed = false; // 已经被放置 callback = null; // 确定之后调用的函数 constructor(title) { this.title = title; } /** * 使用一个新的函数处理用户输入 * @param func 用作处理的函数 */ handle(func = null) { this.callback = func; } /** * 尝试在当前文档中放置设置块。 * 如果已经存在,则不会做任何事。 */ place() { if (this.placed) return; let parent = document.getElementById(Prefix + "boxes-parent"); if (!(parent instanceof HTMLDivElement)) return; let new_element = document.createElement("div"); new_element.classList.add("lg-article"); // 标题元素 let title_element = document.createElement("h2"); title_element.innerText = this.title; // "收起"按钮 let fold_button = document.createElement("span"); fold_button.innerText = "[收起]"; fold_button.style.marginLeft = "0.5em"; fold_button.setAttribute("fold", "0"); title_element.appendChild(fold_button); new_element.appendChild(title_element); // 依次创建接下来的询问 let queries = document.createElement("div"); for (let x of this.items) { queries.appendChild(x.createElement()); } // “确定”按钮 let confirm_button = document.createElement("input"); confirm_button.type = "button"; confirm_button.value = "确定"; confirm_button.classList.add("am-btn", "am-btn-primary", "am-btn-sm"); if (this.callback !== null) { let callback = this.callback; let args = this.items; confirm_button.onclick = () => callback(args); } queries.appendChild(confirm_button); new_element.appendChild(queries); fold_button.onclick = () => { if (fold_button.getAttribute("fold") === "0") { fold_button.innerText = "[展开]"; fold_button.setAttribute("fold", "1"); queries.style.display = "none"; } else { fold_button.innerText = "[收起]"; fold_button.setAttribute("fold", "0"); queries.style.display = "block"; } }; parent.insertBefore(new_element, parent.children[0]); // 插入到开头 this.placed = true; } } /** * 用户自定义标签 */ class UserTag { id; tag; constructor(id, tag) { this.id = id; this.tag = tag; } /** * 应用一个标签 */ apply() { // 寻找所有用户名出现的位置 // 对于页面中的所有超链接,如果链接内容含有 "/user/uid",且为叶子节点,则认为这是一个用户名 let feature = `/user/${this.id.uid}`; let selector = `a[href*='${feature}']`; if (window.location.href.includes(feature)) { selector += ", .user-name > span"; } let links = document.querySelectorAll(selector); for (let link of links) { if (!(link instanceof HTMLElement)) { console.log("UserTag::apply(): link is not HTMLElement."); continue; } // 已经放置过标签 if (link.parentElement?.getElementsByClassName(Prefix + "customized-tag").length !== 0) { // console.log("UserTag::apply(): already placed tag."); continue; } if (link.children.length === 1 && link.children[0] instanceof HTMLSpanElement) { // 特别地,仅有一个 span 是允许的 link = link.children[0]; } else if (link.children.length !== 0) { // 否则,要求 link 为叶子节点 // console.log("UserTag::apply(): link is not a leaf node."); continue; } if (!(link instanceof HTMLElement)) continue; // 让 Typescript 认为 link 是 HTMLElement // console.log(link); // 获取用户名颜色信息 // - 如果存在颜色属性,直接使用 // - 否则,尝试通过 class 推断颜色 let existsColorStyle = false; let color = link.style.color; let colorHex = ""; let colorName = ""; // 通过 class 推断的颜色名 if (color !== "") { existsColorStyle = true; // 尝试解析十六进制颜色或者 rgb 颜色 if (color.startsWith("#")) { colorHex = color; } else if (color.startsWith("rgb")) { let rgb = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (rgb !== null) { // 十进制转为十六进制 const f = (x) => parseInt(x).toString(16).padStart(2, "0"); colorHex = "#" + f(rgb[1]) + f(rgb[2]) + f(rgb[3]); } else { throw "UserTag::apply(): cannot parse color " + color; } } else { throw "UserTag::apply(): cannot parse color " + color; } } else { // 尝试从类名推断 let classList = link.classList; for (let x of classList) { if (x.startsWith("lg-fg-")) { colorName = x.substring(6); break; } } } if (!existsColorStyle && colorName === "") { // 尝试使用缓存中的颜色 if (cache.has(this.id.uid)) { let data = cache.get(this.id.uid)?.get("color"); console.log("data", data); if (data !== undefined && typeof data === "string") { colorHex = data; existsColorStyle = true; } } } // 完全无法推断,使用缺省值灰色 if (!existsColorStyle && colorName === "") { let color = Colors.get("gray"); if (color !== undefined) { colorHex = color; } else { throw "UserTag::apply(): cannot find color gray."; } } console.log(`tag ${this.tag} for ${this.id.uid}. colorHex = ${colorHex}, colorName = ${colorName}`); // 生成标签 let new_element = document.createElement("span"); new_element.classList.add("lg-bg-" + colorName); new_element.classList.add("am-badge"); new_element.classList.add("am-radius"); new_element.classList.add(Prefix + "customized-tag"); new_element.innerText = this.tag; if (!existsColorStyle) { let color = Colors.get(colorName); if (color !== undefined) { colorHex = color; } else { throw "UserTag::apply(): cannot find color " + colorName; } } new_element.style.setProperty("background", colorHex, "important"); new_element.style.setProperty("border-color", colorHex, "important"); new_element.style.setProperty("color", "#fff", "important"); // 特别地,如果 innerText 不以空格结尾,添加 0.3em 的 margin-left if (!link.innerText.endsWith(" ")) { new_element.style.marginLeft = "0.3em"; } // 插入到文档中 if (!(link instanceof HTMLAnchorElement)) { if (link.parentElement instanceof HTMLAnchorElement) { link = link.parentElement; } } if (!(link instanceof HTMLElement)) { throw "UserTag::apply(): link is not HTMLElement before insertion."; } let parent = link.parentElement; if (parent === null) { throw "UserTag::apply(): cannot find parent."; } // 在 link 之后 if (parent.lastChild === link) { parent.appendChild(new_element); } else { parent.insertBefore(new_element, link.nextSibling); } // 在原始元素被修改时删除标签 // 仍然是为了适配私信界面 let observer = new MutationObserver(() => { observer.disconnect(); new_element.remove(); }); observer.observe(link, { childList: true, characterData: true, subtree: true, attributes: true, }); // 在缓存中保存颜色信息 if (!cache.has(this.id.uid)) cache.set(this.id.uid, new Map()); cache.get(this.id.uid)?.set("color", colorHex); saveCache(); } } dump() { return { uid: this.id.uid, tag: this.tag }; } } let tags = new Map(); /** * 从 localStorage 加载/存储数据 */ const StorageKeyName = Prefix + "alias_tag_data"; const StorageCacheKeyName = Prefix + "alias_tag_cache"; function load() { let json = localStorage.getItem(StorageKeyName); if (json !== null) { let data = JSON.parse(json); let _identifiers = data.identifiers; if (_identifiers instanceof Array) { for (let x of _identifiers) { let uid = x.uid; let username = x.username; // 判断 uid 为数字,username 为字符串 if (typeof uid === "number" && typeof username === "string") { let identifier = new UserIdentifier(uid, username); uidToIdentifier.set(uid, identifier); usernameToIdentifier.set(username, identifier); } } } let _aliases = data.aliases; if (_aliases instanceof Array) { for (let x of _aliases) { let uid = x.uid; let newName = x.newName; if (typeof uid === "number" && typeof newName === "string") { let identifier = uidToIdentifier.get(uid); if (identifier !== undefined) { aliases.set(identifier, new UsernameAlias(identifier, newName)); } } } } let _tags = data.tags; if (_tags instanceof Array) { for (let x of _tags) { let uid = x.uid; let tag = x.tag; if (typeof uid === "number" && typeof tag === "string") { let identifier = uidToIdentifier.get(uid); if (identifier !== undefined) { tags.set(identifier, new UserTag(identifier, tag)); } } } } } let json_cache = localStorage.getItem(StorageCacheKeyName); if (json_cache !== null) { let _cache = JSON.parse(json_cache); if (_cache instanceof Array) { for (let item of _cache) { if (item instanceof Array && item.length === 2) { let [uid, data] = item; if (typeof uid === "number" && typeof data === "object") { let data_map = new Map(); for (let [key, value] of Object.entries(data)) { if (typeof key === "string") { data_map.set(key, value); } } cache.set(uid, data_map); } } } } } } function save() { let data = { identifiers: Array.from(uidToIdentifier.values()).map((x) => x.dump()), aliases: Array.from(aliases.values()).map((x) => x.dump()), tags: Array.from(tags.values()).map((x) => x.dump()), }; let json = JSON.stringify(data); localStorage.setItem(StorageKeyName, json); } function saveCache() { let cache_data = Array.from(cache.entries()).map(([uid, data]) => [ uid, Object.fromEntries(data.entries()), ]); let json_cache = JSON.stringify(cache_data); localStorage.setItem(StorageCacheKeyName, json_cache); } (function () { "use strict"; load(); // // Your code here... // “添加别名”设置块 let alias_box = new SettingBox("添加别名"); alias_box.items.push(new SettingBoxItemText("UID/用户名")); alias_box.items.push(new SettingBoxItemText("别名")); alias_box.handle((arr) => { let uid_or_name = arr[0].getValue(); let alias = arr[1].getValue(); console.log(`${uid_or_name} -> ${alias}?`); UserIdentifier.fromAny(uid_or_name).then((identifier) => { console.log(`${identifier.uid} ${identifier.username} -> ${alias}`); aliases.set(identifier, new UsernameAlias(identifier, alias)); alert(`为 ${identifier.username} (${identifier.uid}) 添加别名 ${alias}`); save(); run(); }); }); // “添加标签”设置块 let tag_box = new SettingBox("添加标签"); tag_box.items.push(new SettingBoxItemText("UID/用户名")); tag_box.items.push(new SettingBoxItemText("标签")); tag_box.handle((arr) => { let uid_or_name = arr[0].getValue(); let tag = arr[1].getValue(); UserIdentifier.fromAny(uid_or_name).then((identifier) => { console.log(`${identifier.uid} ${identifier.username} -> tag ${tag}`); tags.set(identifier, new UserTag(identifier, tag)); alert(`为 ${identifier.username} (${identifier.uid}) 添加标签 ${tag}`); save(); run(); }); save(); }); // “还原用户”设置块 let restore_box = new SettingBox("还原用户"); restore_box.items.push(new SettingBoxItemText("UID/用户名")); restore_box.handle((arr) => { let uid_or_name = arr[0].getValue(); UserIdentifier.fromAny(uid_or_name).then((identifier) => { let deleted_item = []; if (aliases.has(identifier)) { aliases.delete(identifier); deleted_item.push("别名"); } if (tags.has(identifier)) { tags.delete(identifier); deleted_item.push("标签"); } if (deleted_item.length > 0) { alert(`已删除 ${identifier.username} (${identifier.uid}) 的 ${deleted_item.join("和")}(刷新网页生效)`); } save(); }); }); console.log("Luogu Alias And Customize Tags"); // let prev_time = Date.now(); function run() { // if (Date.now() - prev_time < Cooldown) return; try { restore_box.place(); tag_box.place(); alias_box.place(); } catch (_) { } for (let [_, alias] of aliases) { alias.apply(); } for (let [_, tag] of tags) { tag.apply(); } } window.onload = () => { // 创建 boxes-parent function create_boxes_parent() { let boxes_grand_parent = document.querySelectorAll(".am-g .am-u-lg-3"); if (boxes_grand_parent.length !== 1) throw "cannot place boxes-parent"; let boxes_parent = document.createElement("div"); boxes_parent.id = Prefix + "boxes-parent"; boxes_grand_parent[0].insertBefore(boxes_parent, boxes_grand_parent[0].firstChild); } try { create_boxes_parent(); } catch (err) { console.log("create_boxes_parent: ", err); } // 加入 style 标签 let new_style = document.createElement("style"); new_style.innerHTML = ` span.${Prefix}customized-tag { display: inline-block; color: #fff; padding: 0.25em 0.625em; font-size: min(0.8em, 1.3rem); font-weight: 800; /* margin-left: 0.3em; */ border-radius: 2px; }`; // console.log(new_style); new_style.id = Prefix + "customized-tags-style"; document.head.appendChild(new_style); /* const observer = new MutationObserver(run); observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, }); setTimeout(run, Cooldown); */ setInterval(run, Cooldown); }; })();