Codeforces Username Alias Column

Adds a new column before "Who" to display custom aliases for Codeforces usernames.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Codeforces Username Alias Column
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Adds a new column before "Who" to display custom aliases for Codeforces usernames.
// @author       Sam5440
// @match        https://codeforces.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

(function($) {
    'use strict';

    // 默认的用户名映射,如果 localStorage 中没有数据,将使用这个
    const DEFAULT_USERNAME_MAP = {};
    let USERNAME_MAP = GM_getValue('cf_username_map', DEFAULT_USERNAME_MAP);

    // ================== UI 界面部分 ==================

    // 1. 添加设置按钮
    function addSettingsButton() {
        const settingsButton = $('<button class="cf-rename-settings-button">📝Alias</button>');
        settingsButton.css({
            'position': 'fixed',
            'right': '15px',
            'top': '50%', // 垂直居中
            'transform': 'translateY(-50%)', // 精确垂直居中
            'background-color': '#4CAF50',
            'color': 'white',
            'padding': '8px 12px',
            'border': 'none',
            'border-radius': '5px',
            'cursor': 'pointer',
            'font-size': '14px',
            'z-index': '9999',
            'box-shadow': '0 2px 5px rgba(0,0,0,0.2)',
            'opacity': '0.8',
            'transition': 'opacity 0.3s',
        });

        settingsButton.hover(
            function() { $(this).css('opacity', '1'); },
            function() { $(this).css('opacity', '0.8'); }
        );

        $('body').append(settingsButton);

        settingsButton.on('click', showSettingsModal);
    }

    // 2. 显示设置模态框
    function showSettingsModal() {
        // 创建模态框容器
        const modalOverlay = $('<div id="cf-rename-modal-overlay"></div>').css({
            'position': 'fixed',
            'top': '0',
            'left': '0',
            'width': '100%',
            'height': '100%',
            'background-color': 'rgba(0, 0, 0, 0.6)',
            'display': 'flex',
            'justify-content': 'center',
            'align-items': 'center',
            'z-index': '10000',
        });

        // 创建模态框内容框
        const modalContent = $('<div id="cf-rename-modal-content"></div>').css({
            'background-color': 'white',
            'padding': '25px',
            'border-radius': '8px',
            'box-shadow': '0 4px 10px rgba(0, 0, 0, 0.3)',
            'width': '500px',
            'max-width': '90%',
            'display': 'flex',
            'flex-direction': 'column',
            'gap': '15px',
            'position': 'relative',
        });

        // 关闭按钮
        const closeButton = $('<span style="position: absolute; top: 10px; right: 15px; font-size: 28px; cursor: pointer; color: #555;">&times;</span>');
        closeButton.on('click', () => modalOverlay.remove());

        // 标题
        const modalTitle = $('<h3>Codeforces 用户名别名设置</h3>').css({
            'margin': '0',
            'color': '#333',
            'text-align': 'center',
            'font-size': '1.5em',
        });

        // 提示信息
        const modalHint = $('<p>输入 JSON 格式的字典。要求严格json格式,最后一个k-v没有逗号结尾,例如: <br> {"sam5440": "萝卜","yyf": "🐖", "old_name3": "alias3"}</p>').css('font-size', '0.9em; color: #666; margin-bottom: 10px;');

        // 输入框
        const textarea = $('<textarea id="cf-rename-username-dict"></textarea>').css({
            'width': 'calc(100% - 20px)',
            'height': '200px',
            'padding': '10px',
            'border': '1px solid #ccc',
            'border-radius': '4px',
            'font-family': 'monospace',
            'font-size': '14px',
            'resize': 'vertical',
        });
        try {
            textarea.val(JSON.stringify(USERNAME_MAP, null, 2)); // 格式化显示已保存的字典
        } catch (e) {
            textarea.val(JSON.stringify(DEFAULT_USERNAME_MAP, null, 2));
            console.error("Error parsing existing username map from localStorage:", e);
        }

        // 保存按钮
        const saveButton = $('<button>保存设置</button>').css({
            'background-color': '#007bff',
            'color': 'white',
            'padding': '10px 15px',
            'border': 'none',
            'border-radius': '5px',
            'cursor': 'pointer',
            'font-size': '16px',
            'align-self': 'flex-end', // 按钮靠右对齐
            'transition': 'background-color 0.3s',
        });
        saveButton.hover(
            function() { $(this).css('background-color', '#0056b3'); },
            function() { $(this).css('background-color', '#007bff'); }
        );

        saveButton.on('click', () => {
            try {
                const newMap = JSON.parse(textarea.val());
                USERNAME_MAP = newMap;
                GM_setValue('cf_username_map', USERNAME_MAP);
                alert('用户名别名映射已保存!页面将自动刷新以应用更改。');
                modalOverlay.remove();
                location.reload(); // 保存后刷新页面以应用更改
            } catch (e) {
                alert('无效的 JSON 格式!请检查输入。');
                console.error('JSON parse error:', e);
            }
        });

        // 组合模态框内容
        modalContent.append(closeButton, modalTitle, modalHint, textarea, saveButton);
        modalOverlay.append(modalContent);
        $('body').append(modalOverlay);
    }

    // ================== 核心功能部分 ==================

    // 添加新的列头
    function addAliasColumnHeader() {
        // 查找 standings 表格的表头
        let $standingsTable = $('.standings');
        if ($standingsTable.length === 0) return;

        let $whoHeader = $standingsTable.find('th:contains("Who")');
        if ($whoHeader.length > 0 && $whoHeader.prevAll('th.cf-alias-header').length === 0) { // 防止重复添加
            let $aliasHeader = $('<th class="top cf-alias-header" style="text-align:left;width:8em;">Alias</th>');
            $whoHeader.before($aliasHeader);

            // 同时处理统计行(如果有的话)
            let $statsWhoCell = $standingsTable.find('tr.standingsStatisticsRow td:contains("Accepted")').first();
            if ($statsWhoCell.length > 0 && $statsWhoCell.prevAll('td.cf-alias-stats-cell').length === 0) {
                let $aliasStatsCell = $('<td class="smaller bottom cf-alias-stats-cell" style="text-align:left;padding-left:1em;">Alias</td>');
                $statsWhoCell.before($aliasStatsCell);
            }
        }
    }

    // 在每一行添加别名
    function addAliasToRows() {
        // 确保列头已存在
        addAliasColumnHeader();

        $('.standings tr').not('.standingsStatisticsRow, .cf-alias-processed').each(function() {
            let $row = $(this);
            let $whoCell = $row.find('.contestant-cell'); // "Who" 列的 td 元素
            if ($whoCell.length > 0 && $whoCell.prevAll('td.cf-alias-cell').length === 0) { // 检查是否已添加过别名列
                let originalUsernameElement = $whoCell.find('a.rated-user, span.participant').first();
                if (originalUsernameElement.length === 0) {
                    // 对于没有 rated-user 或 participant 的行(可能不是选手行)
                    // 插入一个空的td以保持表格结构
                    $whoCell.before('<td class="cf-alias-cell"></td>');
                    $row.addClass('cf-alias-processed');
                    return;
                }

                let originalUsername = originalUsernameElement.text().trim();
                let alias = USERNAME_MAP[originalUsername] || '';

                let $aliasCell = $('<td class="cf-alias-cell" style="text-align:left;padding-left:1em;"></td>');
                if (alias) {
                    $aliasCell.text(alias);
                } else {
                    $aliasCell.html('&nbsp;'); // 保持高度
                }

                // 复制背景颜色和黑暗类
                if ($whoCell.hasClass('dark')) {
                    $aliasCell.addClass('dark');
                }

                $whoCell.before($aliasCell);
                $row.addClass('cf-alias-processed'); // 标记为已处理,防止重复添加
            }
        });

        // 刷新 datatable 的样式,确保新列的样式正确
        $('.datatable').each(function () {
            // 重新应用交替背景色和边角样式
            $(this).find("th, td")
                .removeClass("top").removeClass("bottom")
                .removeClass("left").removeClass("right")
                .removeClass("dark");
            $(this).find("tr:first th").addClass("top");
            $(this).find("tr:last td").addClass("bottom");
            $(this).find("tr:odd td").addClass("dark");
            $(this).find("tr td:first-child, tr th:first-child").addClass("left");
            $(this).find("tr td:last-child, tr th:last-child").addClass("right");
        });
    }

    // 使用 MutationObserver 监听 DOM 变化
    function observeDOMChanges() {
        const observer = new MutationObserver(mutations => {
            let columnAdded = false;
            mutations.forEach(mutation => {
                if (mutation.type === 'childList') {
                    // 仅当表格内容有变化时才尝试重新添加列头和行别名
                    if ($(mutation.target).closest('.standings').length || $('.standings').length > 0) {
                        addAliasColumnHeader(); // 确保列头始终存在
                        addAliasToRows();
                        columnAdded = true;
                    }
                }
            });
            // if(columnAdded) {
            //     // 触发datatable的样式更新(如果需要)
            //     if (typeof window.updateDatatableFilter === 'function') {
            //         // Codeforces的datatable更新函数可能需要一个参数,通常是触发筛选的input
            //         // 这里我们只是想触发样式更新,可以尝试传入一个空的或不相关的元素
            //         $('.datatable .filter input').each(function() {
            //             window.updateDatatableFilter(this);
            //         });
            //     }
            // }
        });

        // 监听 body 元素及其子元素的任何变化,特别是对 '.standings' 表格
        // 适当调整监听范围以提高性能,避免过度触发
        const standingsTable = $('.standings')[0]; // 仅监听 standings 表格
        if (standingsTable) {
            observer.observe(standingsTable, {
                childList: true,    // 监听子节点的添加或移除 (例如新行)
                subtree: true,      // 监听所有后代节点的添加、移除或内容修改
            });
        } else {
            // 如果一开始没有 standings 表格,监听 body
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    }


    // ================== 脚本初始化 ==================

    // 等待页面完全加载 (使用 jQuery ready 确保 DOM 准备好)
    $(document).ready(function() {
        addSettingsButton();
        addAliasColumnHeader(); // 首次加载时添加列头
        addAliasToRows(); // 首次加载时添加别名到现有行
        observeDOMChanges(); // 监听后续的 DOM 变化
    });

})(jQuery);