Codeforces Username Alias Column

徹底革新M-Team種子列表為高度自定義卡片網格佈局。功能涵蓋點擊放大、按鈕同步、字體/顏色調節、大種子高亮、靈活佈局與多語言支持。最新版新增「Free」種子綠色高亮、下載新分頁、刷新延遲自定義、下載進度顯示等,所有設置均可持久化保存。

Version au 26/09/2025. Voir la dernière version.

// ==UserScript==
// @name         Codeforces Username Alias Column
// @namespace    https://github.com/Sam5440/mteam_next_beautification
// @version      1.6
// @description  徹底革新M-Team種子列表為高度自定義卡片網格佈局。功能涵蓋點擊放大、按鈕同步、字體/顏色調節、大種子高亮、靈活佈局與多語言支持。最新版新增「Free」種子綠色高亮、下載新分頁、刷新延遲自定義、下載進度顯示等,所有設置均可持久化保存。
// @author       ChatGPT & Sam5440
// @match        https://next.m-team.cc/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      raw.githubusercontent.com
// @homepageURL  https://github.com/Sam5440/mteam_next_beautification
// @supportURL   https://github.com/Sam5440/mteam_next_beautification/issues
// @license      MIT
// @downloadURL https://update.greasyfork.org/scripts/541917/M-Team%20%E5%B0%81%E9%9D%A2%E5%A2%9E%E5%BC%B7PRO%20%28%E7%B6%B2%E6%A0%BC%E4%BD%88%E5%B1%80%E3%80%81%E9%BB%9E%E6%93%8A%E6%94%BE%E5%A4%A7%E3%80%81%E9%AB%98%E7%B4%9A%E8%87%AA%E5%AE%9A%E7%BE%A9%29.user.js
// @updateURL https://update.greasyfork.org/scripts/541917/M-Team%20%E5%B0%81%E9%9D%A2%E5%A2%9E%E5%BC%B7PRO%20%28%E7%B6%B2%E6%A0%BC%E4%BD%88%E5%B1%80%E3%80%81%E9%BB%9E%E6%93%8A%E6%94%BE%E5%A4%A7%E3%80%81%E9%AB%98%E7%B4%9A%E8%87%AA%E5%AE%9A%E7%BE%A9%29.meta.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() {
        // 防止重复添加按钮
        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);