云剪贴板

云剪贴板 4yohy8mt

公开

云剪贴板操作

liyifan2022012026/02/04 21:54 创建,当前公开

当前快照
1 份
快照标识符
@mlgxxuw8
此快照首次捕获于
2026/02/11 02:35
上周
此快照最后确认于
2026/02/11 02:35
上周
查看原剪贴板
// ==UserScript== // @name GenGen-RMJ // @icon https://cdn.luogu.com.cn/upload/image_hosting/3s3czya0.png // @namespace https://gengen.qzz.io/ // @version 3.1.2.11 // @description GenGen RMJ 完整版本 // @author GenGen 队 // @run-at document-start // @match https://www.luogu.com.cn/* // @match https://atcoder.jp/contests//submit?RMJ=1 // @match https://codeforces.com/problemset/submit/ // @match https://codeforces.com/problemset/status?my=on // @require https://cdn.bootcdn.net/ajax/libs/html2canvas/1.4.1/html2canvas.js // @require https://cdn.bootcdn.net/ajax/libs/sweetalert2/11.23.0/sweetalert2.all.js // @require https://scriptcat.org/scripts/code/5095/GenGen-CF-RMJ-JL.user.js // @require https://scriptcat.org/scripts/code/5096/GenGen-Cloudflare-RMJ.user.js // @require https://scriptcat.org/scripts/code/5093/GenGen-AT-RMJ-JL.user.js // @require https://scriptcat.org/scripts/code/5094/GenGen-CF-RMJ.user.js // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // ==/UserScript==
(function () { 'use strict';
CPP
const VERSION = '3.1.2.11';
const BUILD_DATE = '2026/2/4';
const UPDATE_INTERVAL = 500;

/* ---------------- 工具函数 ---------------- */
const ready = fn => document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', fn) : fn();
const wait = (sel, cb) => { const t = setInterval(() => { const el = document.querySelector(sel); if (el) { clearInterval(t); cb(el); } }, 100); };

/* ---------------- 样式注入 ---------------- */
GM_addStyle(`
    /* 首页专属按钮:直立粗体罗马体 + 天蓝色 */
    .gengen-home-btn {
        background: linear-gradient(120deg, #4da6ff, #2196f3);
        color: white !important;
        border: none;
        border-radius: 6px;
        padding: 4px 12px;
        font-weight: 700;          /* 粗体 */
        font-style: normal;
        font-size: 16px;
        cursor: pointer;
        margin-right: 12px;
        box-shadow: 0 2px 6px rgba(0, 67, 133, 0.25);
        transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        height: 30px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        letter-spacing: 0.5px;
    }
    .gengen-home-btn:hover {
        transform: translateY(-1px);
        box-shadow: 0 4px 8px rgba(0, 67, 133, 0.35);
        background: linear-gradient(120deg, #3d8be6, #1976d2);
    }
    .gengen-home-btn:active {
        transform: translateY(0);
    }

    /* 面板基础样式 + 淡入淡出动画 */
    #gengen-panel {
        position: fixed;
        width: 400px;
        backdrop-filter: blur(12px);
        background: rgba(255, 255, 255, 0.2);
        border: 1px solid rgba(0, 0, 0, 0.08);
        border-radius: 5px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
        z-index: 2147483647; /* 最高优先级 */
        padding: 20px;
        font-size: 16px;
        display: flex;
        flex-direction: column;
        gap: 14px;
        opacity: 0;
        visibility: hidden;
        transform: translateY(-8px);
        transition:
            opacity 0.32s cubic-bezier(0.16, 1, 0.3, 1),
            transform 0.36s cubic-bezier(0.175, 0.885, 0.32, 1.275),
            visibility 0.32s;
        pointer-events: none; /* 隐藏时不可交互 */
    }
    #gengen-panel.gengen-panel-visible {
        opacity: 1;
        visibility: visible;
        transform: translateY(0);
        pointer-events: all;
    }

    /* 标题:直立粗体罗马体 + 左侧图标 */
    #gengen-panel h3 {
        margin: 0 0 12px 0;
        font-size: 34px;
        font-weight: 700;          /* 粗体 */
        font-style: normal;        /* 直立罗马体(关键!) */
        font-family: "Times New Roman", "Georgia", "SimSun", "宋体", "Songti SC", "STSong", serif;
        color: #1a237e;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        border-bottom: 1.5px solid #e8eaf6;
        padding-bottom: 10px;
        letter-spacing: -0.5px;
    }
    #gengen-panel h3 img {
        width: 30px;
        height: 30px;
        border-radius: 6px;
        flex-shrink: 0;
    }
    #gengen-panel p { margin: 0; line-height: 1.6; color: #37474f; }
    #gengen-panel a { color: #1565c0; text-decoration: none; font-weight: 500; }
    #gengen-panel a:hover { text-decoration: underline; }
    .gengen-account-row { display: flex; justify-content: space-between; padding: 6px 0; }
    .gengen-account-status { font-weight: 600; color: #2e7d32; }
    .gengen-not-bound { color: #c62828; font-weight: 500; }
    .gengen-bind-btn {
        font-size: 13px; padding: 3px 8px;
        background: #e3f2fd; border: 1px solid #1e88e5;
        border-radius: 16px; cursor: pointer;
        color: #0d47a1; font-weight: 500;
        transition: all 0.2s;
    }
    .gengen-bind-btn:hover {
        background: #bbdefb; transform: scale(1.05);
    }
    .gengen-status-wrapper { display: none; justify-content: center; align-items: center; min-height: 24px; width: 100%; }
    .gengen-managed .gengen-original-content { display: none !important; }
    .gengen-managed .gengen-status-wrapper { display: flex !important; }
`);

/* ---------------- 获取账号信息 ---------------- */
async function getAtCoderUser() {
    return new Promise((resolve) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://atcoder.jp/',
            onload: res => {
                if (res.status !== 200) return resolve(null);
                try {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(res.responseText, 'text/html');
                    const userEl = doc.querySelector("#navbar-collapse > ul.nav.navbar-nav.navbar-right > li:nth-child(2) > a");
                    if (!userEl || userEl.textContent.trim() === 'Sign Up') resolve(null);
                    else resolve(userEl.textContent.trim());
                } catch { resolve(null); }
            },
            onerror: () => resolve(null),
            timeout: 8000
        });
    });
}

function getCFHandle(callback) {
    GM_xmlhttpRequest({
        method: 'GET',
        url: 'https://codeforces.com/',
        withCredentials: true,
        onload: function(res) {
            if (res.status !== 200) return callback(null);
            try {
                const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
                const userLink = doc.querySelector('#header > div.lang-chooser > div:nth-child(2) > a:nth-child(1)');
                if (!userLink) return callback(null);
                const text = userLink.textContent.trim();
                if (text === 'Enter') callback(null);
                else callback(text);
            } catch (e) { callback(null); }
        },
        onerror: () => callback(null),
        timeout: 8000
    });
}

/* ---------------- 题目状态图标渲染 ---------------- */
function getOkIcon() {
return `<svg data-v-41ded382="" class="svg-inline--fa fa-check lcolor--success" data-prefix="fas" data-icon="check" role="img" viewBox="0 0 448 512" aria-hidden="true"><path fill="rgb(82, 196, 26)" d="M434.8 70.1c14.3 10.4 17.5 30.4 7.1 44.7l-256 352c-5.5 7.6-14 12.3-23.4 13.1s-18.5-2.7-25.1-9.3l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l101.5 101.5 234-321.7c10.4-14.3 30.4-17.5 44.7-7.1z"></path></svg>`;
}

function getErrorIcon() {
    return `<svg data-v-41ded382="" class="svg-inline--fa fa-xmark lcolor--error" data-prefix="fas" data-icon="xmark" role="img" viewBox="0 0 384 512" aria-hidden="true"><path fill="rgb(231, 76, 60)" d="M55.1 73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L147.2 256 9.9 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192.5 301.3 329.9 438.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.8 256 375.1 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192.5 210.7 55.1 73.4z"></path></svg>`;
}

function getNoIcon() {
    return `<svg data-v-41ded382="" class="svg-inline--fa fa-minus lcolor--grey-4" data-prefix="fas" data-icon="minus" role="img" viewBox="0 0 448 512" aria-hidden="true"><path fill="rgb(89, 89, 89)" d="M0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32z"></path></svg>`;
}

// 缓存映射
let cfSubmissionMap = new Map();
let atSubmissionMap = new Map();
let lastCFRaw = '';
let lastATRaw = '';

function refreshCache() {
    try {
        const cfRaw = GM_getValue('cf-submissions-cache', '[]');
        const atRaw = GM_getValue('at-submissions-cache', '[]');

        if (cfRaw !== lastCFRaw) {
            lastCFRaw = cfRaw;
            cfSubmissionMap = new Map();
            if (cfRaw && cfRaw !== '[]') {
                const cfCache = JSON.parse(cfRaw);
                cfCache.forEach(sub => {
                    if (!sub?.taskDisplay) return;
                    const match = sub.taskDisplay.match(/CF[A-Z0-9]+/i);
                    if (match) {
                        const pid = match[0].toUpperCase();
                        if (!cfSubmissionMap.has(pid)) {
                            cfSubmissionMap.set(pid, sub.verdict || null);
                        }
                    }
                });
            }
        }

        if (atRaw !== lastATRaw) {
            lastATRaw = atRaw;
            atSubmissionMap = new Map();
            if (atRaw && atRaw !== '[]') {
                const atCache = JSON.parse(atRaw);
                atCache.forEach(sub => {
                    if (sub?.taskKey) {
                        const key = sub.taskKey.toLowerCase();
                        if (!atSubmissionMap.has(key)) {
                            atSubmissionMap.set(key, sub.status || null);
                        }
                    }
                });
            }
        }
    } catch (e) {}
}

function initGenGenContainer(cell) {
    if (cell.dataset.gengenInitialized) return;

    const originalWrapper = document.createElement('div');
    originalWrapper.className = 'gengen-original-content';
    originalWrapper.innerHTML = cell.innerHTML;

    const statusWrapper = document.createElement('div');
    statusWrapper.className = 'gengen-status-wrapper';
    statusWrapper.innerHTML = getNoIcon();

    cell.innerHTML = '';
    cell.appendChild(originalWrapper);
    cell.appendChild(statusWrapper);

    cell.dataset.gengenInitialized = '1';
    cell.dataset.gengenStatus = 'none';
}

function updateRowStatus(row) {
    const cells = row.children;
    if (cells.length < 2) return false;

    const firstCell = cells[0];
    const secondCell = cells[1];

    const pidMatch = secondCell.textContent.trim().match(/^(\S+)/);
    if (!pidMatch) {
        if (firstCell.dataset.gengenInitialized) {
            firstCell.classList.remove('gengen-managed');
        }
        return false;
    }

    const pid = pidMatch[1];
    let isCFAT = false;
    let newStatus = 'none';
    let iconHTML = getNoIcon();

    if (pid.startsWith('CF')) {
        isCFAT = true;
        const verdict = cfSubmissionMap.get(pid.toUpperCase());
        if (verdict === 'OK') {
            iconHTML = getOkIcon();
            newStatus = 'AC';
        } else if (verdict) {
            iconHTML = getErrorIcon();
            newStatus = 'WA';
        }
    } else if (pid.startsWith('AT_')) {
        isCFAT = true;
        const taskKey = pid.substring(3).toLowerCase();
        const status = atSubmissionMap.get(taskKey);
        if (status === 'AC') {
            iconHTML = getOkIcon();
            newStatus = 'AC';
        } else if (status) {
            iconHTML = getErrorIcon();
            newStatus = 'WA';
        }
    }

    if (!firstCell.dataset.gengenInitialized) {
        initGenGenContainer(firstCell);
    }

    const oldStatus = firstCell.dataset.gengenStatus;

    if (isCFAT) {
        const statusWrapper = firstCell.querySelector('.gengen-status-wrapper');
        if (statusWrapper) statusWrapper.innerHTML = iconHTML;
        firstCell.classList.add('gengen-managed');
        firstCell.dataset.gengenStatus = newStatus;
        return oldStatus !== newStatus;
    } else {
        firstCell.classList.remove('gengen-managed');
        firstCell.dataset.gengenStatus = 'none';
        return oldStatus !== 'none';
    }
}

let updateIntervalId = null;
function startStatusUpdateLoop() {
    if (updateIntervalId) clearInterval(updateIntervalId);
    updateIntervalId = setInterval(() => {
        refreshCache();
        const rows = document.querySelectorAll('.row');
        rows.forEach(updateRowStatus);
    }, UPDATE_INTERVAL);
}

function stopStatusUpdateLoop() {
    if (updateIntervalId) {
        clearInterval(updateIntervalId);
        updateIntervalId = null;
    }
}

// ---------------- 面板管理逻辑 ----------------
let gengenPanel = null;
let hideTimer = null;
const PANEL_SHOW_DELAY = 200; // 悬浮显示延迟(防误触)
const PANEL_HIDE_DELAY = 300; // 离开隐藏延迟

function createPanel() {
    if (gengenPanel) return;

    const panel = document.createElement('div');
    panel.id = 'gengen-panel';
    panel.innerHTML = `
        <h3>
            <img src="https://gengen.qzz.io/favicon.ico" alt="GenGen">
            GenGen RMJ 3.1
        </h3>
        <p><b>版本:</b>${VERSION} (${BUILD_DATE})</p>
        <p><b>官网:</b><a href="https://gengen.qzz.io" target="_blank" rel="noopener">gengen.qzz.io</a></p>
        <p><b>反馈:</b><a href="https://www.luogu.com.cn/chat?uid=697932" target="_blank" rel="noopener">私信开发者</a></p>
        <p><b>新特性:</b>支持 CodeForces / AtCoder</p>
        <p><b>工单进度:</b><a href="https://www.luogu.qzz.io/article/wcewf5rp" target="_blank" rel="noopener">点击查看</a></p>
        <div class="gengen-account-row">
            <span>Codeforces 账号:</span>
            <span id="cf-status">加载中…</span>
        </div>
        <div class="gengen-account-row">
            <span>AtCoder 账号:</span>
            <span id="at-status">加载中…</span>
        </div>
    `;
    document.body.appendChild(panel);
    gengenPanel = panel;

    // 异步加载账号状态(保持原逻辑)
    getCFHandle(handle => {
        const el = document.getElementById('cf-status');
        if (handle) el.innerHTML = `<span class="gengen-account-status">${handle}</span>`;
        else el.innerHTML = `<span class="gengen-not-bound">未绑定</span> <a class="gengen-bind-btn" href="https://codeforces.com/enter" target="_blank" rel="noopener">绑定</a>`;
    });

    getAtCoderUser().then(handle => {
        const el = document.getElementById('at-status');
        if (handle) el.innerHTML = `<span class="gengen-account-status">${handle}</span>`;
        else el.innerHTML = `<span class="gengen-not-bound">未绑定</span> <a class="gengen-bind-btn" href="https://atcoder.jp/login" target="_blank" rel="noopener">绑定</a>`;
    });

    // 面板悬停事件(防抖动)
    panel.addEventListener('mouseenter', () => clearTimeout(hideTimer));
    panel.addEventListener('mouseleave', schedulePanelHide);
}

function positionPanel(btnRect) {
    if (!gengenPanel) return;

    // 计算理想位置(按钮正下方,左对齐)
    let top = btnRect.bottom + 8; // 8px 间距
    let left = btnRect.left;

    // 边界检测:防止溢出视口
    const panelWidth = 400;
    if (left + panelWidth > window.innerWidth - 16) {
        left = window.innerWidth - panelWidth - 16; // 右侧留16px边距
    }
    if (top + 300 > window.innerHeight) { // 简易高度检测
        top = btnRect.top - 320; // 超出时显示在按钮上方
        if (top < 16) top = 16;
    }

    gengenPanel.style.top = `${top}px`;
    gengenPanel.style.left = `${left}px`;
}

function showPanel(btnRect) {
    clearTimeout(hideTimer);
    if (!gengenPanel) createPanel();
    positionPanel(btnRect);
    gengenPanel.classList.add('gengen-panel-visible');
}

function schedulePanelHide() {
    hideTimer = setTimeout(() => {
        if (gengenPanel) gengenPanel.classList.remove('gengen-panel-visible');
    }, PANEL_HIDE_DELAY);
}

// ---------------- 首页按钮注入(重构版) ----------------
function injectHomeButton() {
    if (location.pathname !== '/' || document.querySelector('.gengen-home-btn')) return;

    wait('.user-nav', nav => {
        if (nav.dataset.gengenProcessed) return;
        nav.dataset.gengenProcessed = '1';

        // 创建天蓝色文字按钮(直立粗体罗马体)
        const btn = document.createElement('div');
        btn.className = 'gengen-home-btn';
        btn.textContent = 'GenGen RMJ';
        btn.setAttribute('aria-label', 'GenGen RMJ 工具面板');

        // 悬浮触发逻辑
        let showTimer = null;
        btn.addEventListener('mouseenter', () => {
            clearTimeout(showTimer);
            showTimer = setTimeout(() => {
                const rect = btn.getBoundingClientRect();
                showPanel(rect);
            }, PANEL_SHOW_DELAY);
        });

        btn.addEventListener('mouseleave', () => {
            clearTimeout(showTimer);
            schedulePanelHide();
        });

        // 插入到导航栏(替换原图标位置)
        nav.parentElement.insertBefore(btn, nav);

        // 全局窗口调整时重定位面板
        window.addEventListener('resize', () => {
            if (gengenPanel?.classList.contains('gengen-panel-visible')) {
                const rect = btn.getBoundingClientRect();
                positionPanel(rect);
            }
        }, { passive: true });
    });
}

/* ---------------- Problem 页面劫持 ---------------- */
function enhanceProblemLink() {
    if (!location.pathname.startsWith('/problem/CF') && !location.pathname.startsWith('/problem/AT')) return;

    const rmj = location.pathname.startsWith('/problem/CF') ? '1' : '2';
    const problemId = location.pathname.split('/problem/')[1];
    if (!problemId) return;

    wait('.side', (sideContainer) => {
        const observer = new MutationObserver(() => {
            const links = sideContainer.querySelectorAll('a');
            for (const link of links) {
                if (!link.href.includes('/record/list?pid=')) continue;

                const span = link.querySelector('span');
                if (span && span.textContent.trim() !== '点击查看') {
                    span.textContent = '点击查看';
                    span.className = 'lcolor-Blue-3';
                    span.style.fontWeight = 'bold';
                }

                if (link.dataset.gengenPatched) continue;

                link.onclick = null;

                link.addEventListener('click', function (e) {
                    e.preventDefault();
                    e.stopImmediatePropagation();

                    const targetUrl = new URL('/record/list', 'https://www.luogu.com.cn');
                    const currentParams = new URLSearchParams(location.search);
                    for (const [key, value] of currentParams) {
                        targetUrl.searchParams.set(key, value);
                    }
                    targetUrl.searchParams.set('pid', problemId);
                    targetUrl.searchParams.set('rmj', rmj);

                    if (window.__LUOGU_ROUTER__) {
                        window.__LUOGU_ROUTER__.push(targetUrl.pathname + targetUrl.search);
                    } else {
                        location.href = targetUrl.toString();
                    }
                }, true);

                link.dataset.gengenPatched = '1';
                link.href = 'javascript:void(0)';
            }
        });

        observer.observe(sideContainer, { childList: true, subtree: true });
        observer.takeRecords();
        observer.disconnect();
        observer.observe(sideContainer, { childList: true, subtree: true });
    });
}

/* ---------------- Record List OJ 选择器 ---------------- */
function enhanceRecordList() {
    if (!location.pathname.startsWith('/record/list')) return;

    wait('section b', b => {
        if (b.dataset.gengen) return;
        b.dataset.gengen = '1';

        const sel = document.createElement('select');
        sel.style.cssText = 'margin-left:8px;padding:0px 8px;border-radius:3px;';
        sel.innerHTML = `
            <option value="">洛谷</option>
            <option value="1">Codeforces</option>
            <option value="2">AtCoder</option>
        `;

        const p = new URLSearchParams(location.search);
        sel.value = p.get('rmj') || '';
        sel.onchange = () => {
            sel.value ? p.set('rmj', sel.value) : p.delete('rmj');
            location.search = p.toString();
        };
        b.after(sel);
    });
}


/* ---------------- 主启动函数 ---------------- */
function main() {
    injectHomeButton();
    enhanceProblemLink();
    enhanceRecordList();
    if (location.pathname.startsWith('/training') || location.pathname.startsWith('/problem/list')) {
      startStatusUpdateLoop();
    }

}

ready(main);

let lastPath = location.pathname;
setInterval(() => {
    let currentPath = location.pathname;
    if (currentPath === lastPath) return;

    if (lastPath.startsWith('/training') || lastPath.startsWith('/problem/list')) {
        stopStatusUpdateLoop();
    }
    if (currentPath.startsWith('/training') || currentPath.startsWith('/problem/list')) {
      startStatusUpdateLoop();
    }
    lastPath = currentPath;
}, 100);

window.addEventListener('beforeunload', () => {
    stopStatusUpdateLoop();
});
})();