Greasy Fork

Luogu Alias And Customize Tags

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