From b789bf28da38e79ce5aee665b90df7c8861d9fc4 Mon Sep 17 00:00:00 2001 From: Zhu Chenrui Date: Fri, 25 Jul 2025 19:46:29 +0800 Subject: [PATCH 01/17] Update Update.json Signed-off-by: Zhu Chenrui --- Update.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Update.json b/Update.json index 8674920b..e4384e5b 100644 --- a/Update.json +++ b/Update.json @@ -2937,7 +2937,7 @@ ], "Notes": "No release notes were provided for this release." }, - "1.10.0": { + "1.999990.0": { "UpdateDate": 1753443146018, "Prerelease": false, "UpdateContents": [ @@ -2949,4 +2949,4 @@ "Notes": "No release notes were provided for this release." } } -} \ No newline at end of file +} From 6adac8da5181731071e8d7fad87fe9552e93c96f Mon Sep 17 00:00:00 2001 From: Zhu Chenrui Date: Sun, 24 Aug 2025 11:07:02 +0800 Subject: [PATCH 02/17] Parse release notes from comment block (cherry picked from commit c7137ff7122b2307b9ea78a24de240b3bd00c6ab) --- .github/workflows/UpdateToRelease.yml | 4 +++- .github/workflows/UpdateVersion.yml | 4 +++- CONTRIBUTING.md | 1 + Update/UpdateToRelease.js | 10 +++++++++- Update/UpdateVersion.js | 13 ++++++++++++- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/UpdateToRelease.yml b/.github/workflows/UpdateToRelease.yml index ba950e34..e401f757 100644 --- a/.github/workflows/UpdateToRelease.yml +++ b/.github/workflows/UpdateToRelease.yml @@ -5,6 +5,7 @@ on: - opened - reopened - synchronize + - edited branches: - master jobs: @@ -24,7 +25,8 @@ jobs: gh pr comment ${{ github.event.pull_request.number }} --body "请向\`dev\`分支提交pull request, 本pull request将被自动关闭" gh pr close ${{ github.event.pull_request.number }} else - node ./Update/UpdateToRelease.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.number }} + node ./Update/UpdateToRelease.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.number }} "$PR_BODY" fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_BODY: ${{ github.event.pull_request.body }} diff --git a/.github/workflows/UpdateVersion.yml b/.github/workflows/UpdateVersion.yml index 8ab253d5..b71fbb1f 100644 --- a/.github/workflows/UpdateVersion.yml +++ b/.github/workflows/UpdateVersion.yml @@ -23,4 +23,6 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@v5 - name: Update version - run: node ./Update/UpdateVersion.js ${{ steps.generate_token.outputs.token }} ${{ github.event.number }} "${{ github.event.pull_request.title }}" + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: node ./Update/UpdateVersion.js ${{ steps.generate_token.outputs.token }} ${{ github.event.number }} "${{ github.event.pull_request.title }}" "$PR_BODY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d357e825..0f5c6e14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,3 +18,4 @@ We believe that you must be excited to contribute to our repo, but first, please - Be patient. We are a small team and may not be able to review your PR immediately. - Please be considerate towards the developers and other users when raising issues or presenting pull requests. - Respect our decision(s), and do not be upset or abusive if your submission is not used. +- For release pull requests, include an HTML comment block starting with `` in the PR description. The automation will extract that block into the release notes. diff --git a/Update/UpdateToRelease.js b/Update/UpdateToRelease.js index 37256782..c675452c 100644 --- a/Update/UpdateToRelease.js +++ b/Update/UpdateToRelease.js @@ -3,6 +3,13 @@ import {execSync} from "child_process"; var GithubToken = process.argv[2]; var PRNumber = process.argv[3]; +function extractReleaseNotes(body) { + const match = body + .replace(/\r\n/g, "\n") + .match(//i); + return match ? match[1].trim() : ""; +} +var CurrentNotes = extractReleaseNotes(String(process.argv[4] || "")); process.env.GITHUB_TOKEN = GithubToken; execSync("gh pr checkout " + PRNumber); console.info("PR #" + PRNumber + " has been checked out."); @@ -45,6 +52,7 @@ console.log("Last JSON version : " + LastJSONVersion); console.log("Last PR : " + LastPR); console.log("Last type : " + LastType); console.log("npm version : " + NpmVersion); +console.log("Current notes : " + (CurrentNotes || "No release notes were provided for this release.")); if (LastJSONVersion != LastJSVersion) { console.error("XMOJ.user.js and Update.json have different patch versions."); @@ -67,7 +75,7 @@ JSONObject.UpdateHistory[CurrentVersion] = { "UpdateDate": Date.now(), "Prerelease": false, "UpdateContents": [], - "Notes": "No release notes were provided for this release." + "Notes": CurrentNotes || "No release notes were provided for this release." }; for (var i = Object.keys(JSONObject.UpdateHistory).length - 2; i >= 0; i--) { diff --git a/Update/UpdateVersion.js b/Update/UpdateVersion.js index 1fab8e5e..840ec2b8 100644 --- a/Update/UpdateVersion.js +++ b/Update/UpdateVersion.js @@ -47,6 +47,13 @@ execSync("git config --global user.email \"github-actions[bot]@users.noreply.git execSync("git config --global user.name \"github-actions[bot]\""); var CurrentPR = Number(PRNumber); var CurrentDescription = String(process.argv[4]); +function extractReleaseNotes(body) { + const match = body + .replace(/\r\n/g, "\n") + .match(//i); + return match ? match[1].trim() : ""; +} +var CurrentNotes = extractReleaseNotes(String(process.argv[5] || "")); if (LastJSVersion != NpmVersion) { console.warn("Assuming you manually ran npm version."); } else if (!(LastPR == CurrentPR && NpmVersion == LastJSVersion)) { @@ -58,6 +65,7 @@ var CurrentVersion = execSync("jq -r '.version' package.json").toString().trim() console.log("Current version : " + CurrentVersion); console.log("Current PR : " + CurrentPR); console.log("Current description: " + CurrentDescription); +console.log("Current notes : " + (CurrentNotes || "No release notes were provided for this release.")); var ChangedFileList = execSync("gh pr diff " + CurrentPR + " --name-only").toString().trim().split("\n"); console.log("Changed files : " + ChangedFileList.join(", ")); @@ -67,6 +75,9 @@ if (LastPR == CurrentPR && NpmVersion == LastJSVersion) { console.warn("Warning: PR is the same as last version."); JSONObject.UpdateHistory[LastJSVersion].UpdateDate = Date.now(); JSONObject.UpdateHistory[LastJSVersion].UpdateContents[0].Description = CurrentDescription; + if (CurrentNotes) { + JSONObject.UpdateHistory[LastJSVersion].Notes = CurrentNotes; + } CommitMessage = "Update time and description of " + LastJSVersion; } else if (ChangedFileList.indexOf("XMOJ.user.js") == -1) { console.warn("XMOJ.user.js is not changed, so the version should not be updated."); @@ -79,7 +90,7 @@ if (LastPR == CurrentPR && NpmVersion == LastJSVersion) { "PR": CurrentPR, "Description": CurrentDescription }], - "Notes": "No release notes were provided for this release." + "Notes": CurrentNotes || "No release notes were provided for this release." }; writeFileSync(JSFileName, JSFileContent.replace(/@version(\s+)\d+\.\d+\.\d+/, "@version$1" + CurrentVersion), "utf8"); console.warn("XMOJ.user.js has been updated."); From f86a45eeda019268fdabb67f03000f616a31da95 Mon Sep 17 00:00:00 2001 From: Zhu Chenrui Date: Sun, 24 Aug 2025 11:36:00 +0800 Subject: [PATCH 03/17] Update bug.yml Signed-off-by: Zhu Chenrui (cherry picked from commit 07d7590955a2913e422e500613f140f7619ff364) Update feature.yml Signed-off-by: Zhu Chenrui (cherry picked from commit 1a99430f1edf90e782b984b956dcb78efd6e88db) Update docs.yml Signed-off-by: Zhu Chenrui (cherry picked from commit 6017bcf99acdb37c877633c4f13f9851393fa1af) --- .github/ISSUE_TEMPLATE/bug.yml | 1 + .github/ISSUE_TEMPLATE/docs.yml | 1 + .github/ISSUE_TEMPLATE/feature.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9e03a313..bf1f6784 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,6 +1,7 @@ name: 综合 Bug 反馈 description: 有功能有问题 labels: bug, needs-triage +type: Bug # assignees: PythonSmall-Q, boomzero, shihongxi title: "[Bug]" body: diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml index 8152f0a9..982e8c57 100644 --- a/.github/ISSUE_TEMPLATE/docs.yml +++ b/.github/ISSUE_TEMPLATE/docs.yml @@ -1,6 +1,7 @@ name: 帮助文档反馈 description: 帮助文档有问题 labels: docs, needs-triage +type: Task title: "[Docs]" body: - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 9fbae4f5..90d3177d 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,6 +1,7 @@ name: 新功能提案 description: 对已有功能的大幅度修改,或添加一个新内容或选项 labels: enhancement, needs-triage +type: Feature #assignees: PythonSmall-Q, boomzero, shihongxi title: "[Feature Request]" body: From 54d39cf3357e0e94ce95c50559de1a735b272872 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Sat, 7 Feb 2026 09:14:08 +0800 Subject: [PATCH 04/17] Update GitHub Actions workflow to skip bot triggers Signed-off-by: Shan Wenxiao --- .github/workflows/UpdateToRelease.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/UpdateToRelease.yml b/.github/workflows/UpdateToRelease.yml index e401f757..5f82aa0f 100644 --- a/.github/workflows/UpdateToRelease.yml +++ b/.github/workflows/UpdateToRelease.yml @@ -11,11 +11,13 @@ on: jobs: UpdateToRelease: runs-on: ubuntu-latest + # 添加条件:如果是 bot 触发的则跳过,避免无限循环 + if: github.event.pull_request.user.login != 'github-actions[bot]' permissions: pull-requests: write contents: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Update to release @@ -25,7 +27,7 @@ jobs: gh pr comment ${{ github.event.pull_request.number }} --body "请向\`dev\`分支提交pull request, 本pull request将被自动关闭" gh pr close ${{ github.event.pull_request.number }} else - node ./Update/UpdateToRelease.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.number }} "$PR_BODY" + node ./Update/UpdateToRelease.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.pull_request.number }} "$PR_BODY" fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 27863a74aad62f6ed0dd7d9b2a4da6bfc3ec0270 Mon Sep 17 00:00:00 2001 From: boomzero Date: Wed, 11 Feb 2026 20:59:12 +0800 Subject: [PATCH 05/17] Prevent UpdateVersion from running if last commit was by github-actions[bot] This prevents infinite loops where the bot commits version updates, which triggers the workflow again, causing another commit. Co-Authored-By: Claude Sonnet 4.5 --- Update/UpdateVersion.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Update/UpdateVersion.js b/Update/UpdateVersion.js index 840ec2b8..cd744cd8 100644 --- a/Update/UpdateVersion.js +++ b/Update/UpdateVersion.js @@ -7,6 +7,14 @@ process.env.GITHUB_TOKEN = GithubToken; execSync("gh pr checkout " + PRNumber); console.info("PR #" + PRNumber + " has been checked out."); +// Check if the last commit was made by github-actions[bot] +const lastCommitAuthor = execSync("git log -1 --pretty=format:'%an'").toString().trim(); +console.log("Last commit author: " + lastCommitAuthor); +if (lastCommitAuthor === "github-actions[bot]") { + console.log("Last commit was made by github-actions[bot]. Skipping to prevent infinite loop."); + process.exit(0); +} + const JSONFileName = "./Update.json"; const JSFileName = "./XMOJ.user.js"; var JSONFileContent = readFileSync(JSONFileName, "utf8"); From fab65934b678092f505e2c20c02d2ff4cc707788 Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 23 Feb 2026 20:06:47 +0800 Subject: [PATCH 06/17] Allow metadata updates on edited PRs after bot version commit The last-commit-author guard now only exits for non-edited events, so PR title/body changes still update Update.json metadata even when the branch tip is a github-actions[bot] commit. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/UpdateVersion.yml | 2 +- Update/UpdateVersion.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/UpdateVersion.yml b/.github/workflows/UpdateVersion.yml index 74a49224..b8409176 100644 --- a/.github/workflows/UpdateVersion.yml +++ b/.github/workflows/UpdateVersion.yml @@ -26,4 +26,4 @@ jobs: - name: Update version env: PR_BODY: ${{ github.event.pull_request.body }} - run: node ./Update/UpdateVersion.js ${{ steps.generate_token.outputs.token }} ${{ github.event.number }} "${{ github.event.pull_request.title }}" "$PR_BODY" + run: node ./Update/UpdateVersion.js ${{ steps.generate_token.outputs.token }} ${{ github.event.number }} "${{ github.event.pull_request.title }}" "$PR_BODY" "${{ github.event.action }}" diff --git a/Update/UpdateVersion.js b/Update/UpdateVersion.js index cd744cd8..891d2c46 100644 --- a/Update/UpdateVersion.js +++ b/Update/UpdateVersion.js @@ -8,9 +8,14 @@ execSync("gh pr checkout " + PRNumber); console.info("PR #" + PRNumber + " has been checked out."); // Check if the last commit was made by github-actions[bot] +// Only skip for synchronize events (push-triggered) to prevent infinite loops. +// For edited events (PR title/body changes), allow metadata updates even when +// the branch tip is a bot commit. +const eventAction = String(process.argv[6] || ""); const lastCommitAuthor = execSync("git log -1 --pretty=format:'%an'").toString().trim(); console.log("Last commit author: " + lastCommitAuthor); -if (lastCommitAuthor === "github-actions[bot]") { +console.log("Event action : " + eventAction); +if (lastCommitAuthor === "github-actions[bot]" && eventAction !== "edited") { console.log("Last commit was made by github-actions[bot]. Skipping to prevent infinite loop."); process.exit(0); } From 2f7e9d6a45f04f3fe3238f45ba895fe7277c9ecf Mon Sep 17 00:00:00 2001 From: boomzero Date: Mon, 23 Feb 2026 20:06:47 +0800 Subject: [PATCH 07/17] Allow metadata updates on edited PRs after bot version commit Exclude all bot actors (not just github-actions[bot]) from triggering the UpdateVersion workflow, preventing loops from AI code review bots. Allow edited events through the script-level guard so PR title/body changes still update Update.json metadata. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/UpdateVersion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/UpdateVersion.yml b/.github/workflows/UpdateVersion.yml index b8409176..6112841f 100644 --- a/.github/workflows/UpdateVersion.yml +++ b/.github/workflows/UpdateVersion.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write contents: write runs-on: ubuntu-latest - if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'github-actions[bot]' + if: github.event.pull_request.head.repo.full_name == github.repository && !endsWith(github.actor, '[bot]') steps: - name: Generate a token id: generate_token From a5182a6509e4b25aab222bbd6ce022c9b4102a6b Mon Sep 17 00:00:00 2001 From: def-WA2025 Date: Thu, 12 Mar 2026 23:48:44 +0800 Subject: [PATCH 08/17] Fix Problem Switcher Not Update --- XMOJ.user.js | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 4c9d072c..1e931532 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -533,6 +533,29 @@ let RequestAPI = (Action, Data, CallBack) => { } }; +unsafeWindow.GetContestProblemList = async function(RefreshList) { + try { + const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + SearchParams.get("cid")); + const res = await contestReq.text(); + if (contestReq.status === 200 && res.indexOf("比赛尚未开始或私有,不能查看题目。") === -1) { + const parser = new DOMParser(); + const dom = parser.parseFromString(res, "text/html"); + const rows = (dom.querySelector("#problemset > tbody")).rows; + let problemList = []; + for (let i = 0; i < rows.length; i++) { + problemList.push({ + "title": rows[i].children[2].innerText, + "url": rows[i].children[2].children[0].href + }); + } + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList", JSON.stringify(problemList)); + if (RefreshList) location.reload(); + } + } catch (e) { + console.error(e); + } +} + // WebSocket Notification System let NotificationSocket = null; let NotificationSocketReconnectAttempts = 0; @@ -2270,22 +2293,8 @@ async function main() { } let ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList"); if (ContestProblemList == null) { - const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + SearchParams.get("cid")); - const res = await contestReq.text(); - if (contestReq.status === 200 && res.indexOf("比赛尚未开始或私有,不能查看题目。") === -1) { - const parser = new DOMParser(); - const dom = parser.parseFromString(res, "text/html"); - const rows = (dom.querySelector("#problemset > tbody")).rows; - let problemList = []; - for (let i = 0; i < rows.length; i++) { - problemList.push({ - "title": rows[i].children[2].innerText, - "url": rows[i].children[2].children[0].href - }); - } - localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList", JSON.stringify(problemList)); - ContestProblemList = JSON.stringify(problemList); - } + unsafeWindow.GetContestProblemList(false); + ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList"); } let problemSwitcher = document.createElement("div"); @@ -2308,6 +2317,7 @@ async function main() { problemSwitcher.style.flexDirection = "column"; let problemList = JSON.parse(ContestProblemList); + problemSwitcher.innerHTML += `刷新`; for (let i = 0; i < problemList.length; i++) { let buttonText = ""; if (i < 26) { From ba48f2258bdcdda4ecffb1bc14c6b6884b11fe31 Mon Sep 17 00:00:00 2001 From: def-WA2025 Date: Fri, 13 Mar 2026 23:41:11 +0800 Subject: [PATCH 09/17] Fix Problem Switcher Not Update --- XMOJ.user.js | 489 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 488 insertions(+), 1 deletion(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 1e931532..fa729d37 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.3.0 +// @version 3.3.1 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen @@ -2144,6 +2144,8 @@ async function main() { }, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, { "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { + "ID": "ImageEnlarger", "Type": "A", "Name": "图片放大功能" + }, { "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" }, { "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" @@ -5582,6 +5584,491 @@ int main() } } } + + // Image Enlargement Feature + if (UtilityEnabled("ImageEnlarger")) { + try { + // Add CSS styles for the enlarger + let EnlargerStyle = document.createElement("style"); + EnlargerStyle.textContent = ` + .xmoj-image-preview { + cursor: pointer; + } + + .xmoj-image-preview:hover { + opacity: 0.8; + transition: opacity 0.2s ease; + } + + + .xmoj-image-modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + } + + .xmoj-image-modal.show { + display: flex; + flex-direction: column; + } + + .xmoj-image-modal-content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; + } + + .xmoj-image-modal-image { + max-width: 100%; + max-height: 100%; + object-fit: contain; + } + + .xmoj-image-modal-toolbar { + display: flex; + justify-content: center; + gap: 10px; + padding: 15px; + background-color: rgba(0, 0, 0, 0.5); + flex-wrap: wrap; + } + + .xmoj-image-modal-toolbar button { + padding: 8px 16px; + background-color: #0d6efd; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s ease; + } + + .xmoj-image-modal-toolbar button:hover { + background-color: #0b5ed7; + } + + .xmoj-image-modal-toolbar button:active { + background-color: #0a58ca; + } + + .xmoj-image-modal-close { + position: absolute; + top: 20px; + right: 30px; + color: white; + background: none; + border: none; + padding: 0; + line-height: 1; + font-size: 40px; + font-weight: bold; + cursor: pointer; + transition: color 0.2s ease; + z-index: 1; + } + + .xmoj-image-modal-close:hover { + color: #ccc; + } + + .xmoj-image-modal-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: white; + border: none; + padding: 20px 12px; + cursor: pointer; + font-size: 28px; + transition: background-color 0.2s ease; + user-select: none; + -webkit-user-select: none; + } + + .xmoj-image-modal-nav:hover { + background: rgba(0, 0, 0, 0.8); + } + + .xmoj-image-modal-nav:disabled { + opacity: 0.3; + cursor: default; + } + + .xmoj-image-modal-nav-prev { + left: 0; + border-radius: 0 4px 4px 0; + } + + .xmoj-image-modal-nav-next { + right: 0; + border-radius: 4px 0 0 4px; + } + `; + document.head.appendChild(EnlargerStyle); + + // Create modal element + let ImageModal = document.createElement("div"); + ImageModal.className = "xmoj-image-modal"; + ImageModal.id = "xmoj-image-modal"; + + let CloseButton = document.createElement("button"); + CloseButton.className = "xmoj-image-modal-close"; + CloseButton.type = "button"; + CloseButton.setAttribute("aria-label", "关闭图片"); + CloseButton.title = "关闭图片"; + CloseButton.innerHTML = "×"; + ImageModal.appendChild(CloseButton); + + let ModalContent = document.createElement("div"); + ModalContent.className = "xmoj-image-modal-content"; + + let PrevBtn = document.createElement("button"); + PrevBtn.className = "xmoj-image-modal-nav xmoj-image-modal-nav-prev"; + PrevBtn.type = "button"; + PrevBtn.setAttribute("aria-label", "上一张"); + PrevBtn.innerHTML = "❮"; + ModalContent.appendChild(PrevBtn); + + let NextBtn = document.createElement("button"); + NextBtn.className = "xmoj-image-modal-nav xmoj-image-modal-nav-next"; + NextBtn.type = "button"; + NextBtn.setAttribute("aria-label", "下一张"); + NextBtn.innerHTML = "❯"; + ModalContent.appendChild(NextBtn); + + let ModalImage = document.createElement("img"); + ModalImage.className = "xmoj-image-modal-image"; + ModalContent.appendChild(ModalImage); + ImageModal.appendChild(ModalContent); + + let Toolbar = document.createElement("div"); + Toolbar.className = "xmoj-image-modal-toolbar"; + + let ZoomInBtn = document.createElement("button"); + ZoomInBtn.innerHTML = "放大 (+)"; + ZoomInBtn.type = "button"; + Toolbar.appendChild(ZoomInBtn); + + let ZoomOutBtn = document.createElement("button"); + ZoomOutBtn.innerHTML = "缩小 (-)"; + ZoomOutBtn.type = "button"; + Toolbar.appendChild(ZoomOutBtn); + + let ResetZoomBtn = document.createElement("button"); + ResetZoomBtn.innerHTML = "重置大小"; + ResetZoomBtn.type = "button"; + Toolbar.appendChild(ResetZoomBtn); + + let SaveBtn = document.createElement("button"); + SaveBtn.innerHTML = "保存图片"; + SaveBtn.type = "button"; + Toolbar.appendChild(SaveBtn); + + ImageModal.appendChild(Toolbar); + document.body.appendChild(ImageModal); + + // Zoom level and navigation state + let CurrentZoom = 1; + const ZoomStep = 0.1; + const MinZoom = 0.1; + const MaxZoom = 5; + let ImageList = []; + let CurrentImageIndex = -1; + let PanX = 0; + let PanY = 0; + let IsDragging = false; + let DragStartX = 0; + let DragStartY = 0; + let DragStartPanX = 0; + let DragStartPanY = 0; + let IsTouchPanning = false; + let TouchStartX = 0; + let TouchStartY = 0; + let TouchPanStartPanX = 0; + let TouchPanStartPanY = 0; + + // Function to update image transform (zoom + pan) + let UpdateImageSize = () => { + ModalImage.style.transform = `translate(${PanX}px, ${PanY}px) scale(${CurrentZoom})`; + ModalImage.style.transition = IsDragging ? "none" : "transform 0.2s ease"; + let CursorStyle = CurrentZoom > 1 ? "grab" : ""; + ModalImage.style.cursor = CursorStyle; + ModalContent.style.cursor = CursorStyle; + }; + + // Function to update prev/next button state + let UpdateNavButtons = () => { + let HasMultiple = ImageList.length > 1; + PrevBtn.style.display = HasMultiple ? "" : "none"; + NextBtn.style.display = HasMultiple ? "" : "none"; + PrevBtn.disabled = CurrentImageIndex <= 0; + NextBtn.disabled = CurrentImageIndex >= ImageList.length - 1; + }; + + // Function to navigate to a specific image by index + let NavigateTo = (index) => { + if (index < 0 || index >= ImageList.length) return; + CurrentImageIndex = index; + CurrentZoom = 1; + PanX = 0; + PanY = 0; + ModalImage.src = ImageList[CurrentImageIndex]; + UpdateNavButtons(); + UpdateImageSize(); + }; + + // Function to open modal + let OpenImageModal = (imgElement) => { + let PreviewImages = [...document.querySelectorAll("img.xmoj-image-preview")]; + ImageList = PreviewImages.map(img => img.currentSrc || img.src).filter(src => src); + CurrentImageIndex = PreviewImages.indexOf(imgElement); + if (CurrentImageIndex === -1) { + ImageList = [(imgElement.currentSrc || imgElement.src)]; + CurrentImageIndex = 0; + } + CurrentZoom = 1; + PanX = 0; + PanY = 0; + ModalImage.src = ImageList[CurrentImageIndex]; + ImageModal.classList.add("show"); + UpdateNavButtons(); + UpdateImageSize(); + }; + + // Function to close modal + let CloseImageModal = () => { + ImageModal.classList.remove("show"); + }; + + // Close button click + CloseButton.addEventListener("click", CloseImageModal); + + // Close when clicking outside the image + ImageModal.addEventListener("click", (e) => { + if (e.target === ImageModal || e.target === ModalContent) { + CloseImageModal(); + } + }); + + // Keyboard shortcuts + document.addEventListener("keydown", (e) => { + if (ImageModal.classList.contains("show")) { + if (e.key === "Escape") { + CloseImageModal(); + } else if (e.key === "+") { + ZoomInBtn.click(); + } else if (e.key === "-") { + ZoomOutBtn.click(); + } else if (e.key === "ArrowLeft") { + NavigateTo(CurrentImageIndex - 1); + } else if (e.key === "ArrowRight") { + NavigateTo(CurrentImageIndex + 1); + } + } + }); + + // Touch events: pan when zoomed, swipe to navigate when at zoom level 1 + ModalContent.addEventListener("touchstart", (e) => { + if (e.touches.length !== 1) return; + TouchStartX = e.touches[0].clientX; + TouchStartY = e.touches[0].clientY; + if (CurrentZoom > 1) { + IsTouchPanning = true; + TouchPanStartPanX = PanX; + TouchPanStartPanY = PanY; + } else { + IsTouchPanning = false; + } + }, { passive: true }); + + ModalContent.addEventListener("touchmove", (e) => { + if (!IsTouchPanning || e.touches.length !== 1) return; + PanX = TouchPanStartPanX + (e.touches[0].clientX - TouchStartX); + PanY = TouchPanStartPanY + (e.touches[0].clientY - TouchStartY); + UpdateImageSize(); + e.preventDefault(); + }, { passive: false }); + + ModalContent.addEventListener("touchend", (e) => { + if (IsTouchPanning) { + IsTouchPanning = false; + return; + } + let TouchEndX = e.changedTouches[0].clientX; + let TouchEndY = e.changedTouches[0].clientY; + let DeltaX = TouchEndX - TouchStartX; + let DeltaY = TouchEndY - TouchStartY; + const SwipeThreshold = 50; + if (Math.abs(DeltaX) > SwipeThreshold && Math.abs(DeltaX) > Math.abs(DeltaY)) { + if (DeltaX < 0) { + NavigateTo(CurrentImageIndex + 1); + } else { + NavigateTo(CurrentImageIndex - 1); + } + } + }, { passive: true }); + + // Mouse drag to pan when zoomed + ModalContent.addEventListener("mousedown", (e) => { + if (CurrentZoom <= 1) return; + if (e.target.tagName.toUpperCase() === "BUTTON") return; + IsDragging = true; + DragStartX = e.clientX; + DragStartY = e.clientY; + DragStartPanX = PanX; + DragStartPanY = PanY; + ModalImage.style.cursor = "grabbing"; + ModalContent.style.cursor = "grabbing"; + e.preventDefault(); + }); + + document.addEventListener("mousemove", (e) => { + if (!IsDragging) return; + PanX = DragStartPanX + (e.clientX - DragStartX); + PanY = DragStartPanY + (e.clientY - DragStartY); + UpdateImageSize(); + }); + + document.addEventListener("mouseup", () => { + if (IsDragging) { + IsDragging = false; + let CursorStyle = CurrentZoom > 1 ? "grab" : ""; + ModalImage.style.cursor = CursorStyle; + ModalContent.style.cursor = CursorStyle; + } + }); + + // Mouse wheel to zoom in/out + ModalContent.addEventListener("wheel", (e) => { + e.preventDefault(); + let ZoomDelta = e.deltaY > 0 ? -ZoomStep : ZoomStep; + CurrentZoom = Math.max(MinZoom, Math.min(MaxZoom, CurrentZoom + ZoomDelta)); + UpdateImageSize(); + }, { passive: false }); + + // Navigation button clicks + PrevBtn.addEventListener("click", (e) => { + e.stopPropagation(); + NavigateTo(CurrentImageIndex - 1); + }); + + NextBtn.addEventListener("click", (e) => { + e.stopPropagation(); + NavigateTo(CurrentImageIndex + 1); + }); + + // Zoom controls + ZoomInBtn.addEventListener("click", () => { + CurrentZoom = Math.min(CurrentZoom + ZoomStep, MaxZoom); + UpdateImageSize(); + }); + + ZoomOutBtn.addEventListener("click", () => { + CurrentZoom = Math.max(CurrentZoom - ZoomStep, MinZoom); + UpdateImageSize(); + }); + + ResetZoomBtn.addEventListener("click", () => { + CurrentZoom = 1; + PanX = 0; + PanY = 0; + UpdateImageSize(); + }); + + // Save/Download image: fetch via GM_xmlhttpRequest to bypass CORS, then use blob URL for reliable download + SaveBtn.addEventListener("click", () => { + let src = ModalImage.src; + let urlPath = src.split("?")[0]; + let filename = urlPath.split("/").pop() || "image.png"; + GM_xmlhttpRequest({ + method: "GET", + url: src, + responseType: "blob", + onload: (resp) => { + let BlobUrl = URL.createObjectURL(resp.response); + let Link = document.createElement("a"); + Link.href = BlobUrl; + Link.download = filename; + document.body.appendChild(Link); + Link.click(); + document.body.removeChild(Link); + setTimeout(() => URL.revokeObjectURL(BlobUrl), 100); + }, + onerror: () => { + let Link = document.createElement("a"); + Link.href = src; + Link.download = filename; + Link.target = "_blank"; + document.body.appendChild(Link); + Link.click(); + document.body.removeChild(Link); + } + }); + }); + + // Apply to all images on the page + let ApplyEnlargerToImage = (img) => { + const effectiveSrc = img.currentSrc || img.src; + if (!img.classList.contains("xmoj-image-preview") && + !img.closest(".xmoj-image-modal") && + effectiveSrc && + !effectiveSrc.includes("gravatar") && + !effectiveSrc.includes("cravatar")) { + + img.classList.add("xmoj-image-preview"); + if (!img.title) { + img.title = "点击放大"; + } + img.addEventListener("click", (e) => { + e.stopPropagation(); + OpenImageModal(img); + }); + } + }; + + let ApplyEnlargerToImages = () => { + document.querySelectorAll("img").forEach(ApplyEnlargerToImage); + }; + + // Apply to existing images + ApplyEnlargerToImages(); + + // Apply to dynamically added images + let Observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType !== Node.ELEMENT_NODE) return; + if (node.tagName === "IMG") { + ApplyEnlargerToImage(node); + } else { + node.querySelectorAll("img").forEach(ApplyEnlargerToImage); + } + }); + }); + }); + + Observer.observe(document.body, { + childList: true, + subtree: true + }); + + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } } catch (e) { console.error(e); if (UtilityEnabled("DebugMode")) { From 478101d4d2873ad0c781253aec3438966e58f200 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 15:47:40 +0000 Subject: [PATCH 10/17] 3.3.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ffb1aa4..7a191097 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.3.1", + "version": "3.3.2", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From 4f5c2e01d4680f1dfb10fc50b939a71b8d9ccc3f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 15:47:46 +0000 Subject: [PATCH 11/17] Update version info to 3.3.2 --- Update.json | 13 ++++++++++++- XMOJ.user.js | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Update.json b/Update.json index 7dce3385..2ea96366 100644 --- a/Update.json +++ b/Update.json @@ -3433,6 +3433,17 @@ } ], "Notes": "No release notes were provided for this release." + }, + "3.3.2": { + "UpdateDate": 1773416860439, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 933, + "Description": "Fix problem switcher not update" + } + ], + "Notes": "Fix ProblemSwitcher Not Update" } } -} +} \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index fa729d37..a8e50a9d 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.3.1 +// @version 3.3.2 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From 77100890b81825169cb82f9620ff543937ffaae0 Mon Sep 17 00:00:00 2001 From: zsTree Date: Sat, 14 Mar 2026 00:00:16 +0800 Subject: [PATCH 12/17] Fix Version List (1.999999.0 -> 1.10.0) Signed-off-by: zsTree --- Update.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Update.json b/Update.json index 2ea96366..b2f7d28b 100644 --- a/Update.json +++ b/Update.json @@ -2937,7 +2937,7 @@ ], "Notes": "No release notes were provided for this release." }, - "1.999990.0": { + "1.10.0": { "UpdateDate": 1753443146018, "Prerelease": false, "UpdateContents": [ @@ -3446,4 +3446,4 @@ "Notes": "Fix ProblemSwitcher Not Update" } } -} \ No newline at end of file +} From 62b1755872093cb6831be3e033e192522f4d7587 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 16:00:39 +0000 Subject: [PATCH 13/17] Update time and description of 3.3.2 --- Update.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Update.json b/Update.json index b2f7d28b..f0334a37 100644 --- a/Update.json +++ b/Update.json @@ -3435,7 +3435,7 @@ "Notes": "No release notes were provided for this release." }, "3.3.2": { - "UpdateDate": 1773416860439, + "UpdateDate": 1773417634610, "Prerelease": true, "UpdateContents": [ { @@ -3446,4 +3446,4 @@ "Notes": "Fix ProblemSwitcher Not Update" } } -} +} \ No newline at end of file From 33535ecd09c12da1c6dedfcd5c54390e38a1d852 Mon Sep 17 00:00:00 2001 From: zsTree Date: Sat, 14 Mar 2026 00:14:43 +0800 Subject: [PATCH 14/17] Fix Problem Switcher Null Problem Signed-off-by: zsTree --- XMOJ.user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index a8e50a9d..f0644c79 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -2295,7 +2295,7 @@ async function main() { } let ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList"); if (ContestProblemList == null) { - unsafeWindow.GetContestProblemList(false); + await unsafeWindow.GetContestProblemList(false); ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList"); } @@ -6079,4 +6079,4 @@ int main() main().then(r => { console.log("XMOJ-Script loaded successfully!"); -}); \ No newline at end of file +}); From a8bf01bd86fe43b2c2763db96353bf5c8475ff85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 16:15:12 +0000 Subject: [PATCH 15/17] Update time and description of 3.3.2 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index f0334a37..095d2501 100644 --- a/Update.json +++ b/Update.json @@ -3435,7 +3435,7 @@ "Notes": "No release notes were provided for this release." }, "3.3.2": { - "UpdateDate": 1773417634610, + "UpdateDate": 1773418506203, "Prerelease": true, "UpdateContents": [ { From c34f6664723ca541abf671a982f55ef9379df2ea Mon Sep 17 00:00:00 2001 From: zsTree Date: Sat, 14 Mar 2026 21:40:00 +0800 Subject: [PATCH 16/17] Fix XSS bug Signed-off-by: zsTree --- XMOJ.user.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index f0644c79..897238fe 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1574,6 +1574,9 @@ async function main() { display: none !important; } } + .refreshList { + cursor: pointer; + } /* Contain images */ img { @@ -1639,6 +1642,11 @@ async function main() { border: 1px solid var(--bs-secondary-bg); border-top: none; border-radius: 0 0 0.3rem 0.3rem; + } + .refreshList { + cursor: pointer; + color: #6c757d; + text-decoration: none; }`; } if (UtilityEnabled("AddAnimation")) { @@ -2319,7 +2327,7 @@ async function main() { problemSwitcher.style.flexDirection = "column"; let problemList = JSON.parse(ContestProblemList); - problemSwitcher.innerHTML += `刷新`; + problemSwitcher.innerHTML += `刷新`; for (let i = 0; i < problemList.length; i++) { let buttonText = ""; if (i < 26) { From c87d08ff4a26c4dedd80c0f4c007f7f35a9f2506 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Mar 2026 13:40:27 +0000 Subject: [PATCH 17/17] Update time and description of 3.3.2 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 095d2501..65325b1f 100644 --- a/Update.json +++ b/Update.json @@ -3435,7 +3435,7 @@ "Notes": "No release notes were provided for this release." }, "3.3.2": { - "UpdateDate": 1773418506203, + "UpdateDate": 1773495619873, "Prerelease": true, "UpdateContents": [ {