From afea98ef2dac1c6f4986efb82f22a4f63830b512 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 21:52:08 +0800 Subject: [PATCH 01/32] Redesign user rating to reward skill instead of AC rate The old rating was accepted/submitted x 1000, which anyone could inflate by only submitting easy problems. The new 0-3000 rating is computed from one userinfo.php page: attempt-weighted solve volume (saturating), shrunk first-try and per-attempt accuracy that peaks at 45-60%, active days, and a per-day AC soft cap against bulk pasting. It never uses the current time, so every viewer computes the same value. Colors move to six tiers and the cache key is renamed so stale AC-rate values are not reused. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 61 ++++++++++++++++++++++++++++++------- tests/profile-page.test.cjs | 2 +- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index b1cfacce..b0d151d1 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -945,9 +945,9 @@ let RenderMathJax = async () => { }; let GetUserInfo = async (Username) => { try { - if (localStorage.getItem("UserScript-User-" + Username + "-UserRating") != null && new Date().getTime() - parseInt(localStorage.getItem("UserScript-User-" + Username + "-LastUpdateTime")) < 1000 * 60 * 60 * 24) { + if (localStorage.getItem("UserScript-User-" + Username + "-UserRatingV2") != null && new Date().getTime() - parseInt(localStorage.getItem("UserScript-User-" + Username + "-LastUpdateTime")) < 1000 * 60 * 60 * 24) { return { - "Rating": localStorage.getItem("UserScript-User-" + Username + "-UserRating"), + "Rating": localStorage.getItem("UserScript-User-" + Username + "-UserRatingV2"), "EmailHash": localStorage.getItem("UserScript-User-" + Username + "-EmailHash") } } @@ -958,7 +958,7 @@ let GetUserInfo = async (Username) => { return null; } const ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let Rating = (ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)") != null && ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)") != null) ? (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText.trim()) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText.trim())).toFixed(3) * 1000 : 0; + let Rating = CalculateUserRating(ParsedDocument); let Temp = (ParsedDocument.querySelector("#statics > tbody") != null) ? ParsedDocument.querySelector("#statics > tbody").children : []; //页面结构异常(如403/404页面)时返回默认值,不写入缓存 if (Temp.length == 0) { @@ -968,7 +968,7 @@ let GetUserInfo = async (Username) => { } let Email = Temp[Temp.length - 1].children[1].innerText.trim(); let EmailHash = CryptoJS.MD5(Email).toString(); - localStorage.setItem("UserScript-User-" + Username + "-UserRating", Rating); + localStorage.setItem("UserScript-User-" + Username + "-UserRatingV2", Rating); if (Email == "") { EmailHash = undefined; } else { @@ -1611,14 +1611,18 @@ let GetUsernameHTML = async (Element, Username, Simple = false, Href = "https:// // HTMLData += "link-fuchsia" // } // else - if (Rating > 500) { + if (Rating >= 2400) { HTMLData += "link-danger"; - } else if (Rating >= 400) { + } else if (Rating >= 2000) { HTMLData += "link-warning"; - } else if (Rating >= 300) { + } else if (Rating >= 1600) { + HTMLData += "link-primary"; + } else if (Rating >= 1200) { HTMLData += "link-success"; - } else { + } else if (Rating >= 800) { HTMLData += "link-info"; + } else { + HTMLData += "link-secondary"; } } else { HTMLData += "link-info"; @@ -2393,6 +2397,43 @@ function GetProfileActivityData(root = document) { return series.map(points => [...points].sort((a, b) => a[0] - b[0])); } +// Deterministic 0-3000 rating computed from a single userinfo.php page. It only +// reads the p(id, attempts) list and the daily history, and never uses the +// current time, so every viewer gets the same number for the same profile. +function CalculateUserRating(root = document) { + let attempts = []; + for (let script of root.querySelector("#statics")?.querySelectorAll("script") || []) { + for (let match of script.textContent.matchAll(/\bp\(\s*\d+\s*,\s*(\d+)\s*\)/g)) attempts.push(Math.max(1, Number(match[1]))); + } + let solved = attempts.length; + if (solved == 0) return 0; + let activity = GetProfileActivityData(root) || [[], []]; + // Soft-cap ACs per day: normal training days count fully, while a day with + // dozens of ACs (pasted solutions, bulk resubmits) adds only logarithmically. + const DailyCap = 8; + let totalAC = 0, cappedAC = 0; + for (let [, count] of activity[1]) { + totalAC += count; + cappedAC += count <= DailyCap ? count : DailyCap + DailyCap * Math.log(1 + (count - DailyCap) / DailyCap); + } + let burst = totalAC > 0 ? cappedAC / totalAC : 1; + // Volume: each solve is worth 1 on the first try and decays slowly with retries. + let weighted = attempts.reduce((sum, count) => sum + 1 / (1 + 0.4 * Math.log(count)), 0); + let volume = 1 - Math.exp(-weighted * burst / 350); + // Accuracy: first-try rate and per-attempt rate, both shrunk toward typical + // values so small samples cannot score high. + let firstTry = (attempts.filter(count => count == 1).length + 20 * 0.25) / (solved + 20); + let perAttempt = (solved + 60 * 0.2) / (attempts.reduce((sum, count) => sum + count, 0) + 60); + let accuracyRaw = 0.5 * firstTry + 0.5 * perAttempt; + // Strong users still fail on hard problems, so credit peaks around 45%-60%; + // near-perfect accuracy over hundreds of solves looks copied and earns less. + let accuracy = accuracyRaw < 0.15 ? 0 : accuracyRaw < 0.45 ? (accuracyRaw - 0.15) / 0.3 : accuracyRaw < 0.6 ? 1 : Math.max(0.3, 1 - (accuracyRaw - 0.6) * 2); + let confidence = solved / (solved + 80); + let consistency = 1 - Math.exp(-activity[0].length / 250); + let score = 0.57 * volume + 0.33 * accuracy * confidence + 0.1 * consistency; + return Math.round(3000 * Math.pow(score, 0.9)); +} + function InitializeProfileActivityChart(chart) { const data = GetProfileActivityData(); chart.style.cssText = "width:100%;max-width:600px;text-align:left;border:1px solid var(--bs-border-color, #adb5bd);box-sizing:border-box;padding:12px;margin-top:12px"; @@ -2523,9 +2564,7 @@ function InitializeUserProfile(isAdmin = false) { let rows = [...table.rows]; const Value = pattern => rows.find(row => pattern.test(row.cells[0]?.textContent.trim() || ""))?.cells[1]?.textContent.trim(); let email = Value(/^(电子邮箱|Email:?)$/i) || ""; - let submitted = Number(Value(/^(提交|Submit(?:ted)?|Submissions?:?)$/i)); - let accepted = Number(Value(/^(正确|Accepted:?)$/i)); - let rating = submitted > 0 && Number.isFinite(accepted) ? Math.round(accepted / submitted * 1000) : 0; + let rating = CalculateUserRating(); let profile = document.createElement("div"); profile.id = "UserScriptProfile"; profile.className = "row text-start text-left"; diff --git a/tests/profile-page.test.cjs b/tests/profile-page.test.cjs index 5530ac7b..7180f615 100644 --- a/tests/profile-page.test.cjs +++ b/tests/profile-page.test.cjs @@ -92,7 +92,7 @@ test('profile page browser regressions', {timeout: 60000}, async t => { assert.equal(await page.title(), '用户 ProfileTarget 的个人中心'); assert.equal(await page.getByText('用户名:ProfileTarget', {exact: true}).isVisible(), true); assert.equal(await page.getByText('昵称:昵称--后缀', {exact: true}).isVisible(), true); - assert.equal(await page.getByText('评分:400', {exact: true}).isVisible(), true); + assert.equal(await page.getByText('评分:31', {exact: true}).isVisible(), true); assert.equal(await page.locator('#UserScriptProfile img').isVisible(), true); assert.match(await page.locator('#UserScriptProfile img').getAttribute('src'), /123456789012345678901234567890abcf/); assert.equal(await page.evaluate(() => avatarEmail), 'profile@example.test'); From ad2cc201e8b17dac5adfa5d23d536b13642347a3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:06:39 +0000 Subject: [PATCH 02/32] 3.8.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3938b5a8..1e542e44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.8.5", + "version": "3.8.6", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From 6f1453a6b36706d60e9a77391764fff1a9439d23 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:06:45 +0000 Subject: [PATCH 03/32] Update version info to 3.8.6 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 891b4b74..2e3a529e 100644 --- a/Update.json +++ b/Update.json @@ -3872,6 +3872,17 @@ } ], "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" + }, + "3.8.6": { + "UpdateDate": 1791209199952, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1048, + "Description": "Redesign user rating to reward skill instead of AC rate" + } + ], + "Notes": "用户评分重新设计:从「AC 率 × 1000」改为综合做题量、准确度和坚持度的 0–3000 分,更难刷分,用户名颜色改为六档。详见 https://docs.xmoj-script.uk/features/rating.html" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index b0d151d1..262751dc 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.8.5 +// @version 3.8.6 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From f3e93bffb6c7070f50006b187f785492ad6ef140 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 22:14:25 +0800 Subject: [PATCH 04/32] Base rating accuracy on verdict totals, not per-problem counts The number after each solved problem on userinfo.php is the total submit count for that problem, including submissions after AC, so it cannot measure attempts before AC. Volume now uses the solved count, and accuracy uses the exact verdict totals: solved / (solved + PE+WA+TLE+MLE+OLE+RE), shrunk with 40 virtual submissions and peaking at 40%-55%. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 41 ++++++++++++++++++++++--------------- tests/profile-page.test.cjs | 2 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 262751dc..45708c10 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -2398,15 +2398,26 @@ function GetProfileActivityData(root = document) { } // Deterministic 0-3000 rating computed from a single userinfo.php page. It only -// reads the p(id, attempts) list and the daily history, and never uses the -// current time, so every viewer gets the same number for the same profile. +// reads the solved list, the verdict totals and the daily history, and never +// uses the current time, so every viewer gets the same number for the same +// profile. The count after each solved problem includes submissions made after +// AC, so it is not used: accuracy comes from the exact verdict totals instead. function CalculateUserRating(root = document) { - let attempts = []; - for (let script of root.querySelector("#statics")?.querySelectorAll("script") || []) { - for (let match of script.textContent.matchAll(/\bp\(\s*\d+\s*,\s*(\d+)\s*\)/g)) attempts.push(Math.max(1, Number(match[1]))); + let table = root.querySelector("#statics"); + let solvedProblems = new Set(); + for (let script of table?.querySelectorAll("script") || []) { + for (let match of script.textContent.matchAll(/\bp\(\s*(\d+)\s*,\s*\d+\s*\)/g)) solvedProblems.add(match[1]); } - let solved = attempts.length; + let solved = solvedProblems.size; if (solved == 0) return 0; + // Judged failures: PE, WA, TLE, MLE, OLE and RE. Compile errors and ignored + // submissions are not counted. Resubmitting AC code adds nothing here and + // cannot raise the solved count either. + let failures = 0; + for (let link of table.querySelectorAll("a[href*='jresult=']")) { + let result = Number((link.getAttribute("href").match(/jresult=(\d+)/) || [])[1]); + if (result >= 5 && result <= 10) failures += Number(link.textContent.trim()) || 0; + } let activity = GetProfileActivityData(root) || [[], []]; // Soft-cap ACs per day: normal training days count fully, while a day with // dozens of ACs (pasted solutions, bulk resubmits) adds only logarithmically. @@ -2417,17 +2428,13 @@ function CalculateUserRating(root = document) { cappedAC += count <= DailyCap ? count : DailyCap + DailyCap * Math.log(1 + (count - DailyCap) / DailyCap); } let burst = totalAC > 0 ? cappedAC / totalAC : 1; - // Volume: each solve is worth 1 on the first try and decays slowly with retries. - let weighted = attempts.reduce((sum, count) => sum + 1 / (1 + 0.4 * Math.log(count)), 0); - let volume = 1 - Math.exp(-weighted * burst / 350); - // Accuracy: first-try rate and per-attempt rate, both shrunk toward typical - // values so small samples cannot score high. - let firstTry = (attempts.filter(count => count == 1).length + 20 * 0.25) / (solved + 20); - let perAttempt = (solved + 60 * 0.2) / (attempts.reduce((sum, count) => sum + count, 0) + 60); - let accuracyRaw = 0.5 * firstTry + 0.5 * perAttempt; - // Strong users still fail on hard problems, so credit peaks around 45%-60%; - // near-perfect accuracy over hundreds of solves looks copied and earns less. - let accuracy = accuracyRaw < 0.15 ? 0 : accuracyRaw < 0.45 ? (accuracyRaw - 0.15) / 0.3 : accuracyRaw < 0.6 ? 1 : Math.max(0.3, 1 - (accuracyRaw - 0.6) * 2); + let volume = 1 - Math.exp(-solved * burst / 350); + // Share of judged submissions that solved a new problem, shrunk toward 25% + // with 40 virtual submissions so small samples cannot score high. + let precision = (solved + 40 * 0.25) / (solved + failures + 40); + // Strong users still fail on hard problems, so credit peaks at 40%-55%; + // near-perfect precision over hundreds of solves looks copied and earns less. + let accuracy = precision < 0.15 ? 0 : precision < 0.4 ? (precision - 0.15) / 0.25 : precision < 0.55 ? 1 : Math.max(0.3, 1 - (precision - 0.55) * 2); let confidence = solved / (solved + 80); let consistency = 1 - Math.exp(-activity[0].length / 250); let score = 0.57 * volume + 0.33 * accuracy * confidence + 0.1 * consistency; diff --git a/tests/profile-page.test.cjs b/tests/profile-page.test.cjs index 7180f615..e6949a4f 100644 --- a/tests/profile-page.test.cjs +++ b/tests/profile-page.test.cjs @@ -92,7 +92,7 @@ test('profile page browser regressions', {timeout: 60000}, async t => { assert.equal(await page.title(), '用户 ProfileTarget 的个人中心'); assert.equal(await page.getByText('用户名:ProfileTarget', {exact: true}).isVisible(), true); assert.equal(await page.getByText('昵称:昵称--后缀', {exact: true}).isVisible(), true); - assert.equal(await page.getByText('评分:31', {exact: true}).isVisible(), true); + assert.equal(await page.getByText('评分:33', {exact: true}).isVisible(), true); assert.equal(await page.locator('#UserScriptProfile img').isVisible(), true); assert.match(await page.locator('#UserScriptProfile img').getAttribute('src'), /123456789012345678901234567890abcf/); assert.equal(await page.evaluate(() => avatarEmail), 'profile@example.test'); From 225d5abf66b0a05c2f3aee76d8d2d258fb511e2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:15:22 +0000 Subject: [PATCH 05/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 2e3a529e..8f51a666 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791209199952, + "UpdateDate": 1791209717438, "Prerelease": true, "UpdateContents": [ { From 26b29a53f850ee4d5b217607d60ccc765a8a13b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:16:16 +0000 Subject: [PATCH 06/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 8f51a666..067226c3 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791209717438, + "UpdateDate": 1791209770718, "Prerelease": true, "UpdateContents": [ { From 3c7ceba67f75783c2a7084d57a3fc89fe7ae693c Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 22:20:03 +0800 Subject: [PATCH 07/32] Show rating colors on the web contest rank page /web/contest//rank-correct never called GetUsernameHTML, so usernames had no avatar, rating color or badge, unlike contestrank-oi.php. Vue owns the rank table and reuses rows when it re-sorts, so hide its link and add a script-owned copy keyed by username, redrawn only when the row's user changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/XMOJ.user.js b/XMOJ.user.js index 45708c10..0012a126 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -3219,6 +3219,27 @@ async function InitializeContestWebApp() { } } + function EnhanceRank() { + // Give rank usernames the same avatar, rating color and badge as the legacy + // rank pages. Vue owns its link and may reuse the row for another user when + // it re-sorts, so hide its link and keep an owned copy keyed by username. + for (const row of root.querySelectorAll("#rank tbody tr")) { + const cell = row.cells[1]; + const link = cell?.querySelector(`a:not([${owned}] a)`); + if (!link) continue; + const username = link.textContent.trim(); + let span = cell.querySelector(`[${owned}="rank-user"]`); + if (span?.dataset.username === username) continue; + span?.remove(); + if (!username) continue; + link.hidden = true; + span = MakeControl("span", "rank-user", ""); + span.dataset.username = username; + cell.appendChild(span); + GetUsernameHTML(span, username); + } + } + function EnhanceContest() { const table = root.querySelector(".xmoj-problems-table"); if (!table || !routeData?.problems) return; @@ -3415,6 +3436,7 @@ async function InitializeContestWebApp() { if (route.page === "contest") EnhanceContest(); if (route.page === "problem") EnhanceProblem(); if (route.page === "std" || route.page === "solution") EnhanceCode(); + if (route.page === "rank") EnhanceRank(); if (UtilityEnabled("NewBootstrap")) root.querySelector("#rank")?.classList.add("table", "table-hover"); if (UtilityEnabled("Translate")) { const labels = {Rank: "排名", User: "用户", Nick: "昵称", Name: "姓名", Solved: "AC数", Mark: "得分"}; From d6c575603052bada2ee38134c75260aee471c984 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:20:57 +0000 Subject: [PATCH 08/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 067226c3..b7b08885 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791209770718, + "UpdateDate": 1791210051794, "Prerelease": true, "UpdateContents": [ { From d69a4f2a4e673d6090ae3b65630c4c947edf6273 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:21:34 +0000 Subject: [PATCH 09/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index b7b08885..bc47818d 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791210051794, + "UpdateDate": 1791210088844, "Prerelease": true, "UpdateContents": [ { From 00e839bd0fbe195ca28f5ac22a825e70f33d4116 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 22:28:54 +0800 Subject: [PATCH 10/32] Match the web rank-correct page to the legacy OI rank styling Decode the server's cell colors into tries counts and shaded green/red backgrounds like contestrank-oi.php, show the rank in a badge instead of the server's orange cell, render nick and AC count as plain text, stop the general .well theme rule from boxing problem cells, center cells vertically, and use the black MonochromeUI header. Vue resets inline colors when it patches a row, so the observer now also watches style changes and each cell remembers the color Vue set. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 0012a126..3f700563 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -2909,7 +2909,10 @@ function ApplyContestWebTheme() { #app .xmoj-script-has-editors .xmoj-std-code { display: none !important; } #app .xmoj-std-overlay { pointer-events: none; } #app .xmoj-script-code-ready { display: none !important; } - #app #rank td.well { color: #222 !important; } + #app #rank td, #app #rank th { vertical-align: middle; } + #app #rank td.well { color: ${dark ? "white" : "black"} !important; padding: 0.5rem; margin: 0; border: 0; border-radius: 0; } + #app #rank tbody td:not(:nth-child(2)) a { color: inherit; text-decoration: none; }` + (get("MonochromeUI") ? ` + #app #rank thead th, #app #rank thead th a { background-color: black !important; color: white !important; }` : "") + ` #app .xmoj-problem-head h3 { font-size: 1rem; font-weight: inherit !important; font-family: inherit !important; margin: 0; } #app .xmoj-problem-actions .btn { margin: 0 5px; } #app .xmoj-problem-body pre { font-size: 1rem; padding: 0.3em 0.5em; margin: 0.5em 0; } @@ -3224,6 +3227,8 @@ async function InitializeContestWebApp() { // rank pages. Vue owns its link and may reuse the row for another user when // it re-sorts, so hide its link and keep an owned copy keyed by username. for (const row of root.querySelectorAll("#rank tbody tr")) { + EnhanceRankBadge(row.cells[0]); + for (const cell of row.querySelectorAll("td.well")) EnhanceRankCell(cell); const cell = row.cells[1]; const link = cell?.querySelector(`a:not([${owned}] a)`); if (!link) continue; @@ -3240,6 +3245,55 @@ async function InitializeContestWebApp() { } } + function EnhanceRankBadge(cell) { + // The server styles the whole cell as an orange badge; show the rank in a + // badge like the legacy page instead. Vue rewrites the text if it changes, + // which also drops the owned badge, so it is rebuilt on the next pass. + if (!cell || cell.querySelector(`[${owned}="rank-badge"]`)) return; + const text = cell.textContent.trim(); + if (!text) return; + for (const node of cell.childNodes) { + if (node.nodeType === Node.TEXT_NODE) node.nodeValue = ""; + } + cell.className = ""; + const badge = MakeControl("span", "rank-badge", text); + badge.className = "badge text-bg-primary"; + cell.appendChild(badge); + } + + function EnhanceRankCell(cell) { + // Decode the server's cell color like the legacy OI rank page: green 255 + // means solved, and the blue channel encodes the number of failed tries. + // Vue resets the inline color whenever it patches the row, so remember the + // color it set and the one applied here to tell the two apart. + const current = cell.style.backgroundColor; + let tries = cell.querySelector(`[${owned}="rank-tries"]`); + if (tries && current === cell.dataset.xmojColor) return; + const source = current === cell.dataset.xmojColor ? cell.dataset.xmojSource : current; + const match = (source || "").match(/^rgb\((\d+), (\d+), (\d+)\)$/); + if (!match) return; + tries?.remove(); + const [Red, Green, Blue] = match.slice(1).map(Number); + let BackgroundColor, Suffix = ""; + if (Red == 238 && Green == 238 && Blue == 238) { + BackgroundColor = ""; + } else if (Red == 170 && Green == 170 && Blue == 255) { + BackgroundColor = "rgb(127, 127, 255)"; + } else if (Green == 255) { + let ErrorCount = (Blue == 170 ? 5 : (Blue - 51) / 32); + BackgroundColor = "rgba(0, 255, 0, " + Math.max(1 / 10 * (10 - ErrorCount), 0.2) + ")"; + if (ErrorCount != 0) Suffix = " (" + (ErrorCount == 5 ? "4+" : ErrorCount) + ")"; + } else { + let ErrorCount = (Blue == 22 ? 15 : (170 - Blue) / 10); + BackgroundColor = "rgba(255, 0, 0, " + Math.min(ErrorCount / 10 + 0.2, 1) + ")"; + if (ErrorCount != 0) Suffix = " (" + (ErrorCount == 15 ? "14+" : ErrorCount) + ")"; + } + cell.style.backgroundColor = BackgroundColor; + cell.dataset.xmojSource = source; + cell.dataset.xmojColor = cell.style.backgroundColor; + cell.appendChild(MakeControl("span", "rank-tries", Suffix)); + } + function EnhanceContest() { const table = root.querySelector(".xmoj-problems-table"); if (!table || !routeData?.problems) return; @@ -3445,7 +3499,7 @@ async function InitializeContestWebApp() { } } UpdateCountdowns(); - } finally { observer.observe(root, {childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["class"]}); } + } finally { observer.observe(root, {childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["class", "style"]}); } } function ScheduleEnhance() { From db57c1d3d104dd8186e07837104418ef4decb068 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:29:43 +0000 Subject: [PATCH 11/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index bc47818d..e9325376 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791210088844, + "UpdateDate": 1791210578400, "Prerelease": true, "UpdateContents": [ { From 1d2e4a5dddf800acef9afbf60122f0a3f5796f65 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 22:35:02 +0800 Subject: [PATCH 12/32] Let the web rank table overflow like the OI rank page The rank table's .xmoj-scroll-x wrapper clipped the right columns once tries counts widened the cells, and macOS hides its scrollbar. Let it overflow into the page as contestrank-oi.php does. Also drop the MonochromeUI underline, which is a border-bottom, from the nick and AC count links. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 3f700563..623b2833 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -2909,9 +2909,10 @@ function ApplyContestWebTheme() { #app .xmoj-script-has-editors .xmoj-std-code { display: none !important; } #app .xmoj-std-overlay { pointer-events: none; } #app .xmoj-script-code-ready { display: none !important; } + #app .xmoj-scroll-x:has(> #rank) { overflow: visible; } #app #rank td, #app #rank th { vertical-align: middle; } #app #rank td.well { color: ${dark ? "white" : "black"} !important; padding: 0.5rem; margin: 0; border: 0; border-radius: 0; } - #app #rank tbody td:not(:nth-child(2)) a { color: inherit; text-decoration: none; }` + (get("MonochromeUI") ? ` + #app #rank tbody td:not(:nth-child(2)) a { color: inherit; text-decoration: none; border-bottom: 0 !important; }` + (get("MonochromeUI") ? ` #app #rank thead th, #app #rank thead th a { background-color: black !important; color: white !important; }` : "") + ` #app .xmoj-problem-head h3 { font-size: 1rem; font-weight: inherit !important; font-family: inherit !important; margin: 0; } #app .xmoj-problem-actions .btn { margin: 0 5px; } From 6a0f4b18f2b0d725ba863b227c906819134c749e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:35:38 +0000 Subject: [PATCH 13/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index e9325376..b9bd8856 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791210578400, + "UpdateDate": 1791210932313, "Prerelease": true, "UpdateContents": [ { From 958e1fb7eaa1348e41830cb0c75343039a506766 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:36:50 +0000 Subject: [PATCH 14/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index b9bd8856..2db4ab44 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791210932313, + "UpdateDate": 1791210987464, "Prerelease": true, "UpdateContents": [ { From a6231fd373225256d9ff1fe0cf3266157c322ae1 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 22:50:23 +0800 Subject: [PATCH 15/32] Show rank 1 instead of "Winner" on the web rank page The server labels first place "Winner" while contestrank-oi.php shows the number. Other ranks already arrive as numbers and "*" stays as is. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 623b2833..a6aab260 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -3247,7 +3247,7 @@ async function InitializeContestWebApp() { } function EnhanceRankBadge(cell) { - // The server styles the whole cell as an orange badge; show the rank in a + // The server styles the whole cell as an orange badge. Show the rank in a // badge like the legacy page instead. Vue rewrites the text if it changes, // which also drops the owned badge, so it is rebuilt on the next pass. if (!cell || cell.querySelector(`[${owned}="rank-badge"]`)) return; @@ -3257,7 +3257,8 @@ async function InitializeContestWebApp() { if (node.nodeType === Node.TEXT_NODE) node.nodeValue = ""; } cell.className = ""; - const badge = MakeControl("span", "rank-badge", text); + // The server labels first place "Winner"; the OI rank page shows its number. + const badge = MakeControl("span", "rank-badge", text === "Winner" ? "1" : text); badge.className = "badge text-bg-primary"; cell.appendChild(badge); } From b10f64d3555d3ce329f08769582b84d44fd6db83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:50:56 +0000 Subject: [PATCH 16/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 2db4ab44..bacc34ac 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791210987464, + "UpdateDate": 1791211850787, "Prerelease": true, "UpdateContents": [ { From 4c7b61492f7394658fa7eda70f5cc3ad3a942e70 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 22:53:25 +0800 Subject: [PATCH 17/32] Remove ratings cached under the old key from localStorage The new rating is cached under "-UserRatingV2", which left every old AC-rate value under "UserScript-User--UserRating" behind. Delete those once, guarded by a marker key so localStorage is not rescanned on every page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/XMOJ.user.js b/XMOJ.user.js index a6aab260..6b27e48b 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -986,6 +986,23 @@ let GetUserInfo = async (Username) => { } } }; +// Ratings cached before the 0-3000 rating used the AC-rate formula under the +// "-UserRating" key. Remove them once so they don't linger in localStorage. +try { + if (localStorage.getItem("UserScript-RatingV2-Migrated") === null) { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + let key = localStorage.key(i); + if (key && key.startsWith("UserScript-User-") && key.endsWith("-UserRating")) Temp.push(key); + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + localStorage.setItem("UserScript-RatingV2-Migrated", "true"); + } +} catch (e) { + console.error(e); +} /** * Retrieves the badge information for a given user. * From 3da222b0b44cfa433191f75088d9de4d9f07524e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:54:00 +0000 Subject: [PATCH 18/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index bacc34ac..d48e1d94 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791211850787, + "UpdateDate": 1791212034551, "Prerelease": true, "UpdateContents": [ { From e2de9fefde0991a66107a654f5d8b36754b80121 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 14:54:19 +0000 Subject: [PATCH 19/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index d48e1d94..ccb92752 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791212034551, + "UpdateDate": 1791212058651, "Prerelease": true, "UpdateContents": [ { From 60b84a344c9aec09d0ab8d043bcd03c785ef6a11 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 23:00:04 +0800 Subject: [PATCH 20/32] Use one readable rating palette on every page and theme Bootstrap's link-* colors change with theme and hover: the green was too dark on the dark background and the yellow nearly unreadable on white. Rating tiers now use their own classes with one palette, tuned for at least 3.4:1 contrast on both backgrounds, injected on legacy pages and in /web. The yellow tier becomes a vivid orange. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 6b27e48b..c0cbddf7 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -685,6 +685,18 @@ const ThemeCanvasCSS = ` html[data-bs-theme='light'] { background: var(--mono-white, var(--bs-body-bg, #fff)); color-scheme: light; } `; +// Username colors by rating tier. Each color keeps at least 3.4:1 contrast on both +// the light and the dark background, so one palette is used on every page and +// theme instead of Bootstrap's link-* colors, which change with theme and hover. +const RatingColorCSS = ` + a.xmoj-rating-red, a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #e8434b !important; text-decoration-color: rgba(232, 67, 75, 0.5) !important; } + a.xmoj-rating-orange, a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #e8590c !important; text-decoration-color: rgba(232, 89, 12, 0.5) !important; } + a.xmoj-rating-blue, a.xmoj-rating-blue:hover, a.xmoj-rating-blue:focus { color: #337cf2 !important; text-decoration-color: rgba(51, 124, 242, 0.5) !important; } + a.xmoj-rating-green, a.xmoj-rating-green:hover, a.xmoj-rating-green:focus { color: #2b9f4f !important; text-decoration-color: rgba(43, 159, 79, 0.5) !important; } + a.xmoj-rating-cyan, a.xmoj-rating-cyan:hover, a.xmoj-rating-cyan:focus { color: #1798b0 !important; text-decoration-color: rgba(23, 152, 176, 0.5) !important; } + a.xmoj-rating-gray, a.xmoj-rating-gray:hover, a.xmoj-rating-gray:focus { color: #78828c !important; text-decoration-color: rgba(120, 130, 140, 0.5) !important; } +`; + // Both UI initializers use this link; loading only in legacy main() leaves /web // on the fallback fonts even though it applies the same monochrome font stack. function LoadMonochromeFonts() { @@ -1629,17 +1641,17 @@ let GetUsernameHTML = async (Element, Username, Simple = false, Href = "https:// // } // else if (Rating >= 2400) { - HTMLData += "link-danger"; + HTMLData += "xmoj-rating-red"; } else if (Rating >= 2000) { - HTMLData += "link-warning"; + HTMLData += "xmoj-rating-orange"; } else if (Rating >= 1600) { - HTMLData += "link-primary"; + HTMLData += "xmoj-rating-blue"; } else if (Rating >= 1200) { - HTMLData += "link-success"; + HTMLData += "xmoj-rating-green"; } else if (Rating >= 800) { - HTMLData += "link-info"; + HTMLData += "xmoj-rating-cyan"; } else { - HTMLData += "link-secondary"; + HTMLData += "xmoj-rating-gray"; } } else { HTMLData += "link-info"; @@ -2917,7 +2929,7 @@ function ApplyContestWebTheme() { document.documentElement.setAttribute("data-bs-theme", dark ? "dark" : "light"); localStorage.setItem("UserScript-Setting-DarkMode", String(dark)); const modern = get("NewBootstrap"); - style.textContent = ThemeCanvasCSS + ` + style.textContent = ThemeCanvasCSS + RatingColorCSS + ` #app .xmoj-script-tools { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; } #app .copy-btn { margin-left: 10px; } #app .xmoj-script-countdown { margin-left: 8px; white-space: nowrap; } @@ -4021,6 +4033,7 @@ async function main() { if (UtilityEnabled("AddAnimation")) Style.innerHTML += `.status, .test-case { transition: ${_isMono ? "100ms ease" : "0.5s"} !important; }`; if (UtilityEnabled("AddColorText")) Style.innerHTML += `.red { color: red !important; } .green { color: green !important; } .blue { color: blue !important; }`; } + Style.innerHTML += RatingColorCSS; if (UtilityEnabled("RemoveUseless")) { if (document.getElementsByClassName("footer")[0] != null) { From e5bdbb5813f05abe3a66c5a5ca9da741e0cea2d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 15:00:46 +0000 Subject: [PATCH 21/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index ccb92752..af7edb7e 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791212058651, + "UpdateDate": 1791212440504, "Prerelease": true, "UpdateContents": [ { From 0a5778e61acb42cc86db80b97b7fa8501bba4988 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 15:02:22 +0000 Subject: [PATCH 22/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index af7edb7e..15c01c18 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791212440504, + "UpdateDate": 1791212537071, "Prerelease": true, "UpdateContents": [ { From 038d6b0e8da57500eb3e3fb54123b0a1478c83a6 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 5 Oct 2026 23:05:19 +0800 Subject: [PATCH 23/32] Restore a hover effect on rating-colored usernames Like Bootstrap's link-* hover, darken the tier color by 20% and make the underline opaque. The shades are fixed, so they match on every page and theme. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index c0cbddf7..d16e9175 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -687,14 +687,21 @@ const ThemeCanvasCSS = ` // Username colors by rating tier. Each color keeps at least 3.4:1 contrast on both // the light and the dark background, so one palette is used on every page and -// theme instead of Bootstrap's link-* colors, which change with theme and hover. +// theme instead of Bootstrap's link-* colors, which change with theme. Like +// Bootstrap, hover darkens the color by 20% and makes the underline opaque. const RatingColorCSS = ` - a.xmoj-rating-red, a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #e8434b !important; text-decoration-color: rgba(232, 67, 75, 0.5) !important; } - a.xmoj-rating-orange, a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #e8590c !important; text-decoration-color: rgba(232, 89, 12, 0.5) !important; } - a.xmoj-rating-blue, a.xmoj-rating-blue:hover, a.xmoj-rating-blue:focus { color: #337cf2 !important; text-decoration-color: rgba(51, 124, 242, 0.5) !important; } - a.xmoj-rating-green, a.xmoj-rating-green:hover, a.xmoj-rating-green:focus { color: #2b9f4f !important; text-decoration-color: rgba(43, 159, 79, 0.5) !important; } - a.xmoj-rating-cyan, a.xmoj-rating-cyan:hover, a.xmoj-rating-cyan:focus { color: #1798b0 !important; text-decoration-color: rgba(23, 152, 176, 0.5) !important; } - a.xmoj-rating-gray, a.xmoj-rating-gray:hover, a.xmoj-rating-gray:focus { color: #78828c !important; text-decoration-color: rgba(120, 130, 140, 0.5) !important; } + a.xmoj-rating-red { color: #e8434b !important; text-decoration-color: rgba(232, 67, 75, 0.5) !important; } + a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #ba363c !important; text-decoration-color: #ba363c !important; } + a.xmoj-rating-orange { color: #e8590c !important; text-decoration-color: rgba(232, 89, 12, 0.5) !important; } + a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #ba470a !important; text-decoration-color: #ba470a !important; } + a.xmoj-rating-blue { color: #337cf2 !important; text-decoration-color: rgba(51, 124, 242, 0.5) !important; } + a.xmoj-rating-blue:hover, a.xmoj-rating-blue:focus { color: #2963c2 !important; text-decoration-color: #2963c2 !important; } + a.xmoj-rating-green { color: #2b9f4f !important; text-decoration-color: rgba(43, 159, 79, 0.5) !important; } + a.xmoj-rating-green:hover, a.xmoj-rating-green:focus { color: #227f3f !important; text-decoration-color: #227f3f !important; } + a.xmoj-rating-cyan { color: #1798b0 !important; text-decoration-color: rgba(23, 152, 176, 0.5) !important; } + a.xmoj-rating-cyan:hover, a.xmoj-rating-cyan:focus { color: #127a8d !important; text-decoration-color: #127a8d !important; } + a.xmoj-rating-gray { color: #78828c !important; text-decoration-color: rgba(120, 130, 140, 0.5) !important; } + a.xmoj-rating-gray:hover, a.xmoj-rating-gray:focus { color: #606870 !important; text-decoration-color: #606870 !important; } `; // Both UI initializers use this link; loading only in legacy main() leaves /web From 77578362c2fa345cc148026d60c48c144d8a6bfc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 5 Oct 2026 15:05:56 +0000 Subject: [PATCH 24/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 15c01c18..0665a501 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791212537071, + "UpdateDate": 1791212751014, "Prerelease": true, "UpdateContents": [ { From 1bbf8e1434d2173f99897f220eb83154b72c8767 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 08:13:04 +0800 Subject: [PATCH 25/32] Separate the red and orange rating colors The two were only about 30 degrees apart in hue at the same lightness and read as one color. Red moves toward crimson and orange toward amber, more than doubling their perceptual distance while staying readable on both backgrounds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index d16e9175..43268ce0 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -685,15 +685,15 @@ const ThemeCanvasCSS = ` html[data-bs-theme='light'] { background: var(--mono-white, var(--bs-body-bg, #fff)); color-scheme: light; } `; -// Username colors by rating tier. Each color keeps at least 3.4:1 contrast on both +// Username colors by rating tier. Each color keeps at least 3.2:1 contrast on both // the light and the dark background, so one palette is used on every page and // theme instead of Bootstrap's link-* colors, which change with theme. Like // Bootstrap, hover darkens the color by 20% and makes the underline opaque. const RatingColorCSS = ` - a.xmoj-rating-red { color: #e8434b !important; text-decoration-color: rgba(232, 67, 75, 0.5) !important; } - a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #ba363c !important; text-decoration-color: #ba363c !important; } - a.xmoj-rating-orange { color: #e8590c !important; text-decoration-color: rgba(232, 89, 12, 0.5) !important; } - a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #ba470a !important; text-decoration-color: #ba470a !important; } + a.xmoj-rating-red { color: #dc2f5a !important; text-decoration-color: rgba(220, 47, 90, 0.5) !important; } + a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #b02648 !important; text-decoration-color: #b02648 !important; } + a.xmoj-rating-orange { color: #d9730d !important; text-decoration-color: rgba(217, 115, 13, 0.5) !important; } + a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #ae5c0a !important; text-decoration-color: #ae5c0a !important; } a.xmoj-rating-blue { color: #337cf2 !important; text-decoration-color: rgba(51, 124, 242, 0.5) !important; } a.xmoj-rating-blue:hover, a.xmoj-rating-blue:focus { color: #2963c2 !important; text-decoration-color: #2963c2 !important; } a.xmoj-rating-green { color: #2b9f4f !important; text-decoration-color: rgba(43, 159, 79, 0.5) !important; } From ffd6b4743cd74020bb64462320f4ed684c71623d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 00:13:43 +0000 Subject: [PATCH 26/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 0665a501..f476b3b6 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791212751014, + "UpdateDate": 1791245613047, "Prerelease": true, "UpdateContents": [ { From a42778bb2fdad5ee69f9cbf20b76b112200ef9fa Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 08:16:59 +0800 Subject: [PATCH 27/32] Use the vivid orange for the second rating tier The amber looked muddy. With red now crimson, the vivid orange stays clearly distinct from it and has better contrast on white. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 43268ce0..e3941521 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -685,15 +685,15 @@ const ThemeCanvasCSS = ` html[data-bs-theme='light'] { background: var(--mono-white, var(--bs-body-bg, #fff)); color-scheme: light; } `; -// Username colors by rating tier. Each color keeps at least 3.2:1 contrast on both +// Username colors by rating tier. Each color keeps at least 3.3:1 contrast on both // the light and the dark background, so one palette is used on every page and // theme instead of Bootstrap's link-* colors, which change with theme. Like // Bootstrap, hover darkens the color by 20% and makes the underline opaque. const RatingColorCSS = ` a.xmoj-rating-red { color: #dc2f5a !important; text-decoration-color: rgba(220, 47, 90, 0.5) !important; } a.xmoj-rating-red:hover, a.xmoj-rating-red:focus { color: #b02648 !important; text-decoration-color: #b02648 !important; } - a.xmoj-rating-orange { color: #d9730d !important; text-decoration-color: rgba(217, 115, 13, 0.5) !important; } - a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #ae5c0a !important; text-decoration-color: #ae5c0a !important; } + a.xmoj-rating-orange { color: #e8590c !important; text-decoration-color: rgba(232, 89, 12, 0.5) !important; } + a.xmoj-rating-orange:hover, a.xmoj-rating-orange:focus { color: #ba470a !important; text-decoration-color: #ba470a !important; } a.xmoj-rating-blue { color: #337cf2 !important; text-decoration-color: rgba(51, 124, 242, 0.5) !important; } a.xmoj-rating-blue:hover, a.xmoj-rating-blue:focus { color: #2963c2 !important; text-decoration-color: #2963c2 !important; } a.xmoj-rating-green { color: #2b9f4f !important; text-decoration-color: rgba(43, 159, 79, 0.5) !important; } From 503012b3ae5046c2c625244e6acf660908e1b8d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 00:17:32 +0000 Subject: [PATCH 28/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index f476b3b6..1d995b31 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791245613047, + "UpdateDate": 1791245845095, "Prerelease": true, "UpdateContents": [ { From 15c390247c0d6a0140a8aa24cc262b003302cdd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 00:20:05 +0000 Subject: [PATCH 29/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 1d995b31..fb656230 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791245845095, + "UpdateDate": 1791246000012, "Prerelease": true, "UpdateContents": [ { From 672d40877ae3ccdd590783e9b9e2e13a7a2ebf8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 00:20:32 +0000 Subject: [PATCH 30/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index fb656230..3cd7a416 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791246000012, + "UpdateDate": 1791246027600, "Prerelease": true, "UpdateContents": [ { From 6d9fd9092b62826c7c07e7b1a253a5712cf4cdb5 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 08:29:49 +0800 Subject: [PATCH 31/32] Count solved problems from links too when computing the rating CalculateUserRating only read p(id, count) script calls, so a profile that lists solved problems as rendered problem.php links rated 0. Use GetProfileSolvedProblems, which handles both forms. Per-problem attempt counts are not used by the rating, so no fallback is needed for them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- XMOJ.user.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index e3941521..997b2302 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -2440,11 +2440,8 @@ function GetProfileActivityData(root = document) { // AC, so it is not used: accuracy comes from the exact verdict totals instead. function CalculateUserRating(root = document) { let table = root.querySelector("#statics"); - let solvedProblems = new Set(); - for (let script of table?.querySelectorAll("script") || []) { - for (let match of script.textContent.matchAll(/\bp\(\s*(\d+)\s*,\s*\d+\s*\)/g)) solvedProblems.add(match[1]); - } - let solved = solvedProblems.size; + // Solved problems appear as p(id, count) calls or as rendered problem links. + let solved = GetProfileSolvedProblems(root).length; if (solved == 0) return 0; // Judged failures: PE, WA, TLE, MLE, OLE and RE. Compile errors and ignored // submissions are not counted. Resubmitting AC code adds nothing here and From 23c9c02435e79d86d219fe2a7011377fa3da7a32 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 00:30:17 +0000 Subject: [PATCH 32/32] Update time and description of 3.8.6 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 3cd7a416..61a3363d 100644 --- a/Update.json +++ b/Update.json @@ -3874,7 +3874,7 @@ "Notes": "

修复用户没有标签时个人中心误显示加载错误和重试按钮的问题,恢复管理员的添加标签入口。修改账号页面在没有标签时隐藏标签编辑,避免提交无效修改;实际请求失败时仍可重试。

" }, "3.8.6": { - "UpdateDate": 1791246027600, + "UpdateDate": 1791246612810, "Prerelease": true, "UpdateContents": [ {