Codeforces Username Alias Column

Adds a new column before "Who" to display custom aliases for Codeforces usernames. Click the "📝Alias" button on the right to set up your username aliases.

Ekde 2025/09/26. Vidu La ĝisdata versio.

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

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 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.

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

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         Codeforces Username Alias Column
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a new column before "Who" to display custom aliases for Codeforces usernames. Click the "📝Alias" button on the right to set up your username aliases.
// @author       Sam5440
// @match        https://codeforces.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function($) {
    'use strict';

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

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

    // 1. 添加设置按钮
    function addSettingsButton() {
        // 防止重复添加按钮
        if ($('.cf-rename-settings-button').length > 0) {
            return;
        }

        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() {
        // 如果模态框已存在,则不创建新模态框,或者先移除旧的
        if ($('#cf-rename-modal-overlay').length > 0) {
            $('#cf-rename-modal-overlay').remove();
        }

        // 创建模态框容器
        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 格式的字典。例如: {"old_name1": "别名1", "old_name2": "别名2"}</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 GM_getValue:", 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, .datatable'); // 兼容两种表格类
        if ($standingsTable.length === 0) return;

        $standingsTable.each(function() {
            let currentTable = $(this);
            let $whoHeader = currentTable.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);

                // 同时处理统计行(如果有的话)
                // 找到第一个包含 'Accepted' 的 td 并确保它在 standingsStatisticsRow 中
                let $statsWhoCell = currentTable.find('tr.standingsStatisticsRow td').filter(function() {
                    return $(this).text().includes('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 或 .datatable 表格中的行
        $('.standings, .datatable').find('tr').not('.standingsStatisticsRow, .cf-alias-processed').each(function() {
            let $row = $(this);
            let $whoCell = $row.find('.contestant-cell'); // "Who" 列的 td 元素
            if ($whoCell.length === 0) {
                // 如果不是选手行,但可能是普通的表头行(非th)、或分隔行等,也需要添加一个占位符td以保持列对齐
                let firstTd = $row.find('td:first');
                if (firstTd.length > 0 && firstTd.prevAll('td.cf-alias-cell').length === 0) {
                     // 确保这个行不是纯粹的表头(th)行,并且尚未被处理
                    if ($row.find('th').length === 0) { // 检查这行是否只包含th
                        $('<td class="cf-alias-cell">&nbsp;</td>').insertBefore(firstTd);
                        $row.addClass('cf-alias-processed'); // 标记为已处理
                    }
                }
                return; // 跳过此行,因为它不是有效的选手行
            }

            if ($whoCell.prevAll('td.cf-alias-cell').length === 0) { // 检查是否已添加过别名列
                let originalUsernameElement = $whoCell.find('a.rated-user, span.participant').first();
                let originalUsername = originalUsernameElement.length > 0 ? 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');
                }
                
                // 复制其他样式类,如 alternating row colors
                if ($whoCell.hasClass('dark')) $aliasCell.addClass('dark');
                if ($whoCell.hasClass('odd')) $aliasCell.addClass('odd'); // 如果Codeforces有这种类

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

        // 刷新 datatable 的样式,确保新列的样式正确
        // Codeforces的 datatable 样式有时会根据奇偶行来添加 'dark' 类等
        // 这里尝试重新应用或者触发其内部的样式更新逻辑
        $('.datatable').each(function () {
            let $table = $(this);
            // 移除旧的样式,然后重新应用
            $table.find("th, td").removeClass("top bottom left right dark");

            // 重新应用顶部和底部边框样式
            $table.find("tr:first th").addClass("top");
            $table.find("tr:last td").addClass("bottom"); // 注意:这里可能需要更复杂的逻辑来判断真正的最后一行内容

            // 重新应用左右边框样式
            $table.find("tr th:first-child, tr td:first-child").addClass("left");
            $table.find("tr th:last-child, tr td:last-child").addClass("right");

            // 重新应用交替行颜色
            // 从表头下的第一行开始应用交替颜色
            $table.find("tr").not('.standingsStatisticsRow').each(function(index) {
                if ($(this).find('th').length > 0) return; // 跳过表头行
                // 奇数行(0-indexed)通常是第二行,但因为表头也占了一行,所以这里用 (index + 1) 来判断
                if ((index % 2) === 0) { // 表格内容中的偶数索引行(实际是奇数行)
                    $(this).find('td').addClass('dark');
                } else {
                    $(this).find('td').removeClass('dark');
                }
            });
        });

        // 这个部分通常是 Codeforces 自己的脚本在做,但如果我们添加/删除了列,可能需要触发
        // if (typeof window.updateDatatableFilter === 'function') {
        //     // 这个函数通常用于刷新筛选器,可能也有副作用是刷新表格样式
        //     // 但直接调用可能不是最佳实践,因为它通常需要一个输入元素作为参数
        //     // 暂时注释掉,观察是否需要在 DOM 变化后额外触发样式刷新
        // }
    }

    // 使用 MutationObserver 监听 DOM 变化
    function observeDOMChanges() {
        const observer = new MutationObserver(mutations => {
            let processed = false;
            mutations.forEach(mutation => {
                // 检查是否有新的节点被添加,并且这些节点可能包含 standings 表格或其内容
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    let relevantChange = false;
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1 && ($(node).is('.standings, .datatable') || $(node).find('.standings, .datatable').length > 0)) {
                            relevantChange = true;
                            break;
                        }
                    }

                    // 如果检测到相关变化,则重新处理表格
                    if (relevantChange) {
                        addAliasColumnHeader(); // 确保列头始终存在
                        addAliasToRows();
                        processed = true;
                    } else if ($('.standings, .datatable').length > 0) {
                         // 如果已经有表格了,即使添加的不是表格本身,也可能是表格内容(如异步加载的行)
                        // 这样可以确保别名能在表格内容更新后被添加
                        addAliasToRows();
                        processed = true;
                    }
                }
            });
             // 可以添加一个 debounce,避免频繁操作
            // if (processed) {
            //     // Optional: Trigger a custom event or a slight delay for re-styling if needed
            // }
        });

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


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

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

})(jQuery);