Luogu Alias And Customize Tags

try to take over the world!

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
    };
})();