// ==UserScript== // @name GenGen-RMJ // @icon https://cdn.luogu.com.cn/upload/image_hosting/3s3czya0.png // @namespace https://gengen.qzz.io/ // @version 3.1.3.4 // @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'; const V = '3.1.3.3'; const D = '2026/2/23'; const I = 500; // SPA cleanup state let c = false; let u = location.href; // —————— 工具 —————— const r = fn => document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', fn) : fn(); const w = (s, cb) => { const t = setInterval(() => { const e = document.querySelector(s); if (e) { clearInterval(t); cb(e); } }, 100); }; // —————— 样式 —————— GM_addStyle(` .ghb { background: linear-gradient(120deg, #4da6ff, #2196f3); color: white !important; border: none; border-radius: 6px; font-family: "Console", monospace; padding: 4px 12px; font-weight: bold; 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; } .ghb:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 67, 133, 0.35); background: linear-gradient(120deg, #3d8be6, #1976d2); } .ghb:active { transform: translateY(0); } #gp { position: fixed; width: 400px; backdrop-filter: blur(4px); 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, transform 0.36s, visibility 0.32s; pointer-events: none; } #gp.v { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: all; } #gp h3 { margin: 0 0 12px 0; font-size: 34px; font-weight: bold; font-style: normal; font-family: "Courier New", 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; } #gp h3 img { width: 30px; height: 30px; border-radius: 6px; flex-shrink: 0; } #gp p { margin: 0; line-height: 1.6; color: #37474f; } #gp a { color: #1565c0; text-decoration: none; font-weight: 500; } #gp a:hover { text-decoration: underline; } .ar { display: flex; justify-content: space-between; padding: 6px 0; } .as { font-weight: 600; color: #2e7d32; } .nb { color: #c62828; font-weight: 500; } .bb { 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; } .bb:hover { background: #bbdefb; transform: scale(1.05); } .sw { display: none; justify-content: center; align-items: center; min-height: 24px; width: 100%; } .m .oc { display: none !important; } .m .sw { display: flex !important; } `); // —————— 图标 —————— const ok = () => ``; const er = () => ``; const no = () => ``; // —————— 缓存 —————— let cf = new Map(), at = new Map(), lcf = '', lat = ''; function rc() { try { const a = GM_getValue('cf-submissions-cache', '[]'); const b = GM_getValue('at-submissions-cache', '[]'); if (a !== lcf) { lcf = a; cf = new Map(); if (a && a !== '[]') JSON.parse(a).forEach(s => { if (!s?.taskDisplay) return; const m = s.taskDisplay.match(/CF[A-Z0-9]+/i); if (m) { const key = m[0].toUpperCase(); const v = s.verdict || null; if (!cf.has(key) || (cf.get(key) !== 'OK' && v === 'OK')) { cf.set(key, v); } } }); } if (b !== lat) { lat = b; at = new Map(); if (b && b !== '[]') JSON.parse(b).forEach(s => { if (s?.taskKey) { const key = s.taskKey.toLowerCase(); const st = s.status || null; if (!at.has(key) || (at.get(key) !== 'AC' && st === 'AC')) { at.set(key, st); } } }); } } catch (e) {} } // —————— DOM 初始化(仅 CF/AT) —————— function ic(e) { if (e.dataset.g) return; const o = document.createElement('div'); o.className = 'oc'; while (e.firstChild) o.appendChild(e.firstChild); const s = document.createElement('div'); s.className = 'sw'; s.innerHTML = no(); e.appendChild(o); e.appendChild(s); e.dataset.g = '1'; } // —————— 更新行状态(安全版) —————— function ur(rw) { const cs = rw.children; if (cs.length < 2) return false; const c0 = cs[0], c1 = cs[1]; const m = c1.textContent.trim().match(/^(\S+)/); if (!m) return false; const p = m[1]; const isCF = /^CF[A-Z0-9]+$/i.test(p); const isAT = /^AT_[A-Za-z0-9_]+$/.test(p); const man = isCF || isAT; // —————— 非管辖题目:绝不碰 DOM —————— if (!man) { // 如果之前错误初始化了(比如开发残留),清理一次 if (c0.dataset.g) { const o = c0.querySelector('.oc'); if (o) { c0.innerHTML = o.innerHTML; // 恢复原始内容 delete c0.dataset.g; delete c0.dataset.s; } } return false; } // —————— 管辖题目:安全初始化 + 更新 —————— if (!c0.dataset.g) { // 初始化容器(仅移动节点,不 innerHTML) const o = document.createElement('div'); o.className = 'oc'; while (c0.firstChild) o.appendChild(c0.firstChild); const s = document.createElement('div'); s.className = 'sw'; s.innerHTML = no(); c0.appendChild(o); c0.appendChild(s); c0.dataset.g = '1'; } let ns = 'none', icn = no(); if (isCF) { const v = cf.get(p.toUpperCase()); if (v === 'OK') { icn = ok(); ns = 'AC'; } else if (v) { icn = er(); ns = 'WA'; } } else if (isAT) { const k = p.substring(3).toLowerCase(); const st = at.get(k); if (st === 'AC') { icn = ok(); ns = 'AC'; } else if (st) { icn = er(); ns = 'WA'; } } const os = c0.dataset.s || 'none'; const sw = c0.querySelector('.sw'); if (sw) sw.innerHTML = icn; c0.classList.add('m'); c0.dataset.s = ns; return os !== ns; } // —————— 清理函数(离开 CF/AT 时调用) —————— function cl() { document.querySelectorAll('[data-g="1"]').forEach(e => { const o = e.querySelector('.oc'); if (o) { while (o.firstChild) e.appendChild(o.firstChild); e.removeChild(o); const s = e.querySelector('.sw'); if (s) e.removeChild(s); delete e.dataset.g; delete e.dataset.s; e.classList.remove('m'); } }); } let curURL = location.href; function onNav() { const newURL = location.href; if (newURL === curURL) return; curURL = newURL; // 从 CF/AT 单题页离开? const wasCFAT = /^\/problem\/(CF|AT_)/.test(new URL.split('?')[0].replace(location.origin, '')); const nowCFAT = /^\/problem\/(CF|AT_)/.test(location.pathname); if (wasCFAT && !nowCFAT) { // 清理所有被 GenGen 修改的元素 document.querySelectorAll('[data-g="1"]').forEach(e => { const o = e.querySelector('.oc'); if (o) { e.innerHTML = o.innerHTML; delete e.dataset.g; delete e.dataset.s; } }); } } // —————— 循环更新 —————— let tid = null; function st() { if (tid) clearInterval(tid); tid = setInterval(() => { rc(); document.querySelectorAll('.row').forEach(ur); }, I); } function sp() { if (tid) { clearInterval(tid); tid = null; } } // —————— 面板 —————— let pn = null, ht = null; const sd = 200, hd = 300; function cp() { if (pn) return; pn = document.createElement('div'); pn.id = 'gp'; pn.innerHTML = `
GenGen RMJ 3.1
版本:${V} (${D})
反馈:私信开发者
新特性:支持 CodeForces / AtCoder
工单进度:点击查看
GenGen RMJ`;
let stmr = null;
b.addEventListener('mouseenter', () => {
clearTimeout(stmr);
stmr = setTimeout(() => spn(b.getBoundingClientRect()), sd);
});
b.addEventListener('mouseleave', () => {
clearTimeout(stmr);
ht = setTimeout(() => pn?.classList.remove('v'), hd);
});
n.parentElement.insertBefore(b, n);
addEventListener('resize', () => {
if (pn?.classList.contains('v')) pp(b.getBoundingClientRect());
}, { passive: true });
});
}
// —————— Problem 页面劫持 ——————
function ep() {
if (!location.pathname.startsWith('/problem/CF') && !location.pathname.startsWith('/problem/AT')) return;
const rmj = location.pathname.startsWith('/problem/CF') ? '1' : '2';
const pid = location.pathname.split('/problem/')[1];
if (!pid) return;
w('.side', sc => {
const obs = new MutationObserver(() => {
sc.querySelectorAll('a').forEach(l => {
if (!l.href.includes('/record/list?pid=')) return;
const s = l.querySelector('span');
if (s && s.textContent.trim() !== '点击查看') {
s.textContent = '点击查看';
s.className = 'lcolor-Blue-3';
s.style.fontWeight = 'bold';
}
if (l.dataset.pt) return;
l.onclick = null;
l.addEventListener('click', e => {
e.preventDefault();
e.stopImmediatePropagation();
const u = new URL('/record/list', 'https://www.luogu.com.cn');
const p = new URLSearchParams(location.search);
for (const [k, v] of p) u.searchParams.set(k, v);
u.searchParams.set('pid', pid);
u.searchParams.set('rmj', rmj);
location.href = u.toString();
}, true);
l.dataset.pt = '1';
l.href = 'javascript:void(0)';
});
});
obs.observe(sc, { childList: true, subtree: true });
});
}
// —————— Record List 选择器 ——————
function erl() {
if (!location.pathname.startsWith('/record/list')) return;
w('section b', b => {
if (b.dataset.g) return;
b.dataset.g = '1';
const s = document.createElement('select');
s.style.cssText = 'margin-left:8px;padding:0px 8px;border-radius:3px;';
s.innerHTML = ``;
const p = new URLSearchParams(location.search);
s.value = p.get('rmj') || '';
s.onchange = () => {
s.value ? p.set('rmj', s.value) : p.delete('rmj');
location.search = p.toString();
};
b.after(s);
});
}
// —————— 主逻辑 ——————
function mn() {
ih();
ep();
erl();
if (location.pathname.startsWith('/training') || location.pathname.startsWith('/problem/list')) {
st();
}
}
r(mn);
let lp = location.pathname;
setInterval(() => {
const cp = location.pathname;
if (cp === lp) return;
if (lp.startsWith('/training') || lp.startsWith('/problem/list')) sp();
if (cp.startsWith('/training') || cp.startsWith('/problem/list')) st();
lp = cp;
}, 100);
addEventListener('beforeunload', () => sp());
})();