From b789bf28da38e79ce5aee665b90df7c8861d9fc4 Mon Sep 17 00:00:00 2001 From: Zhu Chenrui Date: Fri, 25 Jul 2025 19:46:29 +0800 Subject: [PATCH 01/26] 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/26] 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/26] 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 a9420ac5a50289e7cfed9cafdc5e15380bb35ae2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Nov 2025 22:32:39 +0000 Subject: [PATCH 04/26] Refactor userscript structure with modular architecture This commit refactors the XMOJ-Script from a single 5000-line file into a modular structure using Rollup bundler for better maintainability. Changes: - Added Rollup bundler configuration - Separated code into modules: - src/core/: Core application logic and configuration - src/utils/: Utility functions (HTML, time, format, API, etc.) - src/features/: Feature modules (to be expanded) - Created main entry point (src/main.js) - Updated package.json with build scripts - Added README_REFACTORING.md with structure documentation All original functionality is preserved. The build output (dist/XMOJ.user.js) maintains the same userscript header and behavior. Build instructions: - npm install (install dependencies) - npm run build (build once) - npm run watch (watch and rebuild) Future work: Individual features can now be extracted into separate modules under src/features/ for better organization. --- README_REFACTORING.md | 118 + dist/XMOJ.user.js | 5125 ++++++++++++++++++++++++++++++++++++ package.json | 12 +- rollup.config.js | 21 + src/core/bootstrap.js | 4396 +++++++++++++++++++++++++++++++ src/core/config.js | 24 + src/core/constants.js | 8 + src/core/menu.js | 28 + src/features/index-page.js | 256 ++ src/main.js | 56 + src/utils/alerts.js | 14 + src/utils/api.js | 82 + src/utils/credentials.js | 47 + src/utils/format.js | 62 + src/utils/html.js | 43 + src/utils/mathjax.js | 30 + src/utils/table.js | 24 + src/utils/time.js | 101 + src/utils/user.js | 172 ++ src/utils/version.js | 26 + 20 files changed, 10643 insertions(+), 2 deletions(-) create mode 100644 README_REFACTORING.md create mode 100644 dist/XMOJ.user.js create mode 100644 rollup.config.js create mode 100644 src/core/bootstrap.js create mode 100644 src/core/config.js create mode 100644 src/core/constants.js create mode 100644 src/core/menu.js create mode 100644 src/features/index-page.js create mode 100644 src/main.js create mode 100644 src/utils/alerts.js create mode 100644 src/utils/api.js create mode 100644 src/utils/credentials.js create mode 100644 src/utils/format.js create mode 100644 src/utils/html.js create mode 100644 src/utils/mathjax.js create mode 100644 src/utils/table.js create mode 100644 src/utils/time.js create mode 100644 src/utils/user.js create mode 100644 src/utils/version.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md new file mode 100644 index 00000000..492d627a --- /dev/null +++ b/README_REFACTORING.md @@ -0,0 +1,118 @@ +# XMOJ-Script Refactoring + +This project has been refactored to use a modular structure with a bundler (Rollup) for better maintainability. + +## Project Structure + +``` +XMOJ-Script/ +├── src/ # Source code (modular) +│ ├── core/ # Core application logic +│ │ ├── constants.js # Constants and configuration values +│ │ ├── config.js # Feature configuration (UtilityEnabled) +│ │ ├── bootstrap.js # Main application logic and initialization +│ │ └── menu.js # Greasemonkey menu commands +│ ├── utils/ # Utility modules +│ │ ├── alerts.js # Alert utilities +│ │ ├── api.js # API request utilities +│ │ ├── credentials.js # Credential storage utilities +│ │ ├── format.js # Size formatting utilities +│ │ ├── html.js # HTML escaping and purifying +│ │ ├── mathjax.js # MathJax rendering +│ │ ├── table.js # Table styling utilities +│ │ ├── time.js # Time formatting utilities +│ │ ├── user.js # User information utilities +│ │ └── version.js # Version comparison utilities +│ ├── features/ # Feature modules (to be extracted) +│ └── main.js # Main entry point +├── dist/ # Built output +│ └── XMOJ.user.js # Bundled userscript (generated) +├── rollup.config.js # Rollup bundler configuration +├── package.json # NPM package configuration +└── XMOJ.user.js # Original userscript (legacy) +``` + +## Building + +```bash +# Install dependencies +npm install + +# Build once +npm run build + +# Watch for changes and rebuild automatically +npm run watch +``` + +The bundled output will be in `dist/XMOJ.user.js`. + +## Features + +The userscript includes the following features (controlled via `UtilityEnabled`): + +- AddAnimation - Add animations to UI elements +- AddColorText - Add colored text +- AddUnits - Add units to numbers (KB, MB, etc.) +- ApplyData - Apply user data +- AutoCheat - Auto-cheat features +- AutoCountdown - Automatic countdown +- AutoLogin - Automatic login +- AutoO2 - Auto O2 compilation flag +- AutoRefresh - Auto-refresh pages +- BBSPopup - BBS popup notifications +- CompareSource - Compare source code +- CompileError - Compile error enhancements +- CopyMD - Copy as Markdown +- CopySamples - Copy sample inputs/outputs +- DarkMode - Dark mode theme +- DebugMode - Debug mode logging +- Discussion - Discussion features +- DownloadPlayback - Download playback +- ExportACCode - Export AC code +- IOFile - IO file handling +- ImproveACRate - Improve AC rate display +- LoginFailed - Login failure handling +- MessagePopup - Message popup notifications +- MoreSTD - More standard solutions +- NewBootstrap - New Bootstrap UI +- NewDownload - New download features +- NewTopBar - New top navigation bar +- OpenAllProblem - Open all problems +- ProblemSwitcher - Problem switcher +- Rating - User rating display +- RefreshSolution - Refresh solution display +- RemoveAlerts - Remove alert popups +- RemoveUseless - Remove useless elements +- ReplaceLinks - Replace links +- ReplaceXM - Replace "小明" with "高老师" +- ReplaceYN - Replace Y/N text +- ResetType - Reset type display +- SavePassword - Save password +- SuperDebug - Super debug mode +- Translate - Translation features +- UploadStd - Upload standard solutions + +## Future Improvements + +Individual features should be extracted into separate modules under `src/features/` for better organization and maintainability. Each feature module should: + +1. Export an initialization function +2. Only activate when `UtilityEnabled("FeatureName")` returns true +3. Contain all code specific to that feature + +This allows for: +- Better code organization +- Easier testing of individual features +- Ability to enable/disable features independently +- Simpler debugging + +## Development + +The codebase is now set up for easier development: + +1. Edit files in `src/` +2. Run `npm run watch` to automatically rebuild on changes +3. Test the bundled output in `dist/XMOJ.user.js` + +All original functionality has been preserved - the refactoring only changes the code structure, not the behavior. diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js new file mode 100644 index 00000000..61c66ab6 --- /dev/null +++ b/dist/XMOJ.user.js @@ -0,0 +1,5125 @@ +// ==UserScript== +// @name XMOJ +// @version 2.5.0 +// @description XMOJ增强脚本 +// @author @XMOJ-Script-dev, @langningchen and the community +// @namespace https://github/langningchen +// @match *://*.xmoj.tech/* +// @match *://116.62.212.172/* +// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js +// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js +// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/clike/clike.min.js +// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/merge/merge.min.js +// @require https://gitee.com/mirrors_google/diff-match-patch/raw/master/javascript/diff_match_patch_uncompressed.js +// @require https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.2/purify.min.js +// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js +// @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js +// @grant GM_registerMenuCommand +// @grant GM_xmlhttpRequest +// @grant GM_setClipboard +// @grant unsafeWindow +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_cookie +// @homepage https://www.xmoj-bbs.me/ +// @supportURL https://support.xmoj-bbs.me/form/8050213e-c806-4680-b414-0d1c48263677 +// @connect api.xmoj-bbs.tech +// @connect api.xmoj-bbs.me +// @connect challenges.cloudflare.com +// @connect cppinsights.io +// @connect cdnjs.cloudflare.com +// @connect 127.0.0.1 +// @license GPL +// @icon data:image/png;base64,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Uk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAJcEhZcwAACxMAAAsTAQCanBgAAAPSaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA3LjItYzAwMCA3OS4xYjY1YTc5LCAyMDIyLzA2LzEzLTE3OjQ2OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZTIyMGE0MzYtMWFhYi01MjRjLTg1ZjQtNDUyYjdkYTE4ZjdhIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjlEQTA5MUE5OTM0NEYxNEM5Q0RFMEVFREY2MzA4QThEIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjk1RkQ1QzI3QzBFN0I2NDdCMTBGMzU5NjU0RUI1NjQ2IiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCAyMy41IChXaW5kb3dzKSIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZTIyMGE0MzYtMWFhYi01MjRjLTg1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 +// ==/UserScript== + +(function () { + 'use strict'; + + /** + * Feature configuration and utility enabling/disabling + */ + + /** + * Check if a utility/feature is enabled + * @param {string} Name - The name of the utility/feature + * @returns {boolean} True if enabled, false otherwise + */ + let UtilityEnabled = (Name) => { + try { + if (localStorage.getItem("UserScript-Setting-" + Name) == null) { + const defaultOffItems = ["DebugMode", "SuperDebug", "ReplaceXM"]; + localStorage.setItem("UserScript-Setting-" + Name, defaultOffItems.includes(Name) ? "false" : "true"); + } + return localStorage.getItem("UserScript-Setting-" + Name) == "true"; + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + const { SmartAlert } = require('./alerts'); + 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."); + } + } + }; + + /** + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + const AdminUserList = ["zhuchenrui2", "shanwenxiao", "chenlangning", "admin"]; + + /** + * Alert utilities + */ + + /** + * Shows an alert only if the message has changed + * @param {string} Message - The message to display + */ + let SmartAlert = (Message) => { + if (localStorage.getItem("UserScript-Alert") !== Message) { + alert(Message); + } + localStorage.setItem("UserScript-Alert", Message); + }; + + /** + * HTML utilities for escaping and purifying HTML content + */ + + + /** + * Escapes HTML special characters + * @param {string} str - The string to escape + * @returns {string} The escaped string + */ + let escapeHTML$1 = (str) => { + return str.replace(/[&<>"']/g, function (match) { + const escape = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return escape[match]; + }); + }; + + /** + * Purifies HTML content using DOMPurify + * @param {string} Input - The HTML content to purify + * @returns {string} The purified HTML content + */ + let PurifyHTML$1 = (Input) => { + try { + return DOMPurify.sanitize(Input, { + "ALLOWED_TAGS": ["a", "b", "big", "blockquote", "br", "code", "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "hr", "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "ul", "var"], + "ALLOWED_ATTR": ["abbr", "accept", "accept-charset", "accesskey", "action", "align", "alt", "axis", "border", "cellpadding", "cellspacing", "char", "charoff", "charset", "checked", "cite", "clear", "color", "cols", "colspan", "compact", "coords", "datetime", "dir", "disabled", "enctype", "for", "frame", "headers", "height", "href", "hreflang", "hspace", "ismap", "itemprop", "label", "lang", "longdesc", "maxlength", "media", "method", "multiple", "name", "nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev", "rows", "rowspan", "rules", "scope", "selected", "shape", "size", "span", "src", "start", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "vspace", "width"] + }); + } 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."); + } + } + }; + + /** + * Time utilities for formatting and converting time values + */ + + + /** + * Calculates the relative time based on the input date. + * @param {string|Date} Input - The input date. + * @returns {string} The relative time in a formatted string. + */ + let GetRelativeTime = (Input) => { + try { + Input = new Date(parseInt(Input)); + let Now = new Date().getTime(); + let Delta = Now - Input.getTime(); + let RelativeName = ""; + if (Delta < 0) { + RelativeName = "未来"; + } else if (Delta <= 1000 * 60) { + RelativeName = "刚刚"; + } else if (Delta <= 1000 * 60 * 60) { + RelativeName = Math.floor((Now - Input) / 1000 / 60) + "分钟前"; + } else if (Delta <= 1000 * 60 * 60 * 24) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60) + "小时前"; + } else if (Delta <= 1000 * 60 * 60 * 24 * 31) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24) + "天前"; + } else if (Delta <= 1000 * 60 * 60 * 24 * 365) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 31) + "个月前"; + } else { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 365) + "年前"; + } + return "" + RelativeName + ""; + } 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."); + } + } + }; + + /** + * Converts the given number of seconds to a formatted string representation of hours, minutes, and seconds. + * @param {number} InputSeconds - The number of seconds to convert. + * @returns {string} The formatted string representation of the input seconds. + */ + let SecondsToString = (InputSeconds) => { + try { + let Hours = Math.floor(InputSeconds / 3600); + let Minutes = Math.floor((InputSeconds % 3600) / 60); + let Seconds = InputSeconds % 60; + return (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; + } 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."); + } + } + }; + + /** + * Converts a string in the format "hh:mm:ss" to the equivalent number of seconds. + * @param {string} InputString - The input string to convert. + * @returns {number} The number of seconds equivalent to the input string. + */ + let StringToSeconds = (InputString) => { + try { + let SplittedString = InputString.split(":"); + return parseInt(SplittedString[0]) * 60 * 60 + parseInt(SplittedString[1]) * 60 + parseInt(SplittedString[2]); + } 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."); + } + } + }; + + /** + * Converts a time value to a string representation. + * @param {number} Time - The time value to convert. + * @returns {string|number} - The converted time value as a string, or the original value if UtilityEnabled("AddUnits") is false. + */ + let TimeToStringTime$1 = (Time) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Time < 1000) { + return Time + "ms"; + } else if (Time < 1000 * 60) { + return (Time / 1000).toFixed(2) + "s"; + } + } else { + return Time; + } + } 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."); + } + } + }; + + /** + * Formatting utilities for sizes and other values + */ + + + /** + * Converts a memory size in bytes to a human-readable string representation. + * @param {number} Memory - The memory size in bytes. + * @returns {string} The human-readable string representation of the memory size. + */ + let SizeToStringSize$1 = (Memory) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Memory < 1024) { + return Memory + "KB"; + } else if (Memory < 1024 * 1024) { + return (Memory / 1024).toFixed(2) + "MB"; + } else if (Memory < 1024 * 1024 * 1024) { + return (Memory / 1024 / 1024).toFixed(2) + "GB"; + } else { + return (Memory / 1024 / 1024 / 1024).toFixed(2) + "TB"; + } + } else { + return Memory; + } + } 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."); + } + } + }; + + /** + * Converts a code size in bytes to a human-readable string representation. + * @param {number} Memory - The code size in bytes. + * @returns {string} The human-readable string representation of the code size. + */ + let CodeSizeToStringSize$1 = (Memory) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Memory < 1024) { + return Memory + "B"; + } else if (Memory < 1024 * 1024) { + return (Memory / 1024).toFixed(2) + "KB"; + } else if (Memory < 1024 * 1024 * 1024) { + return (Memory / 1024 / 1024).toFixed(2) + "MB"; + } else { + return (Memory / 1024 / 1024 / 1024).toFixed(2) + "GB"; + } + } else { + return Memory; + } + } 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."); + } + } + }; + + /** + * Version comparison utilities + */ + + /** + * Compares two version strings + * @param {string} currVer - Current version + * @param {string} remoteVer - Remote version + * @returns {boolean} True if update is needed + */ + function compareVersions(currVer, remoteVer) { + const currParts = currVer.split('.').map(Number); + const remoteParts = remoteVer.split('.').map(Number); + + const maxLen = Math.max(currParts.length, remoteParts.length); + for (let i = 0; i < maxLen; i++) { + const curr = currParts[i] !== undefined ? currParts[i] : 0; + const remote = remoteParts[i] !== undefined ? remoteParts[i] : 0; + if (remote > curr) { + return true; // update needed + } else if (remote < curr) { + return false; // no update needed + } + } + return false; // versions are equal + } + + /** + * API request utilities + */ + + + /** + * Make an API request to the backend + * @param {string} Action - The API action + * @param {Object} Data - The data to send + * @param {Function} CallBack - Callback function to handle response + */ + let RequestAPI = (Action, Data, CallBack) => { + try { + let Session = ""; + let Temp = document.cookie.split(";"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].includes("PHPSESSID")) { + Session = Temp[i].split("=")[1]; + } + } + if (Session === "") { //The cookie is httpOnly + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + location.reload(); //Refresh the page to auth with the new PHPSESSID + }) + .catch((error) => { + console.error(error); + }); + } + + // Get current username from profile + let CurrentUsername = ""; + if (document.querySelector("#profile") !== null) { + CurrentUsername = document.querySelector("#profile").innerText; + CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); + } + + let PostData = { + "Authentication": { + "SessionID": Session, "Username": CurrentUsername, + }, "Data": Data, "Version": GM_info.script.version, "DebugMode": UtilityEnabled("DebugMode") + }; + let DataString = JSON.stringify(PostData); + if (UtilityEnabled("DebugMode")) { + console.log("Sent for", Action + ":", DataString); + } + GM_xmlhttpRequest({ + method: "POST", + url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-bbs.me/") + Action, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "XMOJ-UserID": CurrentUsername, + "XMOJ-Script-Version": GM_info.script.version, + "DebugMode": UtilityEnabled("DebugMode") + }, + data: DataString, + onload: (Response) => { + if (UtilityEnabled("DebugMode")) { + console.log("Received for", Action + ":", Response.responseText); + } + try { + CallBack(JSON.parse(Response.responseText)); + } catch (Error) { + console.log(Response.responseText); + } + } + }); + } 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."); + } + } + }; + + /** + * Credential storage utilities using the Credentials API + */ + + /** + * Store user credentials + * @param {string} username - Username + * @param {string} password - Password + */ + let storeCredential$1 = async (username, password) => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + const credential = new PasswordCredential({id: username, password: password}); + await navigator.credentials.store(credential); + } catch (e) { + console.error(e); + } + } + }; + + /** + * Get stored credentials + * @returns {Promise} The stored credentials or null + */ + let getCredential$1 = async () => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + return await navigator.credentials.get({password: true, mediation: 'optional'}); + } catch (e) { + console.error(e); + } + } + return null; + }; + + /** + * Clear stored credentials + */ + let clearCredential = async () => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + await navigator.credentials.preventSilentAccess(); + } catch (e) { + console.error(e); + } + } + }; + + /** + * MathJax rendering utilities + */ + + /** + * Render MathJax on the page + */ + let RenderMathJax$1 = async () => { + try { + if (document.getElementById("MathJax-script") === null) { + var ScriptElement = document.createElement("script"); + ScriptElement.id = "MathJax-script"; + ScriptElement.type = "text/javascript"; + ScriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.0.5/es5/tex-chtml.js"; + document.body.appendChild(ScriptElement); + await new Promise((Resolve) => { + ScriptElement.onload = () => { + Resolve(); + }; + }); + } + if (typeof MathJax !== 'undefined') { //If there is a Math expression + MathJax.startup.input[0].findTeX.options.inlineMath.push(["$", "$"]); + MathJax.startup.input[0].findTeX.getPatterns(); + MathJax.typeset(); + } + } catch (e) { + console.error(e); + } + }; + + /** + * Table utilities for styling and tidying up tables + */ + + + /** + * Tidies up the given table by applying Bootstrap styling and removing unnecessary attributes. + * + * @param {HTMLElement} Table - The table element to be tidied up. + */ + let TidyTable = (Table) => { + try { + if (UtilityEnabled("NewBootstrap") && Table != null) { + Table.className = "table table-hover"; + } + } 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."); + } + } + }; + + /** + * User information utilities + */ + + + /** + * Get user information + * @param {string} Username - The username + * @returns {Promise} User info object with Rating and EmailHash + */ + let GetUserInfo$1 = 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) { + return { + "Rating": localStorage.getItem("UserScript-User-" + Username + "-UserRating"), + "EmailHash": localStorage.getItem("UserScript-User-" + Username + "-EmailHash") + } + } + return await fetch("https://www.xmoj.tech/userinfo.php?user=" + Username).then((Response) => { + return Response.text(); + }).then((Response) => { + if (Response.indexOf("No such User!") !== -1) { + return null; + } + const ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Rating = (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; + let Temp = ParsedDocument.querySelector("#statics > tbody").children; + let Email = Temp[Temp.length - 1].children[1].innerText.trim(); + let EmailHash = CryptoJS.MD5(Email).toString(); + localStorage.setItem("UserScript-User-" + Username + "-UserRating", Rating); + if (Email == "") { + EmailHash = undefined; + } else { + localStorage.setItem("UserScript-User-" + Username + "-EmailHash", EmailHash); + } + localStorage.setItem("UserScript-User-" + Username + "-LastUpdateTime", new Date().getTime()); + return { + "Rating": Rating, "EmailHash": EmailHash + } + }); + } 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."); + } + } + }; + + /** + * Retrieves the badge information for a given user. + * + * @param {string} Username - The username of the user. + * @returns {Promise} - A promise that resolves to an object containing the badge information. + * @property {string} BackgroundColor - The background color of the badge. + * @property {string} Color - The color of the badge. + * @property {string} Content - The content of the badge. + */ + let GetUserBadge$1 = async (Username) => { + try { + if (localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime") != null && new Date().getTime() - parseInt(localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime")) < 1000 * 60 * 60 * 24) { + return { + "BackgroundColor": localStorage.getItem("UserScript-User-" + Username + "-Badge-BackgroundColor"), + "Color": localStorage.getItem("UserScript-User-" + Username + "-Badge-Color"), + "Content": localStorage.getItem("UserScript-User-" + Username + "-Badge-Content") + } + } else { + let BackgroundColor = ""; + let Color = ""; + let Content = ""; + await new Promise((Resolve) => { + RequestAPI("GetBadge", { + "UserID": String(Username) + }, (Response) => { + if (Response.Success) { + BackgroundColor = Response.Data.BackgroundColor; + Color = Response.Data.Color; + Content = Response.Data.Content; + } + Resolve(); + }); + }); + localStorage.setItem("UserScript-User-" + Username + "-Badge-BackgroundColor", BackgroundColor); + localStorage.setItem("UserScript-User-" + Username + "-Badge-Color", Color); + localStorage.setItem("UserScript-User-" + Username + "-Badge-Content", Content); + localStorage.setItem("UserScript-User-" + Username + "-Badge-LastUpdateTime", String(new Date().getTime())); + return { + "BackgroundColor": BackgroundColor, "Color": Color, "Content": Content + } + } + } 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."); + } + } + }; + + /** + * Sets the HTML content of an element to display a username with optional additional information. + * @param {HTMLElement} Element - The element to set the HTML content. + * @param {string} Username - The username to display. + * @param {boolean} [Simple=false] - Indicates whether to display additional information or not. + * @param {string} [Href="https://www.xmoj.tech/userinfo.php?user="] - The URL to link the username to. + * @returns {Promise} - A promise that resolves when the HTML content is set. + */ + let GetUsernameHTML$1 = async (Element, Username, Simple = false, Href = "https://www.xmoj.tech/userinfo.php?user=") => { + try { + //Username = Username.replaceAll(/[^a-zA-Z0-9]/g, ""); + let ID = "Username-" + Username + "-" + Math.random(); + Element.id = ID; + Element.innerHTML = `
`; + Element.appendChild(document.createTextNode(Username)); + let UserInfo = await GetUserInfo$1(Username); + if (UserInfo === null) { + document.getElementById(ID).innerHTML = ""; + document.getElementById(ID).appendChild(document.createTextNode(Username)); + return; + } + let HTMLData = ""; + if (!Simple) { + HTMLData += ``; + } + HTMLData += ` 500) { + HTMLData += "link-danger"; + } else if (Rating >= 400) { + HTMLData += "link-warning"; + } else if (Rating >= 300) { + HTMLData += "link-success"; + } else { + HTMLData += "link-info"; + } + } else { + HTMLData += "link-info"; + } + HTMLData += `\";">`; + if (!Simple) { + if (AdminUserList.includes(Username)) { + HTMLData += `脚本管理员`; + } + let BadgeInfo = await GetUserBadge$1(Username); + if (BadgeInfo.Content != "") { + HTMLData += `${BadgeInfo.Content}`; + } + } + if (document.getElementById(ID) !== null) { + document.getElementById(ID).innerHTML = HTMLData; + document.getElementById(ID).getElementsByTagName("a")[0].appendChild(document.createTextNode(Username)); + } + } 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."); + } + } + }; + + /** + * Bootstrap initialization and main application logic + */ + + + // Time difference for server synchronization (initialized to 0) + let diff = 0; + + // Theme management + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); + + const applyTheme = (theme) => { + document.querySelector("html").setAttribute("data-bs-theme", theme); + localStorage.setItem("UserScript-Setting-DarkMode", theme === "dark" ? "true" : "false"); + }; + + const applySystemTheme = (e) => applyTheme(e.matches ? "dark" : "light"); + + let initTheme = () => { + const saved = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + if (saved === "auto") { + applyTheme(prefersDark.matches ? "dark" : "light"); + prefersDark.addEventListener("change", applySystemTheme); + } else { + applyTheme(saved); + prefersDark.removeEventListener("change", applySystemTheme); + } + }; + + // NavbarStyler class for styling the navigation bar + class NavbarStyler { + constructor() { + try { + this.navbar = document.querySelector('.navbar.navbar-expand-lg.bg-body-tertiary'); + if (this.navbar && UtilityEnabled("NewTopBar")) { + this.init(); + } + } 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."); + } + } + } + + init() { + try { + this.applyStyles(); + this.createOverlay(); + this.createSpacer(); + window.addEventListener('resize', () => this.updateBlurOverlay()); + this.updateBlurOverlay(); + } 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."); + } + } + } + + applyStyles() { + try { + let n = this.navbar; + n.classList.add('fixed-top', 'container', 'ml-auto'); + Object.assign(n.style, { + position: 'fixed', + borderRadius: '28px', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', + margin: '16px auto', + backgroundColor: 'rgba(255, 255, 255, 0)', + opacity: '0.75', + zIndex: '1000' + }); + } 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."); + } + } + } + + createOverlay() { + try { + if (!document.getElementById('blur-overlay')) { + let overlay = document.createElement('div'); + overlay.id = 'blur-overlay'; + document.body.appendChild(overlay); + + let style = document.createElement('style'); + style.textContent = ` + #blur-overlay { + position: fixed; + backdrop-filter: blur(4px); + z-index: 999; + pointer-events: none; + border-radius: 28px; + } + `; + document.head.appendChild(style); + } + } 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."); + } + } + } + + updateBlurOverlay() { + try { + let overlay = document.getElementById('blur-overlay'); + let n = this.navbar; + Object.assign(overlay.style, { + top: `${n.offsetTop}px`, + left: `${n.offsetLeft}px`, + width: `${n.offsetWidth}px`, + height: `${n.offsetHeight}px` + }); + } 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."); + } + } + } + + createSpacer() { + try { + let spacer = document.getElementById('navbar-spacer'); + let newHeight = this.navbar.offsetHeight + 24; + if (!spacer) { + spacer = document.createElement('div'); + spacer.id = 'navbar-spacer'; + spacer.style.height = `${newHeight}px`; + spacer.style.width = '100%'; + document.body.insertBefore(spacer, document.body.firstChild); + } else { + let currentHeight = parseInt(spacer.style.height, 10); + if (currentHeight !== newHeight) { + document.body.removeChild(spacer); + spacer = document.createElement('div'); + spacer.id = 'navbar-spacer'; + spacer.style.height = `${newHeight}px`; + spacer.style.width = '100%'; + document.body.insertBefore(spacer, document.body.firstChild); + } + } + } 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."); + } + } + } + } + + // Utility function to replace markdown images + function replaceMarkdownImages(text, string) { + return text.replace(/!\[.*?\]\(.*?\)/g, string); + } + + // Main initialization function + async function main() { + try { + if (location.href.startsWith('http://')) { + //use https + location.href = location.href.replace('http://', 'https://'); + } + if (location.host != "www.xmoj.tech") { + location.host = "www.xmoj.tech"; + } else { + if (location.href === 'https://www.xmoj.tech/open_contest_sign_up.php') { + return; + } + + // Get current username + let CurrentUsername = ""; + if (document.querySelector("#profile") !== null) { + CurrentUsername = document.querySelector("#profile").innerText; + CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); + } + + // Determine server URL + let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me"); + + document.body.classList.add("placeholder-glow"); + if (document.querySelector("#navbar") != null) { + if (document.querySelector("body > div > div.jumbotron") != null) { + document.querySelector("body > div > div.jumbotron").className = "mt-3"; + } + + if (UtilityEnabled("AutoLogin") && document.querySelector("#profile") != null && document.querySelector("#profile").innerHTML == "登录" && location.pathname != "/login.php" && location.pathname != "/loginpage.php" && location.pathname != "/lostpassword.php") { + localStorage.setItem("UserScript-LastPage", location.pathname + location.search); + location.href = "https://www.xmoj.tech/loginpage.php"; + } + + let Discussion = null; + if (UtilityEnabled("Discussion")) { + Discussion = document.createElement("li"); + document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); + Discussion.innerHTML = "讨论"; + } + if (UtilityEnabled("Translate")) { + document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a").innerText = "题库"; + } + //send analytics + RequestAPI("SendData", {}); + if (UtilityEnabled("ReplaceLinks")) { + document.body.innerHTML = String(document.body.innerHTML).replaceAll(/\[([^<]*)<\/a>\]/g, ""); + } + if (UtilityEnabled("ReplaceXM")) { + document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("海上", "上海"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小红", "徐师娘"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小粉", "彩虹"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("提交上节课的代码", "自动提交当年代码"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("高老师们", "我们"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("自高老师", "自我"); + document.title = String(document.title).replaceAll("小明", "高老师"); + } + + if (UtilityEnabled("NewBootstrap")) { + let Temp = document.querySelectorAll("link"); + for (var i = 0; i < Temp.length; i++) { + if (Temp[i].href.indexOf("bootstrap.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("white.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("semantic.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("bootstrap-theme.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("problem.css") != -1) { + Temp[i].remove(); + } + } + if (UtilityEnabled("DarkMode")) { + document.querySelector("html").setAttribute("data-bs-theme", "dark"); + } else { + document.querySelector("html").setAttribute("data-bs-theme", "light"); + } + var resources = [{ + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/darcula.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/merge/merge.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css', + rel: 'stylesheet' + }, { + type: 'script', + src: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.js', + isModule: true + }]; + let loadResources = async () => { + let promises = resources.map(resource => { + return new Promise((resolve, reject) => { + let element; + if (resource.type === 'script') { + element = document.createElement('script'); + element.src = resource.src; + if (resource.isModule) { + element.type = 'module'; + } + element.onload = resolve; + element.onerror = reject; + } else if (resource.type === 'link') { + element = document.createElement('link'); + element.href = resource.href; + element.rel = resource.rel; + resolve(); // Stylesheets don't have an onload event + } + document.head.appendChild(element); + }); + }); + + await Promise.all(promises); + }; + if (location.pathname == "/submitpage.php") { + await loadResources(); + } else { + loadResources(); + } + document.querySelector("nav").className = "navbar navbar-expand-lg bg-body-tertiary"; + document.querySelector("#navbar > ul:nth-child(1)").classList = "navbar-nav me-auto mb-2 mb-lg-0"; + document.querySelector("body > div > nav > div > div.navbar-header").outerHTML = `${UtilityEnabled("ReplaceXM") ? "高老师" : "小明"}的OJ`; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li").classList = "nav-item dropdown"; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").className = "nav-link dropdown-toggle"; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a > span.caret").remove(); + Temp = document.querySelector("#navbar > ul:nth-child(1)").children; + for (var i = 0; i < Temp.length; i++) { + if (Temp[i].classList.contains("active")) { + Temp[i].classList.remove("active"); + Temp[i].children[0].classList.add("active"); + } + Temp[i].classList.add("nav-item"); + Temp[i].children[0].classList.add("nav-link"); + } + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").setAttribute("data-bs-toggle", "dropdown"); + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").removeAttribute("data-toggle"); + } + if (UtilityEnabled("RemoveUseless") && document.getElementsByTagName("marquee")[0] != undefined) { + document.getElementsByTagName("marquee")[0].remove(); + } + let Style = document.createElement("style"); + document.body.appendChild(Style); + Style.innerHTML = ` + nav { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + blockquote { + border-left: 5px solid var(--bs-secondary-bg); + padding: 0.5em 1em; + } + .status_y:hover { + box-shadow: #52c41a 1px 1px 10px 0px !important; + } + .status_n:hover { + box-shadow: #fe4c61 1px 1px 10px 0px !important; + } + .status_w:hover { + box-shadow: #ffa900 1px 1px 10px 0px !important; + } + .test-case { + border-radius: 5px !important; + } + .test-case:hover { + box-shadow: rgba(0, 0, 0, 0.3) 0px 10px 20px 3px !important; + } + .data[result-item] { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + .software_list { + width: unset !important; + } + .software_item { + margin: 5px 10px !important; + background-color: var(--bs-secondary-bg) !important; + } + .item-txt { + color: var(--bs-emphasis-color) !important; + } + .cnt-row { + justify-content: inherit; + align-items: stretch; + width: 100% !important; + padding: 1rem 0; + } + .cnt-row-head { + padding: 0.8em 1em; + background-color: var(--bs-secondary-bg); + border-radius: 0.3rem 0.3rem 0 0; + width: 100%; + } + .cnt-row-body { + padding: 1em; + border: 1px solid var(--bs-secondary-bg); + border-top: none; + border-radius: 0 0 0.3rem 0.3rem; + }`; + if (UtilityEnabled("AddAnimation")) { + Style.innerHTML += `.status, .test-case { + transition: 0.5s !important; + }`; + } + if (UtilityEnabled("AddColorText")) { + Style.innerHTML += `.red { + color: red !important; + } + .green { + color: green !important; + } + .blue { + color: blue !important; + }`; + } + + if (UtilityEnabled("RemoveUseless")) { + if (document.getElementsByClassName("footer")[0] != null) { + document.getElementsByClassName("footer")[0].remove(); + } + } + + if (UtilityEnabled("ReplaceYN")) { + let Temp = document.getElementsByClassName("status_y");//AC + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "✓"; + } + Temp = document.getElementsByClassName("status_n");//WA + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "✗"; + } + Temp = document.getElementsByClassName("status_w");//Waiting + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "⏳"; + } + } + + let Temp = document.getElementsByClassName("page-item"); + for (let i = 0; i < Temp.length; i++) { + Temp[i].children[0].className = "page-link"; + } + if (document.getElementsByClassName("pagination")[0] != null) { + document.getElementsByClassName("pagination")[0].classList.add("justify-content-center"); + } + + Temp = document.getElementsByTagName("table"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].querySelector("thead") != null) { + TidyTable(Temp[i]); + } + } + + setInterval(() => { + try { + let CurrentDate = new Date(new Date().getTime() + diff); + let Year = CurrentDate.getFullYear(); + if (Year > 3000) { + Year -= 1900; + } + let Month = CurrentDate.getMonth() + 1; + let _Date = CurrentDate.getDate(); + let Hours = CurrentDate.getHours(); + let Minutes = CurrentDate.getMinutes(); + let Seconds = CurrentDate.getSeconds(); + document.getElementById("nowdate").innerHTML = Year + "-" + (Month < 10 ? "0" : "") + Month + "-" + (_Date < 10 ? "0" : "") + _Date + " " + (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; + } catch (Error) { + } + if (UtilityEnabled("NewTopBar")) { + new NavbarStyler(); + } + if (UtilityEnabled("ResetType")) { + if (document.querySelector("#profile") != undefined && document.querySelector("#profile").innerHTML == "登录") { + let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); + PopupUL.innerHTML = ``; + PopupUL.children[0].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/loginpage.php"; + }); + let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); + document.addEventListener("click", (event) => { + if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { + hideDropdownItems(); + } + }); + } else if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul") != undefined && document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul > li:nth-child(2)").innerText != "个人中心") { + let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); + PopupUL.style.cursor = 'pointer'; + PopupUL.innerHTML = ` + + + + + `; + PopupUL.children[0].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/modifypage.php"; + }); + PopupUL.children[1].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername; + }); + PopupUL.children[2].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/mail.php"; + }); + PopupUL.children[3].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/index.php?ByUserScript=1"; + }); + PopupUL.children[4].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/modifypage.php?ByUserScript=1"; + }); + PopupUL.children[5].addEventListener("click", () => { + clearCredential(); + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + }) + .catch((error) => { + console.error(error); + }); //We can no longer rely of the server to set the cookie for us + location.href = "https://www.xmoj.tech/logout.php"; + }); + Array.from(PopupUL.children).forEach(item => { + item.style.opacity = 0; + item.style.transform = 'translateY(-16px)'; + item.style.transition = 'transform 0.3s ease, opacity 0.5s ease'; + }); + let showDropdownItems = () => { + PopupUL.style.display = 'block'; + Array.from(PopupUL.children).forEach((item, index) => { + clearTimeout(item._timeout); + item.style.opacity = 0; + item.style.transform = 'translateY(-4px)'; + item._timeout = setTimeout(() => { + item.style.opacity = 1; + item.style.transform = 'translateY(2px)'; + }, index * 36); + }); + }; + let hideDropdownItems = () => { + Array.from(PopupUL.children).forEach((item) => { + clearTimeout(item._timeout); + item.style.opacity = 0; + item.style.transform = 'translateY(-16px)'; + }); + setTimeout(() => { + PopupUL.style.display = 'none'; + }, 100); + }; + let toggleDropdownItems = () => { + if (PopupUL.style.display === 'block') { + hideDropdownItems(); + } else { + showDropdownItems(); + } + }; + let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); + parentLi.addEventListener("click", toggleDropdownItems); + document.addEventListener("click", (event) => { + if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { + hideDropdownItems(); + } + }); + } + } + if (UtilityEnabled("AutoCountdown")) { + let Temp = document.getElementsByClassName("UpdateByJS"); + for (let i = 0; i < Temp.length; i++) { + let EndTime = Temp[i].getAttribute("EndTime"); + if (EndTime === null) { + Temp[i].classList.remove("UpdateByJS"); + continue; + } + let TimeStamp = parseInt(EndTime) - new Date().getTime(); + if (TimeStamp < 3000) { + Temp[i].classList.remove("UpdateByJS"); + location.reload(); + } + let CurrentDate = new Date(TimeStamp); + let Day = parseInt((TimeStamp / 1000 / 60 / 60 / 24).toFixed(0)); + let Hour = CurrentDate.getUTCHours(); + let Minute = CurrentDate.getUTCMinutes(); + let Second = CurrentDate.getUTCSeconds(); + Temp[i].innerHTML = (Day !== 0 ? Day + "天" : "") + (Hour !== 0 ? (Hour < 10 ? "0" : "") + Hour + "小时" : "") + (Minute !== 0 ? (Minute < 10 ? "0" : "") + Minute + "分" : "") + (Second !== 0 ? (Second < 10 ? "0" : "") + Second + "秒" : ""); + } + } + }, 100); + + // Check for updates + fetch(ServerURL + "/Update.json", {cache: "no-cache"}) + .then((Response) => { + return Response.json(); + }) + .then((Response) => { + let CurrentVersion = GM_info.script.version; + let LatestVersion; + for (let i = Object.keys(Response.UpdateHistory).length - 1; i >= 0; i--) { + let VersionInfo = Object.keys(Response.UpdateHistory)[i]; + if (UtilityEnabled("DebugMode") || Response.UpdateHistory[VersionInfo].Prerelease == false) { + LatestVersion = VersionInfo; + break; + } + } + if (compareVersions(CurrentVersion, LatestVersion)) { + let UpdateDiv = document.createElement("div"); + UpdateDiv.innerHTML = ` + `; + if (UtilityEnabled("NewTopBar")) { + UpdateDiv.style.position = 'fixed'; + UpdateDiv.style.top = '72px'; + UpdateDiv.style.left = '50%'; + UpdateDiv.style.transform = 'translateX(-50%)'; + UpdateDiv.style.zIndex = '1001'; + let spacer = document.createElement("div"); + spacer.style.height = '48px'; + document.body.insertBefore(spacer, document.body.firstChild); + UpdateDiv.querySelector(".btn-close").addEventListener("click", function () { + document.body.removeChild(spacer); + }); + } + document.body.appendChild(UpdateDiv); + document.querySelector("body > div").insertBefore(UpdateDiv, document.querySelector("body > div > div.mt-3")); + } + if (localStorage.getItem("UserScript-Update-LastVersion") != GM_info.script.version) { + localStorage.setItem("UserScript-Update-LastVersion", GM_info.script.version); + let UpdateDiv = document.createElement("div"); + document.querySelector("body").appendChild(UpdateDiv); + UpdateDiv.className = "modal fade"; + UpdateDiv.id = "UpdateModal"; + UpdateDiv.tabIndex = -1; + let UpdateDialog = document.createElement("div"); + UpdateDiv.appendChild(UpdateDialog); + UpdateDialog.className = "modal-dialog"; + let UpdateContent = document.createElement("div"); + UpdateDialog.appendChild(UpdateContent); + UpdateContent.className = "modal-content"; + let UpdateHeader = document.createElement("div"); + UpdateContent.appendChild(UpdateHeader); + UpdateHeader.className = "modal-header"; + let UpdateTitle = document.createElement("h5"); + UpdateHeader.appendChild(UpdateTitle); + UpdateTitle.className = "modal-title"; + UpdateTitle.innerText = "更新日志"; + let UpdateCloseButton = document.createElement("button"); + UpdateHeader.appendChild(UpdateCloseButton); + UpdateCloseButton.type = "button"; + UpdateCloseButton.className = "btn-close"; + UpdateCloseButton.setAttribute("data-bs-dismiss", "modal"); + let UpdateBody = document.createElement("div"); + UpdateContent.appendChild(UpdateBody); + UpdateBody.className = "modal-body"; + let UpdateFooter = document.createElement("div"); + UpdateContent.appendChild(UpdateFooter); + UpdateFooter.className = "modal-footer"; + let UpdateButton = document.createElement("button"); + UpdateFooter.appendChild(UpdateButton); + UpdateButton.type = "button"; + UpdateButton.className = "btn btn-secondary"; + UpdateButton.setAttribute("data-bs-dismiss", "modal"); + UpdateButton.innerText = "关闭"; + let Version = Object.keys(Response.UpdateHistory)[Object.keys(Response.UpdateHistory).length - 1]; + let Data = Response.UpdateHistory[Version]; + let UpdateDataCard = document.createElement("div"); + UpdateBody.appendChild(UpdateDataCard); + UpdateDataCard.className = "card mb-3"; + let UpdateDataCardBody = document.createElement("div"); + UpdateDataCard.appendChild(UpdateDataCardBody); + UpdateDataCardBody.className = "card-body"; + let UpdateDataCardTitle = document.createElement("h5"); + UpdateDataCardBody.appendChild(UpdateDataCardTitle); + UpdateDataCardTitle.className = "card-title"; + UpdateDataCardTitle.innerText = Version; + let UpdateDataCardSubtitle = document.createElement("h6"); + UpdateDataCardBody.appendChild(UpdateDataCardSubtitle); + UpdateDataCardSubtitle.className = "card-subtitle mb-2 text-muted"; + UpdateDataCardSubtitle.innerHTML = GetRelativeTime(Data.UpdateDate); + let UpdateDataCardText = document.createElement("p"); + UpdateDataCardBody.appendChild(UpdateDataCardText); + UpdateDataCardText.className = "card-text"; + //release notes + if (Data.Notes != undefined) { + UpdateDataCardText.innerHTML = Data.Notes; + } + let UpdateDataCardList = document.createElement("ul"); + UpdateDataCardText.appendChild(UpdateDataCardList); + UpdateDataCardList.className = "list-group list-group-flush"; + for (let j = 0; j < Data.UpdateContents.length; j++) { + let UpdateDataCardListItem = document.createElement("li"); + UpdateDataCardList.appendChild(UpdateDataCardListItem); + UpdateDataCardListItem.className = "list-group-item"; + UpdateDataCardListItem.innerHTML = "(" + "#" + Data.UpdateContents[j].PR + ") " + Data.UpdateContents[j].Description; + } + let UpdateDataCardLink = document.createElement("a"); + UpdateDataCardBody.appendChild(UpdateDataCardLink); + UpdateDataCardLink.className = "card-link"; + UpdateDataCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script/releases/tag/" + Version; + UpdateDataCardLink.target = "_blank"; + UpdateDataCardLink.innerText = "查看该版本"; + new bootstrap.Modal(document.getElementById("UpdateModal")).show(); + } + }); + + // Request AddOnScript + RequestAPI("GetAddOnScript", {}, (Response) => { + if (Response.Success) { + eval(Response.Data["Script"]); + } else { + console.warn("Fetch AddOnScript failed: " + Response.Message); + } + }); + + // Toast notifications setup + let ToastContainer = document.createElement("div"); + ToastContainer.classList.add("toast-container", "position-fixed", "bottom-0", "end-0", "p-3"); + document.body.appendChild(ToastContainer); + + addEventListener("focus", () => { + if (UtilityEnabled("BBSPopup")) { + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + ToastContainer.innerHTML = ""; + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有人@你"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "讨论" + MentionList[i].PostTitle + "有新回复"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadBBSMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/discuss3/thread.php?tid=" + MentionList[i].PostID + '&page=' + MentionList[i].PageNumber, "_blank"); + RequestAPI("ReadBBSMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); + } + } + }); + } + if (UtilityEnabled("MessagePopup")) { + RequestAPI("GetMailMentionList", {}, async (Response) => { + if (Response.Success) { + ToastContainer.innerHTML = ""; + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有新消息"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "来自用户" + MentionList[i].FromUserID + "的消息"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadMailMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/mail.php?to_user=" + MentionList[i].FromUserID, "_blank"); + RequestAPI("ReadMailMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); + } + } + }); + } + }); + + dispatchEvent(new Event("focus")); + + if (location.pathname == "/index.php" || location.pathname == "/") { + if (new URL(location.href).searchParams.get("ByUserScript") != null) { + document.title = "脚本设置"; + localStorage.setItem("UserScript-Opened", "true"); + let Container = document.getElementsByClassName("mt-3")[0]; + Container.innerHTML = ""; + let Alert = document.createElement("div"); + Alert.classList.add("alert"); + Alert.classList.add("alert-primary"); + Alert.role = "alert"; + Alert.innerHTML = `欢迎您使用XMOJ增强脚本!点击 + 此处 + 查看更新日志。`; + Container.appendChild(Alert); + let UtilitiesCard = document.createElement("div"); + UtilitiesCard.classList.add("card"); + UtilitiesCard.classList.add("mb-3"); + let UtilitiesCardHeader = document.createElement("div"); + UtilitiesCardHeader.classList.add("card-header"); + UtilitiesCardHeader.innerText = "XMOJ增强脚本功能列表"; + UtilitiesCard.appendChild(UtilitiesCardHeader); + let UtilitiesCardBody = document.createElement("div"); + UtilitiesCardBody.classList.add("card-body"); + let CreateList = (Data) => { + let List = document.createElement("ul"); + List.classList.add("list-group"); + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("li"); + Row.classList.add("list-group-item"); + if (Data[i].Type == "A") { + Row.classList.add("list-group-item-success"); + } else if (Data[i].Type == "F") { + Row.classList.add("list-group-item-warning"); + } else if (Data[i].Type == "D") { + Row.classList.add("list-group-item-danger"); + } + if (Data[i].ID == "Theme") { + let Label = document.createElement("label"); + Label.classList.add("me-2"); + Label.htmlFor = "UserScript-Setting-Theme"; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + let Select = document.createElement("select"); + Select.classList.add("form-select", "form-select-sm", "w-auto", "d-inline"); + Select.id = "UserScript-Setting-Theme"; + [ + ["light", "亮色"], + ["dark", "暗色"], + ["auto", "跟随系统"] + ].forEach(opt => { + let option = document.createElement("option"); + option.value = opt[0]; + option.innerText = opt[1]; + Select.appendChild(option); + }); + Select.value = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + Select.addEventListener("change", () => { + localStorage.setItem("UserScript-Setting-Theme", Select.value); + initTheme(); + }); + Row.appendChild(Select); + } else if (Data[i].Children == undefined) { + let CheckBox = document.createElement("input"); + CheckBox.classList.add("form-check-input"); + CheckBox.classList.add("me-1"); + CheckBox.type = "checkbox"; + CheckBox.id = Data[i].ID; + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == null) { + localStorage.setItem("UserScript-Setting-" + Data[i].ID, "true"); + } + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == "false") { + CheckBox.checked = false; + } else { + CheckBox.checked = true; + } + CheckBox.addEventListener("change", () => { + return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); + }); + + Row.appendChild(CheckBox); + let Label = document.createElement("label"); + Label.classList.add("form-check-label"); + Label.htmlFor = Data[i].ID; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } else { + let Label = document.createElement("label"); + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } + if (Data[i].Children != undefined) { + Row.appendChild(CreateList(Data[i].Children)); + } + List.appendChild(Row); + } + return List; + }; + UtilitiesCardBody.appendChild(CreateList([{ + "ID": "Discussion", + "Type": "F", + "Name": "恢复讨论与短消息功能" + }, { + "ID": "MoreSTD", "Type": "F", "Name": "查看到更多标程" + }, {"ID": "ApplyData", "Type": "A", "Name": "获取数据功能"}, { + "ID": "AutoCheat", "Type": "A", "Name": "自动提交当年代码" + }, {"ID": "Rating", "Type": "A", "Name": "添加用户评分和用户名颜色"}, { + "ID": "AutoRefresh", "Type": "A", "Name": "比赛列表、比赛排名界面自动刷新" + }, { + "ID": "AutoCountdown", "Type": "A", "Name": "比赛列表等界面的时间自动倒计时" + }, {"ID": "DownloadPlayback", "Type": "A", "Name": "回放视频增加下载功能"}, { + "ID": "ImproveACRate", "Type": "A", "Name": "自动提交已AC题目以提高AC率" + }, {"ID": "AutoO2", "Type": "F", "Name": "代码提交界面自动选择O2优化"}, { + "ID": "Beautify", "Type": "F", "Name": "美化界面", "Children": [{ + "ID": "NewTopBar", "Type": "F", "Name": "使用新的顶部导航栏" + }, { + "ID": "NewBootstrap", "Type": "F", "Name": "使用新版的Bootstrap样式库*" + }, {"ID": "ResetType", "Type": "F", "Name": "重新排版*"}, { + "ID": "AddColorText", "Type": "A", "Name": "增加彩色文字" + }, {"ID": "AddUnits", "Type": "A", "Name": "状态界面内存与耗时添加单位"}, { + "ID": "Theme", "Type": "A", "Name": "界面主题" + }, {"ID": "AddAnimation", "Type": "A", "Name": "增加动画"}, { + "ID": "ReplaceYN", "Type": "F", "Name": "题目前状态提示替换为好看的图标" + }, {"ID": "RemoveAlerts", "Type": "D", "Name": "去除多余反复的提示"}, { + "ID": "Translate", "Type": "F", "Name": "统一使用中文,翻译了部分英文*" + }, { + "ID": "ReplaceLinks", "Type": "F", "Name": "将网站中所有以方括号包装的链接替换为按钮" + }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { + "ID": "ReplaceXM", + "Type": "F", + "Name": "将网站中所有“小明”和“我”关键字替换为“高老师”,所有“小红”替换为“徐师娘”,所有“小粉”替换为“彩虹”,所有“下海”、“海上”替换为“上海” (此功能默认关闭)" + }] + }, { + "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" + }, { + "ID": "SavePassword", "Type": "A", "Name": "自动保存用户名与密码,免去每次手动输入密码的繁琐" + }, { + "ID": "CopySamples", "Type": "F", "Name": "题目界面测试样例有时复制无效" + }, { + "ID": "RefreshSolution", "Type": "F", "Name": "状态页面结果自动刷新每次只能刷新一个" + }, {"ID": "CopyMD", "Type": "A", "Name": "复制题目或题解内容"}, { + "ID": "ProblemSwitcher", "Type": "A", "Name": "比赛题目切换器" + }, { + "ID": "OpenAllProblem", "Type": "A", "Name": "比赛题目界面一键打开所有题目" + }, { + "ID": "CheckCode", "Type": "A", "Name": "提交代码前对代码进行检查", "Children": [{ + "ID": "IOFile", "Type": "A", "Name": "是否使用了文件输入输出(如果需要使用)" + }, {"ID": "CompileError", "Type": "A", "Name": "是否有编译错误"}] + }, { + "ID": "ExportACCode", "Type": "F", "Name": "导出AC代码每一道题目一个文件" + }, {"ID": "LoginFailed", "Type": "F", "Name": "修复登录后跳转失败*"}, { + "ID": "NewDownload", "Type": "A", "Name": "下载页面增加下载内容" + }, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, { + "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" + }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { + "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" + }, { + "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" + }])); + let UtilitiesCardFooter = document.createElement("div"); + UtilitiesCardFooter.className = "card-footer text-muted"; + UtilitiesCardFooter.innerText = "* 不建议关闭,可能会导致系统不稳定、界面错乱、功能缺失等问题\n绿色:增加功能 黄色:修改功能 红色:删除功能"; + UtilitiesCardBody.appendChild(UtilitiesCardFooter); + UtilitiesCard.appendChild(UtilitiesCardBody); + Container.appendChild(UtilitiesCard); + let FeedbackCard = document.createElement("div"); + FeedbackCard.className = "card mb-3"; + let FeedbackCardHeader = document.createElement("div"); + FeedbackCardHeader.className = "card-header"; + FeedbackCardHeader.innerText = "反馈、源代码、联系作者"; + FeedbackCard.appendChild(FeedbackCardHeader); + let FeedbackCardBody = document.createElement("div"); + FeedbackCardBody.className = "card-body"; + let FeedbackCardText = document.createElement("p"); + FeedbackCardText.className = "card-text"; + FeedbackCardText.innerText = "如果您有任何建议或者发现了 bug,请前往本项目的 GitHub 页面并提交 issue。提交 issue 前请先搜索是否有相同的 issue,如果有请在该 issue 下留言。请在 issue 中尽可能详细地描述您的问题,并且附上您的浏览器版本、操作系统版本、脚本版本、复现步骤等信息。谢谢您支持本项目。"; + FeedbackCardBody.appendChild(FeedbackCardText); + let FeedbackCardLink = document.createElement("a"); + FeedbackCardLink.className = "card-link"; + FeedbackCardLink.innerText = "GitHub"; + FeedbackCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script"; + FeedbackCardBody.appendChild(FeedbackCardLink); + FeedbackCard.appendChild(FeedbackCardBody); + Container.appendChild(FeedbackCard); + } else { + let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; + let NewsData = []; + for (let i = 0; i < Temp.length; i += 2) { + let Title = Temp[i].children[0].innerText; + let Time = 0; + if (Temp[i].children[1] != null) { + Time = Temp[i].children[1].innerText; + } + let Body = Temp[i + 1].innerHTML; + NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; + for (let i = 0; i < NewsData.length; i++) { + let NewsRow = document.createElement("div"); + NewsRow.className = "cnt-row"; + let NewsRowHead = document.createElement("div"); + NewsRowHead.className = "cnt-row-head title"; + NewsRowHead.innerText = NewsData[i].Title; + if (NewsData[i].Time != 0) { + NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; + } + NewsRow.appendChild(NewsRowHead); + let NewsRowBody = document.createElement("div"); + NewsRowBody.className = "cnt-row-body"; + NewsRowBody.innerHTML = NewsData[i].Body; + NewsRow.appendChild(NewsRowBody); + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); + } + let CountDownData = document.querySelector("#countdown_list").innerHTML; + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
+
倒计时
+
${CountDownData}
+
`; + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML += `
+
公告
+
加载中...
+
`; + RequestAPI("GetNotice", {}, (Response) => { + if (Response.Success) { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = marked.parse(Response.Data["Notice"]).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + RenderMathJax(); + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + } else { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = "加载失败: " + Response.Message; + } + }); + } + } else if (location.pathname == "/problemset.php") { + if (UtilityEnabled("Translate")) { + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input").placeholder = "题目编号"; + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button").innerText = "确认"; + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input").placeholder = "标题或内容"; + document.querySelector("#problemset > thead > tr > th:nth-child(1)").innerText = "状态"; + } + if (UtilityEnabled("ResetType")) { + document.querySelector("#problemset > thead > tr > th:nth-child(1)").style.width = "5%"; + document.querySelector("#problemset > thead > tr > th:nth-child(2)").style.width = "10%"; + document.querySelector("#problemset > thead > tr > th:nth-child(3)").style.width = "75%"; + document.querySelector("#problemset > thead > tr > th:nth-child(4)").style.width = "5%"; + document.querySelector("#problemset > thead > tr > th:nth-child(5)").style.width = "5%"; + } + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)").outerHTML = ` +
+
+
+
+ + +
+
+
+
+ + +
+
+
`; + if (SearchParams.get("search") != null) { + document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input").value = SearchParams.get("search"); + } + + let Temp = document.querySelector("#problemset").rows; + for (let i = 1; i < Temp.length; i++) { + localStorage.setItem("UserScript-Problem-" + Temp[i].children[1].innerText + "-Name", Temp[i].children[2].innerText); + } + } else if (location.pathname == "/problem.php") { + await RenderMathJax(); + if (SearchParams.get("cid") != null && UtilityEnabled("ProblemSwitcher")) { + document.getElementsByTagName("h2")[0].innerHTML += " (" + localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID") + ")"; + 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); + } + } + + let problemSwitcher = document.createElement("div"); + problemSwitcher.style.position = "fixed"; + problemSwitcher.style.top = "50%"; + problemSwitcher.style.left = "0"; + problemSwitcher.style.transform = "translateY(-50%)"; + problemSwitcher.style.maxHeight = "80vh"; + problemSwitcher.style.overflowY = "auto"; + if (document.querySelector("html").getAttribute("data-bs-theme") == "dark") { + problemSwitcher.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; + } else { + problemSwitcher.style.backgroundColor = "rgba(255, 255, 255, 0.8)"; + } + problemSwitcher.style.padding = "10px"; + problemSwitcher.style.borderRadius = "0 10px 10px 0"; + problemSwitcher.style.display = "flex"; + problemSwitcher.style.flexDirection = "column"; + + let problemList = JSON.parse(ContestProblemList); + for (let i = 0; i < problemList.length; i++) { + let buttonText = ""; + if (i < 26) { + buttonText = String.fromCharCode(65 + i); + } else { + buttonText = String.fromCharCode(97 + (i - 26)); + } + let activeClass = ""; + if (problemList[i].url === location.href) { + activeClass = "active"; + } + problemSwitcher.innerHTML += `${buttonText}`; + } + document.body.appendChild(problemSwitcher); + } + if (document.querySelector("body > div > div.mt-3 > h2") != null) { + document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; + setTimeout(() => { + location.href = "https://www.xmoj.tech/problemset.php"; + }, 1000); + } else { + let PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { + document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; + } + //修复提交按钮 + let SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(12)'); + if (SubmitLink == null) { //a special type of problem + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(10)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(11)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(13)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(9)'); + } + if (SubmitLink == null) { //为什么这个破东西老是换位置 + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(7)'); + } + if (SubmitLink == null) { //tmd又换位置 + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(8)'); + } + let SubmitButton = document.createElement('button'); + SubmitButton.id = 'SubmitButton'; + SubmitButton.className = 'btn btn-outline-secondary'; + SubmitButton.textContent = '提交'; + SubmitButton.href = SubmitLink.href; + SubmitButton.onclick = function () { + window.location.href = SubmitLink.href; + console.log(SubmitLink.href); + }; + + // Replace the element with the button + SubmitLink.parentNode.replaceChild(SubmitButton, SubmitLink); + // Remove the button's outer [] + let str = document.querySelector('.mt-3 > center:nth-child(1)').innerHTML; + let target = SubmitButton.outerHTML; + let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); + document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = result; + document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () { + window.location.href = SubmitLink.href; + console.log(SubmitLink.href); + }; + let Temp = document.querySelectorAll(".sampledata"); + for (var i = 0; i < Temp.length; i++) { + Temp[i].parentElement.className = "card"; + } + if (UtilityEnabled("RemoveUseless")) { + document.querySelector("h2.lang_en").remove(); + document.getElementsByTagName("center")[1].remove(); + } + if (UtilityEnabled("CopySamples")) { + $(".copy-btn").click((Event) => { + let CurrentButton = $(Event.currentTarget); + let span = CurrentButton.parent().last().find(".sampledata"); + if (!span.length) { + CurrentButton.text("未找到代码块").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + return; + } + GM_setClipboard(span.text()); + CurrentButton.text("复制成功").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + //document.body.removeChild(textarea[0]); + }); + } + let IOFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (IOFileElement != null) { + while (IOFileElement.childNodes.length >= 1) { + IOFileElement.parentNode.insertBefore(IOFileElement.childNodes[0], IOFileElement); + } + IOFileElement.parentNode.insertBefore(document.createElement("br"), IOFileElement); + IOFileElement.remove(); + let Temp = document.querySelector("body > div > div.mt-3 > center").childNodes[2].data.trim(); + let IOFilename = Temp.substring(0, Temp.length - 3); + localStorage.setItem("UserScript-Problem-" + PID + "-IOFilename", IOFilename); + } + + if (UtilityEnabled("CopyMD")) { + await fetch(location.href).then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelectorAll(".cnt-row-body"); + if (UtilityEnabled("DebugMode")) console.log(Temp); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].children[0].className === "content lang_cn") { + let CopyMDButton = document.createElement("button"); + CopyMDButton.className = "btn btn-sm btn-outline-secondary copy-btn"; + CopyMDButton.innerText = "复制"; + CopyMDButton.style.marginLeft = "10px"; + CopyMDButton.type = "button"; + document.querySelectorAll(".cnt-row-head.title")[i].appendChild(CopyMDButton); + CopyMDButton.addEventListener("click", () => { + GM_setClipboard(Temp[i].children[0].innerText.trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); + CopyMDButton.innerText = "复制成功"; + setTimeout(() => { + CopyMDButton.innerText = "复制"; + }, 1000); + }); + } + } + }); + } + + if (UtilityEnabled("Discussion")) { + let DiscussButton = document.createElement("button"); + DiscussButton.className = "btn btn-outline-secondary position-relative"; + DiscussButton.innerHTML = `讨论`; + DiscussButton.style.marginLeft = "10px"; + DiscussButton.type = "button"; + DiscussButton.addEventListener("click", () => { + if (SearchParams.get("cid") != null) { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); + } else { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); + } + }); + document.querySelector("body > div > div.mt-3 > center").appendChild(DiscussButton); + let UnreadBadge = document.createElement("span"); + UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + UnreadBadge.style.display = "none"; + DiscussButton.appendChild(UnreadBadge); + + let RefreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (Response) => { + if (Response.Success) { + if (Response.Data.DiscussCount != 0) { + UnreadBadge.innerText = Response.Data.DiscussCount; + UnreadBadge.style.display = ""; + } + } + }); + }; + RefreshCount(); + addEventListener("focus", RefreshCount); + } + + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + } + Style.innerHTML += "code, kbd, pre, samp {"; + Style.innerHTML += " font-family: monospace, Consolas, 'Courier New';"; + Style.innerHTML += " font-size: 1rem;"; + Style.innerHTML += "}"; + Style.innerHTML += "pre {"; + Style.innerHTML += " padding: 0.3em 0.5em;"; + Style.innerHTML += " margin: 0.5em 0;"; + Style.innerHTML += "}"; + Style.innerHTML += ".in-out {"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += " display: flex;"; + Style.innerHTML += " padding: 0.5em 0;"; + Style.innerHTML += "}"; + Style.innerHTML += ".in-out .in-out-item {"; + Style.innerHTML += " flex: 1;"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += "}"; + Style.innerHTML += ".cnt-row .title {"; + Style.innerHTML += " font-weight: bolder;"; + Style.innerHTML += " font-size: 1.1rem;"; + Style.innerHTML += "}"; + Style.innerHTML += ".cnt-row .content {"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += "}"; + Style.innerHTML += "a.copy-btn {"; + Style.innerHTML += " float: right;"; + Style.innerHTML += " padding: 0 0.4em;"; + Style.innerHTML += " border: 1px solid var(--bs-primary);"; + Style.innerHTML += " border-radius: 3px;"; + Style.innerHTML += " color: var(--bs-primary);"; + Style.innerHTML += " cursor: pointer;"; + Style.innerHTML += "}"; + Style.innerHTML += "a.copy-btn:hover {"; + Style.innerHTML += " background-color: var(--bs-secondary-bg);"; + Style.innerHTML += "}"; + Style.innerHTML += "a.done, a.done:hover {"; + Style.innerHTML += " background-color: var(--bs-primary);"; + Style.innerHTML += " color: white;"; + Style.innerHTML += "}"; + } else if (location.pathname == "/status.php") { + if (SearchParams.get("ByUserScript") == null) { + document.title = "提交状态"; + document.querySelector("body > script:nth-child(5)").remove(); + if (UtilityEnabled("NewBootstrap")) { + document.querySelector("#simform").outerHTML = `
+ +
+ + +
+
+ + +
+ + +
+
+ +
`; + } + + if (UtilityEnabled("ImproveACRate")) { + let ImproveACRateButton = document.createElement("button"); + document.querySelector("body > div.container > div > div.input-append").appendChild(ImproveACRateButton); + ImproveACRateButton.className = "btn btn-outline-secondary"; + ImproveACRateButton.innerText = "提高正确率"; + ImproveACRateButton.disabled = true; + let ACProblems = []; + await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + ImproveACRateButton.innerText += "(" + (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText) * 100).toFixed(2) + "%)"; + let Temp = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < Temp.length; i++) { + ACProblems.push(Number(Temp[i].substring(2, Temp[i].indexOf(",")))); + } + ImproveACRateButton.disabled = false; + }); + ImproveACRateButton.addEventListener("click", async () => { + ImproveACRateButton.disabled = true; + let SubmitTimes = 3; + let Count = 0; + let SubmitInterval = setInterval(async () => { + if (Count >= SubmitTimes) { + clearInterval(SubmitInterval); + location.reload(); + return; + } + ImproveACRateButton.innerText = "正在提交 (" + (Count + 1) + "/" + SubmitTimes + ")"; + let PID = ACProblems[Math.floor(Math.random() * ACProblems.length)]; + let SID = 0; + await fetch("https://www.xmoj.tech/status.php?problem_id=" + PID + "&jresult=4") + .then((Result) => { + return Result.text(); + }).then((Result) => { + let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); + SID = ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + let Code = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SID) + .then((Response) => { + return Response.text(); + }).then((Response) => { + Code = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, + "method": "POST", + "body": "id=" + PID + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" + }); + Count++; + }, 1000); + }); + ImproveACRateButton.style.marginBottom = ImproveACRateButton.style.marginRight = "7px"; + ImproveACRateButton.style.marginRight = "7px"; + } + if (UtilityEnabled("CompareSource")) { + let CompareButton = document.createElement("button"); + document.querySelector("body > div.container > div > div.input-append").appendChild(CompareButton); + CompareButton.className = "btn btn-outline-secondary"; + CompareButton.innerText = "比较提交记录"; + CompareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php"; + }); + CompareButton.style.marginBottom = "7px"; + } + if (UtilityEnabled("ResetType")) { + document.querySelector("#result-tab > thead > tr > th:nth-child(1)").remove(); + document.querySelector("#result-tab > thead > tr > th:nth-child(2)").remove(); + document.querySelector("#result-tab > thead > tr > th:nth-child(10)").innerHTML = "开启O2"; + } + let Temp = document.querySelector("#result-tab > tbody").childNodes; + let SolutionIDs = []; + for (let i = 1; i < Temp.length; i += 2) { + let SID = Number(Temp[i].childNodes[1].innerText); + SolutionIDs.push(SID); + if (UtilityEnabled("ResetType")) { + Temp[i].childNodes[0].remove(); + Temp[i].childNodes[0].innerHTML = "
" + SID + " " + "重交"; + Temp[i].childNodes[1].remove(); + Temp[i].childNodes[1].children[0].removeAttribute("class"); + Temp[i].childNodes[3].childNodes[0].innerText = SizeToStringSize(Temp[i].childNodes[3].childNodes[0].innerText); + Temp[i].childNodes[4].childNodes[0].innerText = TimeToStringTime(Temp[i].childNodes[4].childNodes[0].innerText); + Temp[i].childNodes[5].innerText = Temp[i].childNodes[5].childNodes[0].innerText; + Temp[i].childNodes[6].innerText = CodeSizeToStringSize(Temp[i].childNodes[6].innerText.substring(0, Temp[i].childNodes[6].innerText.length - 1)); + Temp[i].childNodes[9].innerText = (Temp[i].childNodes[9].innerText == "" ? "否" : "是"); + } + if (SearchParams.get("cid") === null) { + localStorage.setItem("UserScript-Solution-" + SID + "-Problem", Temp[i].childNodes[1].innerText); + } else { + localStorage.setItem("UserScript-Solution-" + SID + "-Contest", SearchParams.get("cid")); + localStorage.setItem("UserScript-Solution-" + SID + "-PID-Contest", Temp[i].childNodes[1].innerText.charAt(0)); + } + } + + if (UtilityEnabled("RefreshSolution")) { + let StdList; + await new Promise((Resolve) => { + RequestAPI("GetStdList", {}, async (Result) => { + if (Result.Success) { + StdList = Result.Data.StdList; + Resolve(); + } + }); + }); + + let Rows = document.getElementById("result-tab").rows; + let Points = Array(); + for (let i = 1; i <= SolutionIDs.length; i++) { + Rows[i].cells[2].className = "td_result"; + let SolutionID = SolutionIDs[i - 1]; + if (Rows[i].cells[2].children.length == 2) { + Points[SolutionID] = Rows[i].cells[2].children[1].innerText; + Rows[i].cells[2].children[1].remove(); + } + Rows[i].cells[2].innerHTML += ""; + setTimeout(() => { + RefreshResult(SolutionID); + }, 0); + } + + let RefreshResult = async (SolutionID) => { + let CurrentRow = null; + let Rows = document.getElementById("result-tab").rows; + for (let i = 0; i < SolutionIDs.length; i++) { + if (SolutionIDs[i] == SolutionID) { + CurrentRow = Rows[i + 1]; + break; + } + } + await fetch("status-ajax.php?solution_id=" + SolutionID) + .then((Response) => { + return Response.text(); + }) + .then((Response) => { + let PID = 0; + if (SearchParams.get("cid") === null) { + PID = localStorage.getItem("UserScript-Solution-" + SolutionID + "-Problem"); + } else { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + (CurrentRow.cells[1].innerText.charCodeAt(0) - 65) + "-PID"); + } + let ResponseData = Response.split(","); + CurrentRow.cells[3].innerHTML = "
" + SizeToStringSize(ResponseData[1]) + "
"; + CurrentRow.cells[4].innerHTML = "
" + TimeToStringTime(ResponseData[2]) + "
"; + let TempHTML = ""; + TempHTML += judge_result[ResponseData[0]]; + TempHTML += ""; + if (Points[SolutionID] != undefined) { + TempHTML += "" + Points[SolutionID] + ""; + if (Points[SolutionID].substring(0, Points[SolutionID].length - 1) >= 50) { + TempHTML += `查看标程`; + } + } + if (ResponseData[0] < 4) { + setTimeout(() => { + RefreshResult(SolutionID); + }, 500); + TempHTML += ""; + } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { + if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; + let Std = StdList.find((Element) => { + return Element == Number(PID); + }); + if (Std != undefined) { + TempHTML += "✅"; + } else { + RequestAPI("UploadStd", { + "ProblemID": Number(PID), + }, (Result) => { + if (Result.Success) { + CurrentRow.cells[2].innerHTML += "🆗"; + } else { + CurrentRow.cells[2].innerHTML += "⚠️"; + } + }); + } + } + CurrentRow.cells[2].innerHTML = TempHTML; + }); + }; + } + } + } else if (location.pathname == "/contest.php") { + if (UtilityEnabled("AutoCountdown")) { + clock = () => { + }; + } + if (location.href.indexOf("?cid=") == -1) { + if (UtilityEnabled("ResetType")) { + document.querySelector("body > div > div.mt-3 > center").innerHTML = String(document.querySelector("body > div > div.mt-3 > center").innerHTML).replaceAll("ServerTime:", "服务器时间:"); + document.querySelector("body > div > div.mt-3 > center > table").style.marginTop = "10px"; + + document.querySelector("body > div > div.mt-3 > center > form").outerHTML = `
+
+
+
+ + +
+
+
`; + } + if (UtilityEnabled("Translate")) { + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[0].innerText = "编号"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[1].innerText = "标题"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[2].innerText = "状态"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].remove(); + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].innerText = "创建者"; + } + let Temp = document.querySelector("body > div > div.mt-3 > center > table > tbody").childNodes; + for (let i = 1; i < Temp.length; i++) { + let CurrentElement = Temp[i].childNodes[2].childNodes; + if (CurrentElement[1].childNodes[0].data.indexOf("运行中") != -1) { + let Time = String(CurrentElement[1].childNodes[1].innerText).substring(4); + let Day = parseInt(Time.substring(0, Time.indexOf("天"))) || 0; + let Hour = parseInt(Time.substring((Time.indexOf("天") == -1 ? 0 : Time.indexOf("天") + 1), Time.indexOf("小时"))) || 0; + let Minute = parseInt(Time.substring((Time.indexOf("小时") == -1 ? 0 : Time.indexOf("小时") + 2), Time.indexOf("分"))) || 0; + let Second = parseInt(Time.substring((Time.indexOf("分") == -1 ? 0 : Time.indexOf("分") + 1), Time.indexOf("秒"))) || 0; + let TimeStamp = new Date().getTime() + diff + ((((isNaN(Day) ? 0 : Day) * 24 + Hour) * 60 + Minute) * 60 + Second) * 1000; + CurrentElement[1].childNodes[1].setAttribute("EndTime", TimeStamp); + CurrentElement[1].childNodes[1].classList.add("UpdateByJS"); + } else if (CurrentElement[1].childNodes[0].data.indexOf("开始于") != -1) { + let TimeStamp = Date.parse(String(CurrentElement[1].childNodes[0].data).substring(4)) + diff; + CurrentElement[1].setAttribute("EndTime", TimeStamp); + CurrentElement[1].classList.add("UpdateByJS"); + } else if (CurrentElement[1].childNodes[0].data.indexOf("已结束") != -1) { + let TimeStamp = String(CurrentElement[1].childNodes[0].data).substring(4); + CurrentElement[1].childNodes[0].data = " 已结束 "; + CurrentElement[1].className = "red"; + let Temp = document.createElement("span"); + CurrentElement[1].appendChild(Temp); + Temp.className = "green"; + Temp.innerHTML = TimeStamp; + } + Temp[i].childNodes[3].style.display = "none"; + Temp[i].childNodes[4].innerHTML = "" + Temp[i].childNodes[4].innerHTML + ""; + localStorage.setItem("UserScript-Contest-" + Temp[i].childNodes[0].innerText + "-Name", Temp[i].childNodes[1].innerText); + } + } else { + document.getElementsByTagName("h3")[0].innerHTML = "比赛" + document.getElementsByTagName("h3")[0].innerHTML.substring(7); + if (document.querySelector("#time_left") != null) { + let EndTime = document.querySelector("body > div > div.mt-3 > center").childNodes[3].data; + EndTime = EndTime.substring(EndTime.indexOf("结束时间是:") + 6, EndTime.lastIndexOf("。")); + EndTime = new Date(EndTime).getTime(); + if (new Date().getTime() < EndTime) { + document.querySelector("#time_left").classList.add("UpdateByJS"); + document.querySelector("#time_left").setAttribute("EndTime", EndTime); + } + } + let HTMLData = document.querySelector("body > div > div.mt-3 > center > div").innerHTML; + HTMLData = HTMLData.replaceAll("  \n  ", " "); + HTMLData = HTMLData.replaceAll("
开始于: ", "开始时间:"); + HTMLData = HTMLData.replaceAll("\n结束于: ", "
结束时间:"); + HTMLData = HTMLData.replaceAll("\n订正截止日期: ", "
订正截止日期:"); + HTMLData = HTMLData.replaceAll("\n现在时间: ", "当前时间:"); + HTMLData = HTMLData.replaceAll("\n状态:", "
状态:"); + document.querySelector("body > div > div.mt-3 > center > div").innerHTML = HTMLData; + if (UtilityEnabled("RemoveAlerts") && document.querySelector("body > div > div.mt-3 > center").innerHTML.indexOf("尚未开始比赛") != -1) { + document.querySelector("body > div > div.mt-3 > center > a").setAttribute("href", "start_contest.php?cid=" + SearchParams.get("cid")); + } else if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", async () => { + await fetch(location.href) + .then((Response) => { + return Response.text(); + }) + .then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelector("#problemset > tbody").children; + if (UtilityEnabled("ReplaceYN")) { + for (let i = 0; i < Temp.length; i++) { + let Status = Temp[i].children[0].innerText; + if (Status.indexOf("Y") != -1) { + document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_y"; + document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✓"; + } else if (Status.indexOf("N") != -1) { + document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_n"; + document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✗"; + } + } + } + }); + }); + document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); + document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); + document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML = String(document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML).replaceAll("
", "

"); + + document.querySelector("#problemset > tbody").innerHTML = String(document.querySelector("#problemset > tbody").innerHTML).replaceAll(/\t ([0-9]*)      问题  ([^<]*)/g, "$2. $1"); + + document.querySelector("#problemset > tbody").innerHTML = String(document.querySelector("#problemset > tbody").innerHTML).replaceAll(/\t\*([0-9]*)      问题  ([^<]*)/g, "拓展$2. $1"); + + if (UtilityEnabled("MoreSTD") && document.querySelector("#problemset > thead > tr").innerHTML.indexOf("标程") != -1) { + let Temp = document.querySelector("#problemset > thead > tr").children; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].innerText == "标程") { + Temp[i].remove(); + let Temp2 = document.querySelector("#problemset > tbody").children; + for (let j = 0; j < Temp2.length; j++) { + if (Temp2[j].children[i] != undefined) { + Temp2[j].children[i].remove(); + } + } + } + } + document.querySelector("#problemset > thead > tr").innerHTML += "标程"; + Temp = document.querySelector("#problemset > tbody").children; + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerHTML += "打开"; + } + } + + Temp = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].childNodes[0].children.length == 0) { + Temp[i].childNodes[0].innerHTML = "
"; + } + let PID = Temp[i].childNodes[1].innerHTML; + if (PID.substring(0, 2) == "拓展") { + PID = PID.substring(2); + } + Temp[i].children[2].children[0].target = "_blank"; + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + i + "-PID", PID.substring(3)); + localStorage.setItem("UserScript-Problem-" + PID.substring(3) + "-Name", Temp[i].childNodes[2].innerText); + } + let CheatDiv = document.createElement("div"); + CheatDiv.style.marginTop = "20px"; + CheatDiv.style.textAlign = "left"; + document.querySelector("body > div > div.mt-3 > center").insertBefore(CheatDiv, document.querySelector("#problemset")); + if (UtilityEnabled("AutoCheat")) { + let AutoCheatButton = document.createElement("button"); + CheatDiv.appendChild(AutoCheatButton); + AutoCheatButton.className = "btn btn-outline-secondary"; + AutoCheatButton.innerText = "自动提交当年代码"; + AutoCheatButton.style.marginRight = "5px"; + AutoCheatButton.disabled = true; + let ACProblems = [], ContestProblems = []; + const UrlParams = new URLSearchParams(window.location.search); + const CID = UrlParams.get("cid"); + await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < Temp.length; i++) { + ACProblems.push(Number(Temp[i].substring(2, Temp[i].indexOf(",")))); + } + AutoCheatButton.disabled = false; + }); + let Rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Rows.length; i++) { + ContestProblems.push(Rows[i].children[1].innerText.substring(Rows[i].children[1].innerText.indexOf('.') + 2)).toFixed; + } + AutoCheatButton.addEventListener("click", async () => { + AutoCheatButton.disabled = true; + let Submitted = false; + for (let i = 0; i < ContestProblems.length; i++) { + let PID = ContestProblems[i]; + if (ACProblems.indexOf(Number(PID)) == -1) { + console.log("Ignoring problem " + PID + " as it has not been solved yet."); + continue; + } + if (Rows[i].children[0].children[0].classList.contains("status_y")) { + console.log("Ignoring problem " + PID + " as it has already been solved in this contest."); + continue; + } + console.log("Submitting problem " + PID); + Submitted = true; + AutoCheatButton.innerHTML = "正在提交 " + PID; + let SID = 0; + await fetch("https://www.xmoj.tech/status.php?problem_id=" + PID + "&jresult=4") + .then((Result) => { + return Result.text(); + }).then((Result) => { + let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); + SID = ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + await new Promise(r => setTimeout(r, 500)); + let Code = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SID) + .then((Response) => { + return Response.text(); + }).then((Response) => { + Code = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + await new Promise(r => setTimeout(r, 500)); + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, + "method": "POST", + "body": "cid=" + CID + "&pid=" + i + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" + }); + await new Promise(r => setTimeout(r, 500)); + } + if (!Submitted) { + AutoCheatButton.innerHTML = "没有可以提交的题目!"; + await new Promise(r => setTimeout(r, 1000)); + } + AutoCheatButton.disabled = false; + if (Submitted) location.reload(); else AutoCheatButton.innerHTML = "自动提交当年代码"; + }); + document.addEventListener("keydown", (Event) => { + if (Event.code === 'Enter' && (Event.metaKey || Event.ctrlKey)) { + AutoCheatButton.click(); + } + }); + } + if (UtilityEnabled("OpenAllProblem")) { + let OpenAllButton = document.createElement("button"); + OpenAllButton.className = "btn btn-outline-secondary"; + OpenAllButton.innerText = "打开全部题目"; + OpenAllButton.style.marginRight = "5px"; + CheatDiv.appendChild(OpenAllButton); + OpenAllButton.addEventListener("click", () => { + let Rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Rows.length; i++) { + open(Rows[i].children[2].children[0].href, "_blank"); + } + }); + let OpenUnsolvedButton = document.createElement("button"); + OpenUnsolvedButton.className = "btn btn-outline-secondary"; + OpenUnsolvedButton.innerText = "打开未解决题目"; + CheatDiv.appendChild(OpenUnsolvedButton); + OpenUnsolvedButton.addEventListener("click", () => { + let Rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Rows.length; i++) { + if (!Rows[i].children[0].children[0].classList.contains("status_y")) { + open(Rows[i].children[2].children[0].href, "_blank"); + } + } + }); + } + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemCount", document.querySelector("#problemset > tbody").rows.length); + } + } + } else if (location.pathname == "/contestrank-oi.php") { + if (document.querySelector("#rank") == null) { + document.querySelector("body > div > div.mt-3").innerHTML = "

比赛排名

"; + } + if (SearchParams.get("ByUserScript") == null) { + if (document.querySelector("body > div > div.mt-3 > center > h3").innerText == "比赛排名") { + document.querySelector("#rank").innerText = "比赛暂时还没有排名"; + } else { + document.querySelector("body > div > div.mt-3 > center > h3").innerText = document.querySelector("body > div > div.mt-3 > center > h3").innerText.substring(document.querySelector("body > div > div.mt-3 > center > h3").innerText.indexOf(" -- ") + 4) + "(OI排名)"; + document.querySelector("#rank > thead > tr > :nth-child(1)").innerText = "排名"; + document.querySelector("#rank > thead > tr > :nth-child(2)").innerText = "用户"; + document.querySelector("#rank > thead > tr > :nth-child(3)").innerText = "昵称"; + document.querySelector("#rank > thead > tr > :nth-child(4)").innerText = "AC数"; + document.querySelector("#rank > thead > tr > :nth-child(5)").innerText = "得分"; + let RefreshOIRank = async () => { + await fetch(location.href) + .then((Response) => { + return Response.text() + }) + .then(async (Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + TidyTable(ParsedDocument.getElementById("rank")); + let Temp = ParsedDocument.getElementById("rank").rows; + for (var i = 1; i < Temp.length; i++) { + let MetalCell = Temp[i].cells[0]; + let Metal = document.createElement("span"); + Metal.innerText = MetalCell.innerText; + Metal.className = "badge text-bg-primary"; + MetalCell.innerText = ""; + MetalCell.appendChild(Metal); + GetUsernameHTML(Temp[i].cells[1], Temp[i].cells[1].innerText); + Temp[i].cells[2].innerHTML = Temp[i].cells[2].innerText; + Temp[i].cells[3].innerHTML = Temp[i].cells[3].innerText; + for (let j = 5; j < Temp[i].cells.length; j++) { + let InnerText = Temp[i].cells[j].innerText; + let BackgroundColor = Temp[i].cells[j].style.backgroundColor; + let Red = BackgroundColor.substring(4, BackgroundColor.indexOf(",")); + let Green = BackgroundColor.substring(BackgroundColor.indexOf(",") + 2, BackgroundColor.lastIndexOf(",")); + let Blue = BackgroundColor.substring(BackgroundColor.lastIndexOf(",") + 2, BackgroundColor.lastIndexOf(")")); + let NoData = (Red == 238 && Green == 238 && Blue == 238); + let FirstBlood = (Red == 170 && Green == 170 && Blue == 255); + let Solved = (Green == 255); + let ErrorCount = ""; + if (Solved) { + ErrorCount = (Blue == 170 ? 5 : (Blue - 51) / 32); + } else { + ErrorCount = (Blue == 22 ? 15 : (170 - Blue) / 10); + } + if (NoData) { + BackgroundColor = ""; + } else if (FirstBlood) { + BackgroundColor = "rgb(127, 127, 255)"; + } else if (Solved) { + BackgroundColor = "rgb(0, 255, 0, " + Math.max(1 / 10 * (10 - ErrorCount), 0.2) + ")"; + if (ErrorCount != 0) { + InnerText += " (" + (ErrorCount == 5 ? "4+" : ErrorCount) + ")"; + } + } else { + BackgroundColor = "rgba(255, 0, 0, " + Math.min(ErrorCount / 10 + 0.2, 1) + ")"; + if (ErrorCount != 0) { + InnerText += " (" + (ErrorCount == 15 ? "14+" : ErrorCount) + ")"; + } + } + Temp[i].cells[j].innerHTML = InnerText; + Temp[i].cells[j].style.backgroundColor = BackgroundColor; + Temp[i].cells[j].style.color = (UtilityEnabled("DarkMode") ? "white" : "black"); + } + } + document.querySelector("#rank > tbody").innerHTML = ParsedDocument.querySelector("#rank > tbody").innerHTML; + }); + }; + RefreshOIRank(); + document.title = document.querySelector("body > div.container > div > center > h3").innerText; + if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", RefreshOIRank); + } + } + } + Style.innerHTML += "td {"; + Style.innerHTML += " white-space: nowrap;"; + Style.innerHTML += "}"; + document.querySelector("body > div.container > div > center").style.paddingBottom = "10px"; + document.querySelector("body > div.container > div > center > a").style.display = "none"; + document.title = document.querySelector("body > div.container > div > center > h3").innerText; + } else if (location.pathname == "/contestrank-correct.php") { + if (document.querySelector("#rank") == null) { + document.querySelector("body > div > div.mt-3").innerHTML = "

比赛排名

"; + } + if (document.querySelector("body > div > div.mt-3 > center > h3").innerText == "比赛排名") { + document.querySelector("#rank").innerText = "比赛暂时还没有排名"; + } else { + if (UtilityEnabled("ResetType")) { + document.querySelector("body > div > div.mt-3 > center > h3").innerText = document.querySelector("body > div > div.mt-3 > center > h3").innerText.substring(document.querySelector("body > div > div.mt-3 > center > h3").innerText.indexOf(" -- ") + 4) + "(订正排名)"; + document.querySelector("body > div > div.mt-3 > center > a").remove(); + } + document.querySelector("#rank > thead > tr > :nth-child(1)").innerText = "排名"; + document.querySelector("#rank > thead > tr > :nth-child(2)").innerText = "用户"; + document.querySelector("#rank > thead > tr > :nth-child(3)").innerText = "昵称"; + document.querySelector("#rank > thead > tr > :nth-child(4)").innerText = "AC数"; + document.querySelector("#rank > thead > tr > :nth-child(5)").innerText = "得分"; + let RefreshCorrectRank = async () => { + await fetch(location.href) + .then((Response) => { + return Response.text() + }) + .then(async (Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + TidyTable(ParsedDocument.getElementById("rank")); + let Temp = ParsedDocument.getElementById("rank").rows; + for (var i = 1; i < Temp.length; i++) { + let MetalCell = Temp[i].cells[0]; + let Metal = document.createElement("span"); + Metal.innerText = MetalCell.innerText; + Metal.className = "badge text-bg-primary"; + MetalCell.innerText = ""; + MetalCell.appendChild(Metal); + GetUsernameHTML(Temp[i].cells[1], Temp[i].cells[1].innerText); + Temp[i].cells[2].innerHTML = Temp[i].cells[2].innerText; + Temp[i].cells[3].innerHTML = Temp[i].cells[3].innerText; + for (let j = 5; j < Temp[i].cells.length; j++) { + let InnerText = Temp[i].cells[j].innerText; + let BackgroundColor = Temp[i].cells[j].style.backgroundColor; + let Red = BackgroundColor.substring(4, BackgroundColor.indexOf(",")); + let Green = BackgroundColor.substring(BackgroundColor.indexOf(",") + 2, BackgroundColor.lastIndexOf(",")); + let Blue = BackgroundColor.substring(BackgroundColor.lastIndexOf(",") + 2, BackgroundColor.lastIndexOf(")")); + let NoData = (Red == 238 && Green == 238 && Blue == 238); + let FirstBlood = (Red == 170 && Green == 170 && Blue == 255); + let Solved = (Green == 255); + let ErrorCount = ""; + if (Solved) { + ErrorCount = (Blue == 170 ? "4+" : (Blue - 51) / 32); + } else { + ErrorCount = (Blue == 22 ? "14+" : (170 - Blue) / 10); + } + if (NoData) { + BackgroundColor = ""; + } else if (FirstBlood) { + BackgroundColor = "rgba(127, 127, 255, 0.5)"; + } else if (Solved) { + BackgroundColor = "rgba(0, 255, 0, 0.5)"; + if (ErrorCount != 0) { + InnerText += " (" + ErrorCount + ")"; + } + } else { + BackgroundColor = "rgba(255, 0, 0, 0.5)"; + if (ErrorCount != 0) { + InnerText += " (" + ErrorCount + ")"; + } + } + Temp[i].cells[j].innerHTML = InnerText; + Temp[i].cells[j].style.backgroundColor = BackgroundColor; + } + } + document.querySelector("#rank > tbody").innerHTML = ParsedDocument.querySelector("#rank > tbody").innerHTML; + }); + }; + RefreshCorrectRank(); + document.title = document.querySelector("body > div.container > div > center > h3").innerText; + if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", RefreshCorrectRank); + } + } + } else if (location.pathname == "/submitpage.php") { + document.title = "提交代码: " + (SearchParams.get("id") != null ? "题目" + Number(SearchParams.get("id")) : "比赛" + Number(SearchParams.get("cid"))); + document.querySelector("body > div > div.mt-3").innerHTML = `
` + `

提交代码

` + (SearchParams.get("id") != null ? `题目${Number(SearchParams.get("id"))}` : `比赛${Number(SearchParams.get("cid")) + ` 题目` + String.fromCharCode(65 + parseInt(SearchParams.get("pid")))}`) + `
+ +
+ +
+ + +
`; + if (UtilityEnabled("AutoO2")) { + document.querySelector("#enable_O2").checked = true; + } + let CodeMirrorElement; + (() => { + CodeMirrorElement = CodeMirror.fromTextArea(document.querySelector("#CodeInput"), { + lineNumbers: true, + matchBrackets: true, + mode: "text/x-c++src", + indentUnit: 4, + indentWithTabs: true, + enterMode: "keep", + tabMode: "shift", + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + extraKeys: { + "Ctrl-Space": "autocomplete", "Ctrl-Enter": function (instance) { + Submit.click(); + } + } + }); + })(); + CodeMirrorElement.setSize("100%", "auto"); + CodeMirrorElement.getWrapperElement().style.border = "1px solid #ddd"; + + if (SearchParams.get("sid") !== null) { + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("sid")) + .then((Response) => { + return Response.text() + }) + .then((Response) => { + CodeMirrorElement.setValue(Response.substring(0, Response.indexOf("/**************************************************************")).trim()); + }); + } + + PassCheck.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + document.querySelector("#Submit").disabled = true; + document.querySelector("#Submit").value = "正在提交..."; + let o2Switch = "&enable_O2=on"; + if (!document.querySelector("#enable_O2").checked) o2Switch = ""; + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + }).then(async (Response) => { + if (Response.redirected) { + location.href = Response.url; + } else { + const text = await Response.text(); + if (text.indexOf("没有这个比赛!") !== -1 && new URL(location.href).searchParams.get("pid") !== null) { + // Credit: https://github.com/boomzero/quicksubmit/blob/main/index.ts + // Also licensed under GPL-3.0 + const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + new URL(location.href).searchParams.get("cid")); + const res = await contestReq.text(); + if ( + contestReq.status !== 200 || + res.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1 + ) { + console.error(`Failed to get contest page!`); + return; + } + const parser = new DOMParser(); + const dom = parser.parseFromString(res, "text/html"); + const contestProblems = []; + const rows = (dom.querySelector( + "#problemset > tbody", + )).rows; + for (let i = 0; i < rows.length; i++) { + contestProblems.push( + rows[i].children[1].textContent.substring(2, 6).replaceAll( + "\t", + "", + ), + ); + } + rPID = contestProblems[new URL(location.href).searchParams.get("pid")]; + if (UtilityEnabled("DebugMode")) { + console.log("Contest Problems:", contestProblems); + console.log("Real PID:", rPID); + } + ErrorElement.style.display = "block"; + ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "比赛已结束, 正在尝试像题目 " + rPID + " 提交"; + console.log("比赛已结束, 正在尝试像题目 " + rPID + " 提交"); + let o2Switch = "&enable_O2=on"; + if (!document.querySelector("#enable_O2").checked) o2Switch = ""; + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + }).then(async (Response) => { + if (Response.redirected) { + location.href = Response.url; + } + console.log(await Response.text()); + }); + + } + if (UtilityEnabled("DebugMode")) { + console.log("Submission failed! Response:", text); + } + ErrorElement.style.display = "block"; + ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "提交失败!请关闭脚本后重试!"; + Submit.disabled = false; + Submit.value = "提交"; + } + }); + }); + + Submit.addEventListener("click", async () => { + PassCheck.style.display = "none"; + ErrorElement.style.display = "none"; + document.querySelector("#Submit").disabled = true; + document.querySelector("#Submit").value = "正在检查..."; + let Source = CodeMirrorElement.getValue(); + let PID = 0; + let IOFilename = ""; + if (SearchParams.get("cid") != null && SearchParams.get("pid") != null) { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + } else { + PID = SearchParams.get("id"); + } + IOFilename = localStorage.getItem("UserScript-Problem-" + PID + "-IOFilename"); + if (UtilityEnabled("IOFile") && IOFilename != null) { + if (Source.indexOf(IOFilename) == -1) { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "此题输入输出文件名为" + IOFilename + ",请检查是否填错"; + + let freopenText = document.createElement('small'); + if (UtilityEnabled("DarkMode")) freopenText.style.color = "white"; else freopenText.style.color = "black"; + freopenText.textContent = '\n您也可以复制freopen语句。\n'; + document.getElementById('ErrorMessage').appendChild(freopenText); + let copyFreopenButton = document.createElement("button"); + copyFreopenButton.className = "btn btn-sm btn-outline-secondary copy-btn"; + copyFreopenButton.innerText = "复制代码"; + copyFreopenButton.style.marginLeft = "10px"; + copyFreopenButton.style.marginTop = "10px"; + copyFreopenButton.style.marginBottom = "10px"; + copyFreopenButton.type = "button"; + copyFreopenButton.addEventListener("click", () => { + navigator.clipboard.writeText('\n freopen("' + IOFilename + '.in", "r", stdin);\n freopen("' + IOFilename + '.out", "w", stdout);'); + copyFreopenButton.innerText = "复制成功"; + setTimeout(() => { + copyFreopenButton.innerText = "复制代码"; + }, 1500); + }); + document.getElementById('ErrorMessage').appendChild(copyFreopenButton); + let freopenCodeField = CodeMirror(document.getElementById('ErrorMessage'), { + value: 'freopen("' + IOFilename + '.in", "r", stdin);\nfreopen("' + IOFilename + '.out", "w", stdout);', + mode: 'text/x-c++src', + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + readOnly: true, + lineNumbers: true + }); + freopenCodeField.setSize("100%", "auto"); + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } else if (RegExp("//.*freopen").test(Source)) { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "请不要注释freopen语句"; + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } + } + if (Source == "") { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "源代码为空"; + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } + if (UtilityEnabled("CompileError")) { + let ResponseData = await new Promise((Resolve) => { + GM_xmlhttpRequest({ + method: "POST", url: "https://cppinsights.io/api/v1/transform", headers: { + "content-type": "application/json;charset=UTF-8" + }, referrer: "https://cppinsights.io/", data: JSON.stringify({ + "insightsOptions": ["cpp14"], "code": Source + }), onload: (Response) => { + Resolve(Response); + } + }); + }); + let Response = JSON.parse(ResponseData.responseText); + if (Response.returncode) { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "编译错误:\n" + Response.stderr.trim(); + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } else { + PassCheck.click(); + } + } else { + PassCheck.click(); + } + }); + } else if (location.pathname == "/modifypage.php") { + if (SearchParams.get("ByUserScript") != null) { + document.title = "XMOJ-Script 更新日志"; + document.querySelector("body > div > div.mt-3").innerHTML = ""; + await fetch(ServerURL + "/Update.json", {cache: "no-cache"}) + .then((Response) => { + return Response.json(); + }) + .then((Response) => { + for (let i = Object.keys(Response.UpdateHistory).length - 1; i >= 0; i--) { + let Version = Object.keys(Response.UpdateHistory)[i]; + let Data = Response.UpdateHistory[Version]; + let UpdateDataCard = document.createElement("div"); + document.querySelector("body > div > div.mt-3").appendChild(UpdateDataCard); + UpdateDataCard.className = "card mb-3"; + if (Data.Prerelease) UpdateDataCard.classList.add("text-secondary"); + let UpdateDataCardBody = document.createElement("div"); + UpdateDataCard.appendChild(UpdateDataCardBody); + UpdateDataCardBody.className = "card-body"; + let UpdateDataCardTitle = document.createElement("h5"); + UpdateDataCardBody.appendChild(UpdateDataCardTitle); + UpdateDataCardTitle.className = "card-title"; + UpdateDataCardTitle.innerText = Version; + if (Data.Prerelease) { + UpdateDataCardTitle.innerHTML += "(预览版)"; + } + let UpdateDataCardSubtitle = document.createElement("h6"); + UpdateDataCardBody.appendChild(UpdateDataCardSubtitle); + UpdateDataCardSubtitle.className = "card-subtitle mb-2 text-muted"; + UpdateDataCardSubtitle.innerHTML = GetRelativeTime(Data.UpdateDate); + let UpdateDataCardText = document.createElement("p"); + UpdateDataCardBody.appendChild(UpdateDataCardText); + UpdateDataCardText.className = "card-text"; + //release notes + if (Data.Notes != undefined) { + UpdateDataCardText.innerHTML = Data.Notes; + } + let UpdateDataCardList = document.createElement("ul"); + UpdateDataCardText.appendChild(UpdateDataCardList); + UpdateDataCardList.className = "list-group list-group-flush"; + for (let j = 0; j < Data.UpdateContents.length; j++) { + let UpdateDataCardListItem = document.createElement("li"); + UpdateDataCardList.appendChild(UpdateDataCardListItem); + UpdateDataCardListItem.className = "list-group-item"; + UpdateDataCardListItem.innerHTML = "(" + "#" + Data.UpdateContents[j].PR + ") " + Data.UpdateContents[j].Description; + } + let UpdateDataCardLink = document.createElement("a"); + UpdateDataCardBody.appendChild(UpdateDataCardLink); + UpdateDataCardLink.className = "card-link"; + UpdateDataCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script/releases/tag/" + Version; + UpdateDataCardLink.target = "_blank"; + UpdateDataCardLink.innerText = "查看该版本"; + } + }); + } else { + document.title = "修改账号"; + let Nickname = document.getElementsByName("nick")[0].value; + let School = document.getElementsByName("school")[0].value; + let EmailAddress = document.getElementsByName("email")[0].value; + let CodeforcesAccount = document.getElementsByName("acc_cf")[0].value; + let AtcoderAccount = document.getElementsByName("acc_atc")[0].value; + let USACOAccount = document.getElementsByName("acc_usaco")[0].value; + let LuoguAccount = document.getElementsByName("acc_luogu")[0].value; + document.querySelector("body > div > div").innerHTML = `
+
+
+
+
+
+
+ + 修改头像 +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
`; + document.getElementById("Nickname").value = Nickname; + document.getElementById("School").value = School; + document.getElementById("EmailAddress").value = EmailAddress; + document.getElementById("CodeforcesAccount").value = CodeforcesAccount; + document.getElementById("AtcoderAccount").value = AtcoderAccount; + document.getElementById("USACOAccount").value = USACOAccount; + document.getElementById("LuoguAccount").value = LuoguAccount; + RequestAPI("GetBadge", { + "UserID": String(CurrentUsername) + }, (Response) => { + if (Response.Success) { + BadgeRow.style.display = ""; + BadgeContent.value = Response.Data.Content; + BadgeBackgroundColor.value = Response.Data.BackgroundColor; + BadgeColor.value = Response.Data.Color; + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-" + CurrentUsername + "-Badge-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + } + }); + ModifyInfo.addEventListener("click", async () => { + ModifyInfo.disabled = true; + ModifyInfo.querySelector("span").style.display = ""; + ErrorElement.style.display = "none"; + SuccessElement.style.display = "none"; + let BadgeContent = document.querySelector("#BadgeContent").value; + let BadgeBackgroundColor = document.querySelector("#BadgeBackgroundColor").value; + let BadgeColor = document.querySelector("#BadgeColor").value; + await new Promise((Resolve) => { + RequestAPI("EditBadge", { + "UserID": String(CurrentUsername), + "Content": String(BadgeContent), + "BackgroundColor": String(BadgeBackgroundColor), + "Color": String(BadgeColor) + }, (Response) => { + if (Response.Success) { + Resolve(); + } else { + ModifyInfo.disabled = false; + ModifyInfo.querySelector("span").style.display = "none"; + ErrorElement.style.display = "block"; + ErrorElement.innerText = Response.Message; + } + }); + }); + let Nickname = document.querySelector("#Nickname").value; + let OldPassword = document.querySelector("#OldPassword").value; + let NewPassword = document.querySelector("#NewPassword").value; + let NewPasswordAgain = document.querySelector("#NewPasswordAgain").value; + let School = document.querySelector("#School").value; + let EmailAddress = document.querySelector("#EmailAddress").value; + let CodeforcesAccount = document.querySelector("#CodeforcesAccount").value; + let AtcoderAccount = document.querySelector("#AtcoderAccount").value; + let USACOAccount = document.querySelector("#USACOAccount").value; + let LuoguAccount = document.querySelector("#LuoguAccount").value; + await fetch("https://www.xmoj.tech/modify.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": "nick=" + encodeURIComponent(Nickname) + "&" + "opassword=" + encodeURIComponent(OldPassword) + "&" + "npassword=" + encodeURIComponent(NewPassword) + "&" + "rptpassword=" + encodeURIComponent(NewPasswordAgain) + "&" + "school=" + encodeURIComponent(School) + "&" + "email=" + encodeURIComponent(EmailAddress) + "&" + "acc_cf=" + encodeURIComponent(CodeforcesAccount) + "&" + "acc_atc=" + encodeURIComponent(AtcoderAccount) + "&" + "acc_usaco=" + encodeURIComponent(USACOAccount) + "&" + "acc_luogu=" + encodeURIComponent(LuoguAccount) + }); + ModifyInfo.disabled = false; + ModifyInfo.querySelector("span").style.display = "none"; + SuccessElement.style.display = "block"; + }); + if (UtilityEnabled("ExportACCode")) { + let ExportACCode = document.createElement("button"); + document.querySelector("body > div.container > div").appendChild(ExportACCode); + ExportACCode.innerText = "导出AC代码"; + ExportACCode.className = "btn btn-outline-secondary"; + ExportACCode.addEventListener("click", () => { + ExportACCode.disabled = true; + ExportACCode.innerText = "正在导出..."; + let Request = new XMLHttpRequest(); + Request.addEventListener("readystatechange", () => { + if (Request.readyState == 4) { + if (Request.status == 200) { + let Response = Request.responseText; + let ACCode = Response.split("------------------------------------------------------\r\n"); + let ScriptElement = document.createElement("script"); + ScriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; + document.head.appendChild(ScriptElement); + ScriptElement.onload = () => { + var Zip = new JSZip(); + for (let i = 0; i < ACCode.length; i++) { + let CurrentCode = ACCode[i]; + if (CurrentCode != "") { + let CurrentQuestionID = CurrentCode.substring(7, 11); + CurrentCode = CurrentCode.substring(14); + CurrentCode = CurrentCode.replaceAll("\r", ""); + Zip.file(CurrentQuestionID + ".cpp", CurrentCode); + } + } + ExportACCode.innerText = "正在生成压缩包……"; + Zip.generateAsync({type: "blob"}) + .then(function (Content) { + saveAs(Content, "ACCodes.zip"); + ExportACCode.innerText = "AC代码导出成功"; + ExportACCode.disabled = false; + setTimeout(() => { + ExportACCode.innerText = "导出AC代码"; + }, 1000); + }); + }; + } else { + ExportACCode.disabled = false; + ExportACCode.innerText = "AC代码导出失败"; + setTimeout(() => { + ExportACCode.innerText = "导出AC代码"; + }, 1000); + } + } + }); + Request.open("GET", "https://www.xmoj.tech/export_ac_code.php", true); + Request.send(); + }); + } + } + } else if (location.pathname == "/userinfo.php") { + if (SearchParams.get("ByUserScript") === null) { + if (UtilityEnabled("RemoveUseless")) { + let Temp = document.getElementById("submission").childNodes; + for (let i = 0; i < Temp.length; i++) { + Temp[i].remove(); + } + } + eval(document.querySelector("body > script:nth-child(5)").innerHTML); + document.querySelector("#statics > tbody > tr:nth-child(1)").remove(); + + let Temp = document.querySelector("#statics > tbody").children; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].children[0] != undefined) { + if (Temp[i].children[0].innerText == "Statistics") { + Temp[i].children[0].innerText = "统计"; + } else if (Temp[i].children[0].innerText == "Email:") { + Temp[i].children[0].innerText = "电子邮箱"; + } + Temp[i].children[1].removeAttribute("align"); + } + } + + Temp = document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)").childNodes; + let ACProblems = []; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].tagName == "A" && Temp[i].href.indexOf("problem.php?id=") != -1) { + ACProblems.push(Number(Temp[i].innerText.trim())); + } + } + document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)").remove(); + + let UserID, UserNick; + [UserID, UserNick] = document.querySelector("#statics > caption").childNodes[0].data.trim().split("--"); + document.querySelector("#statics > caption").remove(); + document.title = "用户 " + UserID + " 的个人中心"; + let Row = document.createElement("div"); + Row.className = "row"; + let LeftDiv = document.createElement("div"); + LeftDiv.className = "col-md-5"; + Row.appendChild(LeftDiv); + + let LeftTopDiv = document.createElement("div"); + LeftTopDiv.className = "row mb-2"; + LeftDiv.appendChild(LeftTopDiv); + let AvatarContainer = document.createElement("div"); + AvatarContainer.classList.add("col-auto"); + let AvatarElement = document.createElement("img"); + let UserEmailHash = (await GetUserInfo(UserID)).EmailHash; + if (UserEmailHash == undefined) { + AvatarElement.src = `https://cravatar.cn/avatar/00000000000000000000000000000000?d=mp&f=y`; + } else { + AvatarElement.src = `https://cravatar.cn/avatar/${UserEmailHash}?d=retro`; + } + AvatarElement.classList.add("rounded", "me-2"); + AvatarElement.style.height = "120px"; + AvatarContainer.appendChild(AvatarElement); + LeftTopDiv.appendChild(AvatarContainer); + + let UserInfoElement = document.createElement("div"); + UserInfoElement.classList.add("col-auto"); + UserInfoElement.style.lineHeight = "40px"; + UserInfoElement.innerHTML += "用户名:" + UserID + "
"; + UserInfoElement.innerHTML += "昵称:" + UserNick + "
"; + if (UtilityEnabled("Rating")) { + UserInfoElement.innerHTML += "评分:" + ((await GetUserInfo(UserID)).Rating) + "
"; + } + // Create a placeholder for the last online time + let lastOnlineElement = document.createElement('div'); + lastOnlineElement.innerHTML = "最后在线:加载中...
"; + UserInfoElement.appendChild(lastOnlineElement); + let BadgeInfo = await GetUserBadge(UserID); + if (IsAdmin) { + if (BadgeInfo.Content !== "") { + let DeleteBadgeButton = document.createElement("button"); + DeleteBadgeButton.className = "btn btn-outline-danger btn-sm"; + DeleteBadgeButton.innerText = "删除标签"; + DeleteBadgeButton.addEventListener("click", async () => { + if (confirm("您确定要删除此标签吗?")) { + RequestAPI("DeleteBadge", { + "UserID": UserID + }, (Response) => { + if (UtilityEnabled("DebugMode")) console.log(Response); + if (Response.Success) { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-" + UserID + "-Badge-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + window.location.reload(); + } else { + SmartAlert(Response.Message); + } + }); + } + }); + UserInfoElement.appendChild(DeleteBadgeButton); + } else { + let AddBadgeButton = document.createElement("button"); + AddBadgeButton.className = "btn btn-outline-primary btn-sm"; + AddBadgeButton.innerText = "添加标签"; + AddBadgeButton.addEventListener("click", async () => { + RequestAPI("NewBadge", { + "UserID": UserID + }, (Response) => { + if (Response.Success) { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-" + UserID + "-Badge-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + window.location.reload(); + } else { + SmartAlert(Response.Message); + } + }); + }); + UserInfoElement.appendChild(AddBadgeButton); + } + } + RequestAPI("LastOnline", {"Username": UserID}, (result) => { + if (result.Success) { + if (UtilityEnabled("DebugMode")) { + console.log('lastOnline:' + result.Data.logintime); + } + lastOnlineElement.innerHTML = "最后在线:" + GetRelativeTime(result.Data.logintime) + "
"; + } else { + lastOnlineElement.innerHTML = "最后在线:近三个月内从未
"; + } + }); + LeftTopDiv.appendChild(UserInfoElement); + LeftDiv.appendChild(LeftTopDiv); + + let LeftTable = document.querySelector("body > div > div > center > table"); + LeftDiv.appendChild(LeftTable); + let RightDiv = document.createElement("div"); + RightDiv.className = "col-md-7"; + Row.appendChild(RightDiv); + RightDiv.innerHTML = "
已解决题目
"; + for (let i = 0; i < ACProblems.length; i++) { + RightDiv.innerHTML += "" + ACProblems[i] + " "; + } + document.querySelector("body > div > div").innerHTML = ""; + document.querySelector("body > div > div").appendChild(Row); + } else { + document.title = "上传标程"; + document.querySelector("body > div > div.mt-3").innerHTML = ` + +
+
0%
+
+

+ 您必须要上传标程以后才能使用“查看标程”功能。点击“上传标程”按钮以后,系统会自动上传标程,请您耐心等待。
+ 首次上传标程可能会比较慢,请耐心等待。后续将可以自动上传AC代码。
+ 系统每过30天会自动提醒您上传标程,您必须要上传标程,否则将会被禁止使用“查看标程”功能。
+

`; + UploadStd.addEventListener("click", async () => { + UploadStd.disabled = true; + ErrorElement.style.display = "none"; + ErrorElement.innerText = ""; + UploadProgress.classList.remove("bg-success"); + UploadProgress.classList.remove("bg-warning"); + UploadProgress.classList.remove("bg-danger"); + UploadProgress.classList.add("progress-bar-animated"); + UploadProgress.style.width = "0%"; + UploadProgress.innerText = "0%"; + let ACList = []; + await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let ScriptData = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText; + ScriptData = ScriptData.substr(ScriptData.indexOf("}") + 1).trim(); + ScriptData = ScriptData.split(";"); + for (let i = 0; i < ScriptData.length; i++) { + ACList.push(Number(ScriptData[i].substring(2, ScriptData[i].indexOf(",")))); + } + }); + RequestAPI("GetStdList", {}, async (Result) => { + if (Result.Success) { + let StdList = Result.Data.StdList; + for (let i = 0; i < ACList.length; i++) { + if (StdList.indexOf(ACList[i]) === -1 && ACList[i] !== 0) { + await new Promise((Resolve) => { + RequestAPI("UploadStd", { + "ProblemID": Number(ACList[i]) + }, (Result) => { + if (!Result.Success) { + ErrorElement.style.display = "block"; + ErrorElement.innerText += Result.Message + "\n"; + UploadProgress.classList.add("bg-warning"); + } + UploadProgress.innerText = (i / ACList.length * 100).toFixed(1) + "% (" + ACList[i] + ")"; + UploadProgress.style.width = (i / ACList.length * 100) + "%"; + Resolve(); + }); + }); + } + } + UploadProgress.classList.add("bg-success"); + UploadProgress.classList.remove("progress-bar-animated"); + UploadProgress.innerText = "100%"; + UploadProgress.style.width = "100%"; + UploadStd.disabled = false; + localStorage.setItem("UserScript-LastUploadedStdTime", new Date().getTime()); + } else { + ErrorElement.style.display = "block"; + ErrorElement.innerText = Result.Message; + UploadStd.disabled = false; + } + }); + }); + } + } else if (location.pathname == "/comparesource.php") { + if (UtilityEnabled("CompareSource")) { + if (location.search == "") { + document.querySelector("body > div.container > div").innerHTML = ""; + let LeftCodeText = document.createElement("span"); + document.querySelector("body > div.container > div").appendChild(LeftCodeText); + LeftCodeText.innerText = "左侧代码的运行编号:"; + let LeftCode = document.createElement("input"); + document.querySelector("body > div.container > div").appendChild(LeftCode); + LeftCode.classList.add("form-control"); + LeftCode.style.width = "40%"; + LeftCode.style.marginBottom = "5px"; + let RightCodeText = document.createElement("span"); + document.querySelector("body > div.container > div").appendChild(RightCodeText); + RightCodeText.innerText = "右侧代码的运行编号:"; + let RightCode = document.createElement("input"); + document.querySelector("body > div.container > div").appendChild(RightCode); + RightCode.classList.add("form-control"); + RightCode.style.width = "40%"; + RightCode.style.marginBottom = "5px"; + let CompareButton = document.createElement("button"); + document.querySelector("body > div.container > div").appendChild(CompareButton); + CompareButton.innerText = "比较"; + CompareButton.className = "btn btn-primary"; + CompareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php?left=" + Number(LeftCode.value) + "&right=" + Number(RightCode.value); + }); + } else { + document.querySelector("body > div > div.mt-3").innerHTML = ` +
+ + +
+
`; + + let LeftCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("left")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + LeftCode = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + let RightCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("right")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + RightCode = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + + let MergeViewElement = CodeMirror.MergeView(CompareElement, { + value: LeftCode, + origLeft: null, + orig: RightCode, + lineNumbers: true, + mode: "text/x-c++src", + collapseIdentical: "true", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + revertButtons: false, + ignoreWhitespace: true + }); + + IgnoreWhitespace.addEventListener("change", () => { + MergeViewElement.ignoreWhitespace = ignorews.checked; + }); + } + } + } else if (location.pathname == "/loginpage.php") { + if (UtilityEnabled("NewBootstrap")) { + document.querySelector("#login").innerHTML = `
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+
`; + } + let ErrorText = document.createElement("div"); + ErrorText.style.color = "red"; + ErrorText.style.marginBottom = "5px"; + document.querySelector("#login").appendChild(ErrorText); + let LoginButton = document.getElementsByName("submit")[0]; + LoginButton.addEventListener("click", async () => { + let Username = document.getElementsByName("user_id")[0].value; + let Password = document.getElementsByName("password")[0].value; + if (Username == "" || Password == "") { + ErrorText.innerText = "用户名或密码不能为空"; + } else { + await fetch("https://www.xmoj.tech/login.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: "user_id=" + encodeURIComponent(Username) + "&password=" + hex_md5(Password) + }) + .then((Response) => { + return Response.text(); + }) + .then(async (Response) => { + if (UtilityEnabled("LoginFailed")) { + if (Response.indexOf("history.go(-2);") != -1) { + if (UtilityEnabled("SavePassword")) { + await storeCredential(Username, Password); + } + let NewPage = localStorage.getItem("UserScript-LastPage"); + if (NewPage == null) { + NewPage = "https://www.xmoj.tech/index.php"; + } + location.href = NewPage; + } else { + if (UtilityEnabled("SavePassword")) { + clearCredential(); + } + Response = Response.substring(Response.indexOf("alert('") + 7); + Response = Response.substring(0, Response.indexOf("');")); + if (Response == "UserName or Password Wrong!") { + ErrorText.innerText = "用户名或密码错误!"; + } else { + ErrorText.innerText = Response; + } + } + } else { + document.innerHTML = Response; + } + }); + } + }); + if (UtilityEnabled("SavePassword")) { + (async () => { + let Credential = await getCredential(); + if (Credential) { + document.querySelector("#login > div:nth-child(1) > div > input").value = Credential.id; + document.querySelector("#login > div:nth-child(2) > div > input").value = Credential.password; + LoginButton.click(); + } + })(); + } + } else if (location.pathname == "/contest_video.php" || location.pathname == "/problem_video.php") { + let ScriptData = document.querySelector("body > div > div.mt-3 > center > script").innerHTML; + if (document.getElementById("J_prismPlayer0").innerHTML != "") { + document.getElementById("J_prismPlayer0").innerHTML = ""; + if (player) { + player.dispose(); + } + eval(ScriptData); + } + if (UtilityEnabled("DownloadPlayback")) { + ScriptData = ScriptData.substring(ScriptData.indexOf("{")); + ScriptData = ScriptData.substring(0, ScriptData.indexOf("}") + 1); + ScriptData = ScriptData.replace(/([a-zA-Z0-9]+) ?:/g, "\"$1\":"); + ScriptData = ScriptData.replace(/'/g, "\""); + let VideoData = JSON.parse(ScriptData); + let RandomUUID = () => { + let t = "0123456789abcdef"; + let e = []; + for (let r = 0; r < 36; r++) e[r] = t.substr(Math.floor(16 * Math.random()), 1); + e[14] = "4"; + e[19] = t.substr(3 & e[19] | 8, 1); + e[8] = e[13] = e[18] = e[23] = "-"; + return e.join(""); + }; + let URLParams = new URLSearchParams({ + "AccessKeyId": VideoData.accessKeyId, + "Action": "GetPlayInfo", + "VideoId": VideoData.vid, + "Formats": "", + "AuthTimeout": 7200, + "Rand": RandomUUID(), + "SecurityToken": VideoData.securityToken, + "StreamType": "video", + "Format": "JSON", + "Version": "2017-03-21", + "SignatureMethod": "HMAC-SHA1", + "SignatureVersion": "1.0", + "SignatureNonce": RandomUUID(), + "PlayerVersion": "2.9.3", + "Channel": "HTML5" + }); + URLParams.sort(); + await fetch("https://vod." + VideoData.region + ".aliyuncs.com/?" + URLParams.toString() + "&Signature=" + encodeURIComponent(CryptoJS.HmacSHA1("GET&%2F&" + encodeURIComponent(URLParams.toString()), VideoData.accessKeySecret + "&").toString(CryptoJS.enc.Base64))) + .then((Response) => { + return Response.json(); + }) + .then((Response) => { + let DownloadButton = document.createElement("a"); + DownloadButton.className = "btn btn-outline-secondary"; + DownloadButton.innerText = "下载"; + DownloadButton.href = Response.PlayInfoList.PlayInfo[0].PlayURL; + DownloadButton.download = Response.VideoBase.Title; + document.querySelector("body > div > div.mt-3 > center").appendChild(DownloadButton); + }); + } + } else if (location.pathname == "/reinfo.php") { + document.title = "测试点信息: " + SearchParams.get("sid"); + if (document.querySelector("#results > div") == undefined) { + document.querySelector("#results").parentElement.innerHTML = "没有测试点信息"; + } else { + for (let i = 0; i < document.querySelector("#results > div").children.length; i++) { + let CurrentElement = document.querySelector("#results > div").children[i].children[0].children[0].children[0]; + let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/"); + CurrentElement.innerText = TimeToStringTime(Temp[0]) + "/" + SizeToStringSize(Temp[1]); + } + if (document.getElementById("apply_data")) { + let ApplyDiv = document.getElementById("apply_data").parentElement; + console.log("启动!!!"); + if (UtilityEnabled("ApplyData")) { + let GetDataButton = document.createElement("button"); + GetDataButton.className = "ms-2 btn btn-outline-secondary"; + GetDataButton.innerText = "获取数据"; + console.log("按钮创建成功"); + ApplyDiv.appendChild(GetDataButton); + GetDataButton.addEventListener("click", async () => { + GetDataButton.disabled = true; + GetDataButton.innerText = "正在获取数据..."; + let PID = localStorage.getItem("UserScript-Solution-" + SearchParams.get("sid") + "-Problem"); + if (PID == null) { + GetDataButton.innerText = "失败! 无法获取PID"; + GetDataButton.disabled = false; + await new Promise((resolve) => { + setTimeout(resolve, 800); + }); + GetDataButton.innerText = "获取数据"; + return; + } + let Code = ""; + if (localStorage.getItem(`UserScript-Problem-${PID}-IOFilename`) !== null) { + Code = `#define IOFile "${localStorage.getItem(`UserScript-Problem-${PID}-IOFilename`)}"\n`; + } + Code += `//XMOJ-Script 获取数据代码 + #include +using namespace std; +string Base64Encode(string Input) +{ + const string Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + string Output; + for (int i = 0; i < Input.length(); i += 3) + { + Output.push_back(i + 0 > Input.length() ? '=' : Base64Chars[(Input[i + 0] & 0xfc) >> 2]); + Output.push_back(i + 1 > Input.length() ? '=' : Base64Chars[((Input[i + 0] & 0x03) << 4) + ((Input[i + 1] & 0xf0) >> 4)]); + Output.push_back(i + 2 > Input.length() ? '=' : Base64Chars[((Input[i + 1] & 0x0f) << 2) + ((Input[i + 2] & 0xc0) >> 6)]); + Output.push_back(i + 3 > Input.length() ? '=' : Base64Chars[Input[i + 2] & 0x3f]); + } + return Output; +} +int main() +{ +#ifdef IOFile + freopen(IOFile ".in", "r", stdin); + freopen(IOFile ".out", "w", stdout); +#endif + string Input; + while (1) + { + char Data = getchar(); + if (Data == EOF) + break; + Input.push_back(Data); + } + throw logic_error("[" + Base64Encode(Input.c_str()) + "]"); + return 0; +}`; + + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, + "method": "POST", + "body": "id=" + PID + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" + }); + + let SID = await fetch("https://www.xmoj.tech/status.php").then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + return ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + + await new Promise((Resolve) => { + let Interval = setInterval(async () => { + await fetch("status-ajax.php?solution_id=" + SID).then((Response) => { + return Response.text(); + }).then((Response) => { + if (Response.split(",")[0] >= 4) { + clearInterval(Interval); + Resolve(); + } + }); + }, 500); + }); + + await fetch(`https://www.xmoj.tech/reinfo.php?sid=${SID}`).then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let ErrorData = ParsedDocument.getElementById("errtxt").innerText; + let MatchResult = ErrorData.match(/\what\(\): \[([A-Za-z0-9+\/=]+)\]/g); + if (MatchResult === null) { + GetDataButton.innerText = "获取数据失败"; + GetDataButton.disabled = false; + return; + } + for (let i = 0; i < MatchResult.length; i++) { + let Data = CryptoJS.enc.Base64.parse(MatchResult[i].substring(10, MatchResult[i].length - 1)).toString(CryptoJS.enc.Utf8); + ApplyDiv.appendChild(document.createElement("hr")); + ApplyDiv.appendChild(document.createTextNode("数据" + (i + 1) + ":")); + let CodeElement = document.createElement("div"); + ApplyDiv.appendChild(CodeElement); + CodeMirror(CodeElement, { + value: Data, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + lineNumbers: true, + readOnly: true + }).setSize("100%", "auto"); + } + GetDataButton.innerText = "获取数据成功"; + GetDataButton.disabled = false; + }); + }); + } + document.getElementById("apply_data").addEventListener("click", () => { + let ApplyElements = document.getElementsByClassName("data"); + for (let i = 0; i < ApplyElements.length; i++) { + ApplyElements[i].style.display = (ApplyElements[i].style.display == "block" ? "" : "block"); + } + }); + } + let ApplyElements = document.getElementsByClassName("data"); + for (let i = 0; i < ApplyElements.length; i++) { + ApplyElements[i].addEventListener("click", async () => { + await fetch("https://www.xmoj.tech/data_distribute_ajax_apply.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: "user_id=" + CurrentUsername + "&" + "solution_id=" + SearchParams.get("sid") + "&" + "name=" + ApplyElements[i].getAttribute("name") + }).then((Response) => { + return Response.json(); + }).then((Response) => { + ApplyElements[i].innerText = Response.msg; + setTimeout(() => { + ApplyElements[i].innerText = "申请数据"; + }, 1000); + }); + }); + } + } + } else if (location.pathname == "/downloads.php") { + let SoftwareList = document.querySelector("body > div > ul"); + SoftwareList.remove(); + SoftwareList = document.createElement("ul"); + SoftwareList.className = "software_list"; + let Container = document.createElement("div"); + document.querySelector("body > div").appendChild(Container); + Container.className = "mt-3"; + Container.appendChild(SoftwareList); + if (UtilityEnabled("NewDownload")) { + let Softwares = [{ + "Name": "Bloodshed Dev-C++", + "Image": "https://a.fsdn.com/allura/p/dev-cpp/icon", + "URL": "https://sourceforge.net/projects/dev-cpp/" + }, { + "Name": "DevC++ 5.11 TDM-GCC 4.9.2", + "Image": "https://www.xmoj.tech/image/devcpp.png", + "URL": "https://www.xmoj.tech/downloads/Dev-Cpp+5.11+TDM-GCC+4.9.2+Setup.exe" + }, { + "Name": "Orwell Dev-C++", + "Image": "https://a.fsdn.com/allura/p/orwelldevcpp/icon", + "URL": "https://sourceforge.net/projects/orwelldevcpp/" + }, { + "Name": "Embarcadero Dev-C++", + "Image": "https://a.fsdn.com/allura/s/embarcadero-dev-cpp/icon", + "URL": "https://sourceforge.net/software/product/Embarcadero-Dev-Cpp/" + }, { + "Name": "RedPanda C++", + "Image": "https://a.fsdn.com/allura/p/redpanda-cpp/icon", + "URL": "https://sourceforge.net/projects/redpanda-cpp/" + }, { + "Name": "CP Editor", + "Image": "https://a.fsdn.com/allura/mirror/cp-editor/icon?c35437565079e4135a985ba557ef2fdbe97de6bafb27aceafd76bc54490c26e3?&w=90", + "URL": "https://cpeditor.org/zh/download/" + }, { + "Name": "CLion", + "Image": "https://resources.jetbrains.com/storage/products/company/brand/logos/CLion_icon.png", + "URL": "https://www.jetbrains.com/clion/download" + }, { + "Name": "CP Editor", + "Image": "https://a.fsdn.com/allura/mirror/cp-editor/icon", + "URL": "https://sourceforge.net/projects/cp-editor.mirror/" + }, { + "Name": "Code::Blocks", + "Image": "https://a.fsdn.com/allura/p/codeblocks/icon", + "URL": "https://sourceforge.net/projects/codeblocks/" + }, { + "Name": "Visual Studio Code", + "Image": "https://code.visualstudio.com/favicon.ico", + "URL": "https://code.visualstudio.com/Download" + }, { + "Name": "Lazarus", + "Image": "https://a.fsdn.com/allura/p/lazarus/icon", + "URL": "https://sourceforge.net/projects/lazarus/" + }, { + "Name": "Geany", + "Image": "https://www.geany.org/static/img/geany.svg", + "URL": "https://www.geany.org/download/releases/" + }, { + "Name": "NOI Linux", + "Image": "https://www.noi.cn/upload/resources/image/2021/07/16/163780.jpg", + "URL": "https://www.noi.cn/gynoi/jsgz/2021-07-16/732450.shtml" + }, { + "Name": "VirtualBox", + "Image": "https://www.virtualbox.org/graphics/vbox_logo2_gradient.png", + "URL": "https://www.virtualbox.org/wiki/Downloads" + }, { + "Name": "MinGW", + "Image": "https://www.mingw-w64.org/logo.svg", + "URL": "https://sourceforge.net/projects/mingw/" + }]; + for (let i = 0; i < Softwares.length; i++) { + SoftwareList.innerHTML += "
  • " + "" + "
    " + "
    " + "\"点击下载\"" + "
    " + "
    " + Softwares[i].Name + "
    " + "
    " + "
    " + "
  • "; + } + } + } else if (location.pathname == "/problemstatus.php") { + document.querySelector("body > div > div.mt-3 > center").insertBefore(document.querySelector("#statics"), document.querySelector("body > div > div.mt-3 > center > table")); + document.querySelector("body > div > div.mt-3 > center").insertBefore(document.querySelector("#problemstatus"), document.querySelector("body > div > div.mt-3 > center > table")); + + document.querySelector("body > div > div.mt-3 > center > table:nth-child(3)").remove(); + let Temp = document.querySelector("#statics").rows; + for (let i = 0; i < Temp.length; i++) { + Temp[i].removeAttribute("class"); + } + + document.querySelector("#problemstatus > thead > tr").innerHTML = document.querySelector("#problemstatus > thead > tr").innerHTML.replaceAll("td", "th"); + document.querySelector("#problemstatus > thead > tr > th:nth-child(2)").innerText = "运行编号"; + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + Temp = document.querySelector("#problemstatus > thead > tr").children; + for (let i = 0; i < Temp.length; i++) { + Temp[i].removeAttribute("class"); + } + Temp = document.querySelector("#problemstatus > tbody").children; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].children[5].children[0] != null) { + Temp[i].children[1].innerHTML = `${escapeHTML(Temp[i].children[1].innerText.trim())}`; + } + GetUsernameHTML(Temp[i].children[2], Temp[i].children[2].innerText); + Temp[i].children[3].remove(); + Temp[i].children[3].remove(); + Temp[i].children[3].remove(); + Temp[i].children[3].remove(); + } + + + let CurrentPage = parseInt(SearchParams.get("page") || 0); + let PID = Number(SearchParams.get("id")); + document.title = "问题 " + PID + " 状态"; + let Pagination = ``; + document.querySelector("body > div > div.mt-3 > center").innerHTML += Pagination; + } else if (location.pathname == "/problem_solution.php") { + if (UtilityEnabled("RemoveUseless")) { + document.querySelector("h2.lang_en").remove(); //fixes #332 + } + if (UtilityEnabled("CopyMD")) { + await fetch(location.href).then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let CopyMDButton = document.createElement("button"); + CopyMDButton.className = "btn btn-sm btn-outline-secondary copy-btn"; + CopyMDButton.innerText = "复制"; + CopyMDButton.style.marginLeft = "10px"; + CopyMDButton.type = "button"; + document.querySelector("body > div > div.mt-3 > center > h2").appendChild(CopyMDButton); + CopyMDButton.addEventListener("click", () => { + GM_setClipboard(ParsedDocument.querySelector("body > div > div > div").innerText.trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); + CopyMDButton.innerText = "复制成功"; + setTimeout(() => { + CopyMDButton.innerText = "复制"; + }, 1000); + }); + }); + } + let Temp = document.getElementsByClassName("prettyprint"); + for (let i = 0; i < Temp.length; i++) { + let Code = Temp[i].innerText; + Temp[i].outerHTML = ``; + Temp[i].value = Code; + } + for (let i = 0; i < Temp.length; i++) { + CodeMirror.fromTextArea(Temp[i], { + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + } + } else if (location.pathname == "/open_contest.php") { + let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; + let NewsData = []; + for (let i = 0; i < Temp.length; i += 2) { + let Title = Temp[i].children[0].innerText; + let Time = 0; + if (Temp[i].children[1] != null) { + Time = Temp[i].children[1].innerText; + } + let Body = Temp[i + 1].innerHTML; + NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; + for (let i = 0; i < NewsData.length; i++) { + let NewsRow = document.createElement("div"); + NewsRow.className = "cnt-row"; + let NewsRowHead = document.createElement("div"); + NewsRowHead.className = "cnt-row-head title"; + NewsRowHead.innerText = NewsData[i].Title; + if (NewsData[i].Time.getTime() != 0) { + NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; + } + NewsRow.appendChild(NewsRowHead); + let NewsRowBody = document.createElement("div"); + NewsRowBody.className = "cnt-row-body"; + NewsRowBody.innerHTML = NewsData[i].Body; + NewsRow.appendChild(NewsRowBody); + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); + } + let MyContestData = document.querySelector("body > div > div.mt-3 > div > div.col-md-4 > div:nth-child(2)").innerHTML; + let CountDownData = document.querySelector("#countdown_list").innerHTML; + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
    +
    我的月赛
    +
    ${MyContestData}
    +
    +
    +
    倒计时
    +
    ${CountDownData}
    +
    `; + } else if (location.pathname == "/showsource.php") { + let Code = ""; + if (SearchParams.get("ByUserScript") == null) { + document.title = "查看代码: " + SearchParams.get("id"); + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("id")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + Code = Response.replace("\n\n", ""); + }); + } else { + document.title = "查看标程: " + SearchParams.get("pid"); + if (localStorage.getItem("UserScript-LastUploadedStdTime") === undefined || new Date().getTime() - localStorage.getItem("UserScript-LastUploadedStdTime") > 1000 * 60 * 60 * 24 * 30) { + location.href = "https://www.xmoj.tech/userinfo.php?ByUserScript=1"; + } + await new Promise((Resolve) => { + RequestAPI("GetStd", { + "ProblemID": Number(SearchParams.get("pid")) + }, (Response) => { + if (Response.Success) { + Code = Response.Data.StdCode; + } else { + Code = Response.Message; + } + Resolve(); + }); + }); + } + document.querySelector("body > div > div.mt-3").innerHTML = ``; + CodeMirror.fromTextArea(document.querySelector("body > div > div.mt-3 > textarea"), { + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + } else if (location.pathname == "/ceinfo.php") { + await fetch(location.href) + .then((Result) => { + return Result.text(); + }).then((Result) => { + let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); + document.querySelector("body > div > div.mt-3").innerHTML = ""; + let CodeElement = document.createElement("div"); + CodeElement.className = "mb-3"; + document.querySelector("body > div > div.mt-3").appendChild(CodeElement); + CodeMirror(CodeElement, { + value: ParsedDocument.getElementById("errtxt").innerHTML.replaceAll("<", "<").replaceAll(">", ">"), + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + }); + } else if (location.pathname == "/problem_std.php") { + await fetch("https://www.xmoj.tech/problem_std.php?cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.getElementsByTagName("pre"); + document.querySelector("body > div > div.mt-3").innerHTML = ""; + for (let i = 0; i < Temp.length; i++) { + let CodeElement = document.createElement("div"); + CodeElement.className = "mb-3"; + document.querySelector("body > div > div.mt-3").appendChild(CodeElement); + CodeMirror(CodeElement, { + value: Temp[i].innerText, + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + } + }); + } else if (location.pathname == "/mail.php") { + if (SearchParams.get("to_user") == null) { + document.querySelector("body > div > div.mt-3").innerHTML = `
    +
    + + +
    +
    + +
    +
    + + + + + + + + + +
    接收者最新消息最后联系时间
    + `; + let RefreshMessageList = (Silent = true) => { + if (!Silent) { + ReceiveTable.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + ReceiveTable.children[1].appendChild(Row); + for (let j = 0; j < 3; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("GetMailList", {}, async (ResponseData) => { + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + let Data = ResponseData.Data.MailList; + ReceiveTable.children[1].innerHTML = ""; + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("tr"); + ReceiveTable.children[1].appendChild(Row); + let UsernameCell = document.createElement("td"); + Row.appendChild(UsernameCell); + let UsernameSpan = document.createElement("span"); + UsernameCell.appendChild(UsernameSpan); + GetUsernameHTML(UsernameSpan, Data[i].OtherUser, false, "https://www.xmoj.tech/mail.php?to_user="); + if (Data[i].UnreadCount != 0) { + let UnreadCountSpan = document.createElement("span"); + UsernameCell.appendChild(UnreadCountSpan); + UnreadCountSpan.className = "ms-1 badge text-bg-danger"; + UnreadCountSpan.innerText = Data[i].UnreadCount; + } + let LastsMessageCell = document.createElement("td"); + Row.appendChild(LastsMessageCell); + LastsMessageCell.innerText = replaceMarkdownImages(Data[i].LastsMessage, '[image]'); + let SendTimeCell = document.createElement("td"); + Row.appendChild(SendTimeCell); + SendTimeCell.innerHTML = GetRelativeTime(Data[i].SendTime); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }; + Username.addEventListener("input", () => { + Username.classList.remove("is-invalid"); + }); + AddUser.addEventListener("click", () => { + let UsernameData = Username.value; + if (UsernameData == "") { + Username.classList.add("is-invalid"); + return; + } + AddUser.children[0].style.display = ""; + AddUser.disabled = true; + RequestAPI("SendMail", { + "ToUser": String(UsernameData), + "Content": String("您好,我是" + CurrentUsername) + }, (ResponseData) => { + AddUser.children[0].style.display = "none"; + AddUser.disabled = false; + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + RefreshMessageList(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + RefreshMessageList(false); + addEventListener("focus", RefreshMessageList); + } else { + document.querySelector("body > div > div.mt-3").innerHTML = `
    +
    +
    + +
    +
    + + +
    +
    + + + + + + + + + + + + +
    发送者内容发送时间阅读状态
    `; + GetUsernameHTML(ToUser, SearchParams.get("to_user")); + let RefreshMessage = (Silent = true) => { + if (!Silent) { + MessageTable.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + MessageTable.children[1].appendChild(Row); + for (let j = 0; j < 4; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("ReadUserMailMention", { + "UserID": String(SearchParams.get("to_user")) + }); + RequestAPI("GetMail", { + "OtherUser": String(SearchParams.get("to_user")) + }, async (ResponseData) => { + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + let Data = ResponseData.Data.Mail; + MessageTable.children[1].innerHTML = ""; + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("tr"); + MessageTable.children[1].appendChild(Row); + if (!Data[i].IsRead && Data[i].FromUser != CurrentUsername) { + Row.className = "table-info"; + } + let UsernameCell = document.createElement("td"); + Row.appendChild(UsernameCell); + GetUsernameHTML(UsernameCell, Data[i].FromUser); + let ContentCell = document.createElement("td"); + let ContentDiv = document.createElement("div"); + ContentDiv.style.display = "flex"; + ContentDiv.style.maxWidth = window.innerWidth - 300 + "px"; + ContentDiv.style.maxHeight = "500px"; + ContentDiv.style.overflowX = "auto"; + ContentDiv.style.overflowY = "auto"; + ContentDiv.innerHTML = PurifyHTML(marked.parse(Data[i].Content)); + let mediaElements = ContentDiv.querySelectorAll('img, video'); + for (let media of mediaElements) { + media.style.objectFit = 'contain'; + media.style.maxWidth = '100%'; + media.style.maxHeight = '100%'; + } + ContentCell.appendChild(ContentDiv); + Row.appendChild(ContentCell); + let SendTimeCell = document.createElement("td"); + Row.appendChild(SendTimeCell); + SendTimeCell.innerHTML = GetRelativeTime(Data[i].SendTime); + let IsReadCell = document.createElement("td"); + Row.appendChild(IsReadCell); + IsReadCell.innerHTML = (Data[i].IsRead ? "已读" : "未读"); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }; + Content.addEventListener("input", () => { + Content.classList.remove("is-invalid"); + }); + Content.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = Content.value.substring(0, Content.selectionStart); + let After = Content.value.substring(Content.selectionEnd, Content.value.length); + const UploadMessage = "![正在上传图片...]()"; + Content.value = Before + UploadMessage + After; + Content.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + Content.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + Content.dispatchEvent(new Event("input")); + } else { + Content.value = Before + `![上传失败!` + ResponseData.Message + `]()` + After; + Content.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + Content.addEventListener("keydown", (Event) => { + if (Event.keyCode == 13) { + Send.click(); + } + }); + Send.addEventListener("click", () => { + if (Content.value == "") { + Content.classList.add("is-invalid"); + return; + } + Send.disabled = true; + Send.children[0].style.display = ""; + let ContentData = Content.value; + RequestAPI("SendMail", { + "ToUser": String(SearchParams.get("to_user")), "Content": String(ContentData) + }, (ResponseData) => { + Send.disabled = false; + Send.children[0].style.display = "none"; + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + Content.value = ""; + RefreshMessage(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + RefreshMessage(false); + addEventListener("focus", RefreshMessage); + } + } else if (location.pathname.indexOf("/discuss3") != -1) { + if (UtilityEnabled("Discussion")) { + Discussion.classList.add("active"); + if (location.pathname == "/discuss3/discuss.php") { + document.title = "讨论列表"; + let ProblemID = parseInt(SearchParams.get("pid")); + let BoardID = parseInt(SearchParams.get("bid")); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    讨论列表${(isNaN(ProblemID) ? "" : ` - 题目` + ProblemID)}

    + + +
    + + + + + + + + + + + + + + + +
    编号标题作者题目编号发布时间回复数最后回复
    `; + NewPost.addEventListener("click", () => { + if (!isNaN(ProblemID)) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?pid=" + ProblemID; + } else if (SearchParams.get("bid") != null) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?bid=" + SearchParams.get("bid"); + } else { + location.href = "https://www.xmoj.tech/discuss3/newpost.php"; + } + }); + const RefreshPostList = (Silent = true) => { + if (!Silent) { + PostList.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + for (let j = 0; j < 7; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("GetPosts", { + "ProblemID": Number(ProblemID || 0), + "Page": Number(Page), + "BoardID": Number(SearchParams.get("bid") || -1) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + ErrorElement.style.display = "none"; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + } + let Posts = ResponseData.Data.Posts; + PostList.children[1].innerHTML = ""; + if (Posts.length == 0) { + PostList.children[1].innerHTML = `暂无数据`; + } + for (let i = 0; i < Posts.length; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + let IDCell = document.createElement("td"); + Row.appendChild(IDCell); + IDCell.innerText = Posts[i].PostID + " " + Posts[i].BoardName; + let TitleCell = document.createElement("td"); + Row.appendChild(TitleCell); + let TitleLink = document.createElement("a"); + TitleCell.appendChild(TitleLink); + TitleLink.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + Posts[i].PostID; + if (Posts[i].Lock.Locked) { + TitleLink.classList.add("link-secondary"); + TitleLink.innerHTML = "🔒 "; + } + TitleLink.innerHTML += Posts[i].Title; + let AuthorCell = document.createElement("td"); + Row.appendChild(AuthorCell); + GetUsernameHTML(AuthorCell, Posts[i].UserID); + let ProblemIDCell = document.createElement("td"); + Row.appendChild(ProblemIDCell); + if (Posts[i].ProblemID != 0) { + let ProblemIDLink = document.createElement("a"); + ProblemIDCell.appendChild(ProblemIDLink); + ProblemIDLink.href = "https://www.xmoj.tech/problem.php?id=" + Posts[i].ProblemID; + ProblemIDLink.innerText = Posts[i].ProblemID; + } + let PostTimeCell = document.createElement("td"); + Row.appendChild(PostTimeCell); + PostTimeCell.innerHTML = GetRelativeTime(Posts[i].PostTime); + let ReplyCountCell = document.createElement("td"); + Row.appendChild(ReplyCountCell); + ReplyCountCell.innerText = Posts[i].ReplyCount; + let LastReplyTimeCell = document.createElement("td"); + Row.appendChild(LastReplyTimeCell); + LastReplyTimeCell.innerHTML = GetRelativeTime(Posts[i].LastReplyTime); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }; + RefreshPostList(false); + addEventListener("focus", RefreshPostList); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php"; + LinkElement.classList.add("me-2"); + LinkElement.innerText = "全部"; + GotoBoard.appendChild(LinkElement); + for (let i = 0; i < ResponseData.Data.Boards.length; i++) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php?bid=" + ResponseData.Data.Boards[i].BoardID; + LinkElement.classList.add("me-2"); + LinkElement.innerText = ResponseData.Data.Boards[i].BoardName; + GotoBoard.appendChild(LinkElement); + } + } + }); + } else if (location.pathname == "/discuss3/newpost.php") { + let ProblemID = parseInt(SearchParams.get("pid")); + document.querySelector("body > div > div").innerHTML = `

    发布新讨论` + (!isNaN(ProblemID) ? ` - 题目` + ProblemID : ``) + `

    +
    + +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + ContentElement.classList.remove("is-invalid"); + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + TitleElement.addEventListener("input", () => { + TitleElement.classList.remove("is-invalid"); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + let Title = TitleElement.value; + let Content = ContentElement.value; + let ProblemID = parseInt(SearchParams.get("pid")); + if (Title === "") { + TitleElement.classList.add("is-invalid"); + return; + } + if (Content === "") { + ContentElement.classList.add("is-invalid"); + return; + } + if (document.querySelector("#Board input:checked") === null) { + ErrorElement.innerText = "请选择要发布的板块"; + ErrorElement.style.display = "block"; + return; + } + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewPost", { + "Title": String(Title), + "Content": String(Content), + "ProblemID": Number(isNaN(ProblemID) ? 0 : ProblemID), + "CaptchaSecretKey": String(CaptchaSecretKey), + "BoardID": Number(document.querySelector("#Board input:checked").value) + }, (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ResponseData.Data.PostID; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let Data = ResponseData.Data.Boards; + for (let i = 0; i < Data.length; i++) { + let RadioElement = document.createElement("div"); + RadioElement.className = "col-auto form-check form-check-inline"; + let RadioInput = document.createElement("input"); + RadioInput.className = "form-check-input"; + RadioInput.type = "radio"; + RadioInput.name = "Board"; + RadioInput.id = "Board" + Data[i].BoardID; + RadioInput.value = Data[i].BoardID; + RadioElement.appendChild(RadioInput); + if (SearchParams.get("bid") !== null && SearchParams.get("bid") == Data[i].BoardID) { + RadioInput.checked = true; + } + if (!isNaN(ProblemID)) { + RadioInput.disabled = true; + } + if (Data[i].BoardID == 4) { + if (!isNaN(ProblemID)) RadioInput.checked = true; + RadioInput.disabled = true; + } + let RadioLabel = document.createElement("label"); + RadioLabel.className = "form-check-label"; + RadioLabel.htmlFor = "Board" + Data[i].BoardID; + RadioLabel.innerText = Data[i].BoardName; + RadioElement.appendChild(RadioLabel); + Board.appendChild(RadioElement); + } + } + }); + } else if (location.pathname == "/discuss3/thread.php") { + if (SearchParams.get("tid") == null) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + let ThreadID = SearchParams.get("tid"); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    +
    + 作者:
    + 发布时间: + 板块: + + + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + let RefreshReply = (Silent = true) => { + if (!Silent) { + PostTitle.innerHTML = ``; + PostAuthor.innerHTML = ``; + PostTime.innerHTML = ``; + PostBoard.innerHTML = ``; + PostReplies.innerHTML = ""; + for (let i = 0; i < 10; i++) { + PostReplies.innerHTML += `
    +
    +
    + + +
    +
    + + + +
    +
    `; + } + } + RequestAPI("GetPost", { + "PostID": Number(ThreadID), "Page": Number(Page) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + let OldScrollTop = document.documentElement.scrollTop; + let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { + Delete.style.display = ""; + } + } + PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); + document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; + PostAuthor.innerHTML = ""; + GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); + PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); + PostBoard.innerHTML = ResponseData.Data.BoardName; + let Replies = ResponseData.Data.Reply; + PostReplies.innerHTML = ""; + for (let i = 0; i < Replies.length; i++) { + let CardElement = document.createElement("div"); + PostReplies.appendChild(CardElement); + CardElement.className = "card mb-3"; + let CardBodyElement = document.createElement("div"); + CardElement.appendChild(CardBodyElement); + CardBodyElement.className = "card-body row"; + let CardBodyRowElement = document.createElement("div"); + CardBodyElement.appendChild(CardBodyRowElement); + CardBodyRowElement.className = "row mb-3"; + let AuthorElement = document.createElement("span"); + CardBodyRowElement.appendChild(AuthorElement); + AuthorElement.className = "col-4 text-muted"; + let AuthorSpanElement = document.createElement("span"); + AuthorElement.appendChild(AuthorSpanElement); + AuthorSpanElement.innerText = "作者:"; + let AuthorUsernameElement = document.createElement("span"); + AuthorElement.appendChild(AuthorUsernameElement); + GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); + let SendTimeElement = document.createElement("span"); + CardBodyRowElement.appendChild(SendTimeElement); + SendTimeElement.className = "col-4 text-muted"; + SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); + + let OKButton; + if (!LockButtons) { + let ButtonsElement = document.createElement("span"); + CardBodyRowElement.appendChild(ButtonsElement); + ButtonsElement.className = "col-4"; + let ReplyButton = document.createElement("button"); + ButtonsElement.appendChild(ReplyButton); + ReplyButton.type = "button"; + ReplyButton.className = "btn btn-sm btn-info"; + ReplyButton.innerText = "回复"; + ReplyButton.addEventListener("click", () => { + let Content = Replies[i].Content; + Content = Content.split("\n").map((Line) => { + // Count the number of '>' characters at the beginning of the line + let nestingLevel = 0; + while (Line.startsWith(">")) { + nestingLevel++; + Line = Line.substring(1).trim(); + } + // If the line is nested more than 2 levels deep, skip it + if (nestingLevel > 2) { + return null; + } + // Reconstruct the line with the appropriate number of '>' characters + return "> ".repeat(nestingLevel + 1) + Line; + }).filter(Line => Line !== null) // Remove null entries + .join("\n"); + ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; + ContentElement.focus(); + }); + let DeleteButton = document.createElement("button"); + ButtonsElement.appendChild(DeleteButton); + DeleteButton.type = "button"; + DeleteButton.className = "btn btn-sm btn-danger ms-1"; + DeleteButton.innerText = "删除"; + DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + DeleteButton.addEventListener("click", () => { + DeleteButton.disabled = true; + DeleteButton.lastChild.style.display = ""; + RequestAPI("DeleteReply", { + "ReplyID": Number(Replies[i].ReplyID) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + DeleteButton.disabled = false; + DeleteButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let DeleteSpin = document.createElement("div"); + DeleteButton.appendChild(DeleteSpin); + DeleteSpin.className = "spinner-border spinner-border-sm"; + DeleteSpin.role = "status"; + DeleteSpin.style.display = "none"; + OKButton = document.createElement("button"); + ButtonsElement.appendChild(OKButton); + OKButton.type = "button"; + OKButton.style.display = "none"; + OKButton.className = "btn btn-sm btn-success ms-1"; + OKButton.innerText = "确认"; + let OKSpin = document.createElement("div"); + OKButton.appendChild(OKSpin); + OKSpin.className = "spinner-border spinner-border-sm"; + OKSpin.role = "status"; + OKSpin.style.display = "none"; + OKButton.addEventListener("click", () => { + OKButton.disabled = true; + OKButton.lastChild.style.display = ""; + RequestAPI("EditReply", { + ReplyID: Number(Replies[i].ReplyID), + Content: String(ContentEditor.value) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + OKButton.disabled = false; + OKButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let CancelButton = document.createElement("button"); + ButtonsElement.appendChild(CancelButton); + CancelButton.type = "button"; + CancelButton.style.display = "none"; + CancelButton.className = "btn btn-sm btn-secondary ms-1"; + CancelButton.innerText = "取消"; + CancelButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = ""; + CardBodyElement.children[3].style.display = "none"; + EditButton.style.display = ""; + OKButton.style.display = "none"; + CancelButton.style.display = "none"; + }); + let EditButton = document.createElement("button"); + ButtonsElement.appendChild(EditButton); + EditButton.type = "button"; + EditButton.className = "btn btn-sm btn-warning ms-1"; + EditButton.innerText = "编辑"; + EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + EditButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = "none"; + CardBodyElement.children[3].style.display = ""; + EditButton.style.display = "none"; + OKButton.style.display = ""; + CancelButton.style.display = ""; + }); + } + + let CardBodyHRElement = document.createElement("hr"); + CardBodyElement.appendChild(CardBodyHRElement); + + let ReplyContentElement = document.createElement("div"); + CardBodyElement.appendChild(ReplyContentElement); + ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + if (Replies[i].EditTime != null) { + if (Replies[i].EditPerson == Replies[i].UserID) { + ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } else { + ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } + } + let ContentEditElement = document.createElement("div"); + CardBodyElement.appendChild(ContentEditElement); + ContentEditElement.classList.add("input-group"); + ContentEditElement.style.display = "none"; + let ContentEditor = document.createElement("textarea"); + ContentEditElement.appendChild(ContentEditor); + ContentEditor.className = "form-control col-6"; + ContentEditor.rows = 3; + ContentEditor.value = Replies[i].Content; + if (ContentEditor.value.indexOf("
    ") != -1) { + ContentEditor.value = ContentEditor.value.substring(0, ContentEditor.value.indexOf("
    ")); + } + ContentEditor.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + OKButton.click(); + } + }); + let PreviewTab = document.createElement("div"); + ContentEditElement.appendChild(PreviewTab); + PreviewTab.className = "form-control col-6"; + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + ContentEditor.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + RenderMathJax(); + }); + ContentEditor.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentEditor.value.substring(0, ContentEditor.selectionStart); + let After = ContentEditor.value.substring(ContentEditor.selectionEnd, ContentEditor.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentEditor.value = Before + UploadMessage + After; + ContentEditor.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentEditor.dispatchEvent(new Event("input")); + } else { + ContentEditor.value = Before + `![上传失败!]()` + After; + ContentEditor.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + } + + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + + let CodeElements = document.querySelectorAll("#PostReplies > div > div > div:nth-child(3) > pre > code"); + for (let i = 0; i < CodeElements.length; i++) { + let ModeName = "text/x-c++src"; + if (CodeElements[i].className == "language-c") { + ModeName = "text/x-csrc"; + } else if (CodeElements[i].className == "language-cpp") { + ModeName = "text/x-c++src"; + } + CodeMirror(CodeElements[i].parentElement, { + value: CodeElements[i].innerText, + mode: ModeName, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + lineNumbers: true, + readOnly: true + }).setSize("100%", "auto"); + CodeElements[i].remove(); + } + + if (LockButtons) { + let LockElement = ContentElement.parentElement.parentElement; + LockElement.innerHTML = "讨论已于 " + await GetRelativeTime(ResponseData.Data.Lock.LockTime) + " 被 "; + let LockUsernameSpan = document.createElement("span"); + LockElement.appendChild(LockUsernameSpan); + GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); + LockElement.innerHTML += " 锁定"; + LockElement.classList.add("mb-5"); + } + + if (IsAdmin) { + ToggleLock.style.display = "inline-block"; + ToggleLockButton.checked = ResponseData.Data.Lock.Locked; + ToggleLockButton.onclick = () => { + ToggleLockButton.disabled = true; + ErrorElement.style.display = "none"; + RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { + "PostID": Number(ThreadID) + }, (LockResponseData) => { + ToggleLockButton.disabled = false; + if (LockResponseData.Success) { + RefreshReply(); + } else { + ErrorElement.style.display = ""; + ErrorElement.innerText = "错误:" + LockResponseData.Message; + ToggleLockButton.checked = !ToggleLockButton.checked; + } + }); + }; + } + + Style.innerHTML += "img {"; + Style.innerHTML += " width: 50%;"; + Style.innerHTML += "}"; + + RenderMathJax(); + + if (Silent) { + scrollTo({ + top: OldScrollTop, behavior: "instant" + }); + } + } else { + PostTitle.innerText = "错误:" + ResponseData.Message; + } + }); + }; + Delete.addEventListener("click", () => { + Delete.disabled = true; + Delete.children[0].style.display = "inline-block"; + RequestAPI("DeletePost", { + "PostID": Number(SearchParams.get("tid")) + }, (ResponseData) => { + Delete.disabled = false; + Delete.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewReply", { + "PostID": Number(SearchParams.get("tid")), + "Content": String(ContentElement.value), + "CaptchaSecretKey": String(CaptchaSecretKey) + }, async (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + RefreshReply(); + ContentElement.value = ""; + PreviewTab.innerHTML = ""; + while (PostReplies.innerHTML.indexOf("placeholder") != -1) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + ContentElement.focus(); + ContentElement.scrollIntoView(); + turnstile.reset(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RefreshReply(false); + addEventListener("focus", RefreshReply); + } + } + } + } + } + } + } 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."); + } + } + } + + /** + * Menu command registrations + */ + + /** + * Register Greasemonkey menu commands + */ + function registerMenuCommands() { + GM_registerMenuCommand("清除缓存", () => { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + location.reload(); + }); + + GM_registerMenuCommand("重置数据", () => { + if (confirm("确定要重置数据吗?")) { + localStorage.clear(); + location.reload(); + } + }); + } + + /** + * Main entry point for XMOJ Script + * This file imports all utilities and features, then initializes the application + */ + + + // Make utilities globally available (for compatibility with inline code) + window.escapeHTML = escapeHTML$1; + window.PurifyHTML = PurifyHTML$1; + window.SmartAlert = SmartAlert; + window.GetRelativeTime = GetRelativeTime; + window.SecondsToString = SecondsToString; + window.StringToSeconds = StringToSeconds; + window.TimeToStringTime = TimeToStringTime$1; + window.SizeToStringSize = SizeToStringSize$1; + window.CodeSizeToStringSize = CodeSizeToStringSize$1; + window.compareVersions = compareVersions; + window.RequestAPI = RequestAPI; + window.storeCredential = storeCredential$1; + window.getCredential = getCredential$1; + window.clearCredential = clearCredential; + window.RenderMathJax = RenderMathJax$1; + window.TidyTable = TidyTable; + window.GetUserInfo = GetUserInfo$1; + window.GetUserBadge = GetUserBadge$1; + window.GetUsernameHTML = GetUsernameHTML$1; + window.UtilityEnabled = UtilityEnabled; + window.AdminUserList = AdminUserList; + + // Register menu commands + registerMenuCommands(); + + // Initialize theme + initTheme(); + + // Start the main application + main(); + +})(); diff --git a/package.json b/package.json index b0598518..5a1bb28c 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,11 @@ "name": "xmoj-script", "version": "2.5.0", "description": "an improvement script for xmoj.tech", - "main": "AddonScript.js", + "type": "module", + "main": "dist/XMOJ.user.js", "scripts": { + "build": "rollup -c", + "watch": "rollup -c -w", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -18,5 +21,10 @@ "bugs": { "url": "https://github.com/XMOJ-Script-dev/XMOJ-Script/issues" }, - "homepage": "https://github.com/XMOJ-Script-dev/XMOJ-Script#readme" + "homepage": "https://github.com/XMOJ-Script-dev/XMOJ-Script#readme", + "devDependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "rollup": "^4.53.1" + } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..713901cb --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,21 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import { readFileSync } from 'fs'; + +// Read the original userscript header +const originalScript = readFileSync('./XMOJ.user.js', 'utf-8'); +const headerMatch = originalScript.match(/\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/); +const header = headerMatch ? headerMatch[0] : ''; + +export default { + input: 'src/main.js', + output: { + file: 'dist/XMOJ.user.js', + format: 'iife', + banner: header + '\n', + }, + plugins: [ + nodeResolve(), + commonjs(), + ], +}; diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js new file mode 100644 index 00000000..909ac434 --- /dev/null +++ b/src/core/bootstrap.js @@ -0,0 +1,4396 @@ +/** + * Bootstrap initialization and main application logic + */ + +import { UtilityEnabled } from './config.js'; +import { AdminUserList } from './constants.js'; +import { SmartAlert } from '../utils/alerts.js'; +import { RequestAPI } from '../utils/api.js'; +import { GetRelativeTime } from '../utils/time.js'; +import { TidyTable } from '../utils/table.js'; +import { compareVersions } from '../utils/version.js'; +import { clearCredential } from '../utils/credentials.js'; + +// Time difference for server synchronization (initialized to 0) +let diff = 0; + +// Theme management +const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); + +const applyTheme = (theme) => { + document.querySelector("html").setAttribute("data-bs-theme", theme); + localStorage.setItem("UserScript-Setting-DarkMode", theme === "dark" ? "true" : "false"); +}; + +const applySystemTheme = (e) => applyTheme(e.matches ? "dark" : "light"); + +export let initTheme = () => { + const saved = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + if (saved === "auto") { + applyTheme(prefersDark.matches ? "dark" : "light"); + prefersDark.addEventListener("change", applySystemTheme); + } else { + applyTheme(saved); + prefersDark.removeEventListener("change", applySystemTheme); + } +}; + +// NavbarStyler class for styling the navigation bar +export class NavbarStyler { + constructor() { + try { + this.navbar = document.querySelector('.navbar.navbar-expand-lg.bg-body-tertiary'); + if (this.navbar && UtilityEnabled("NewTopBar")) { + this.init(); + } + } 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."); + } + } + } + + init() { + try { + this.applyStyles(); + this.createOverlay(); + this.createSpacer(); + window.addEventListener('resize', () => this.updateBlurOverlay()); + this.updateBlurOverlay(); + } 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."); + } + } + } + + applyStyles() { + try { + let n = this.navbar; + n.classList.add('fixed-top', 'container', 'ml-auto'); + Object.assign(n.style, { + position: 'fixed', + borderRadius: '28px', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', + margin: '16px auto', + backgroundColor: 'rgba(255, 255, 255, 0)', + opacity: '0.75', + zIndex: '1000' + }); + } 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."); + } + } + } + + createOverlay() { + try { + if (!document.getElementById('blur-overlay')) { + let overlay = document.createElement('div'); + overlay.id = 'blur-overlay'; + document.body.appendChild(overlay); + + let style = document.createElement('style'); + style.textContent = ` + #blur-overlay { + position: fixed; + backdrop-filter: blur(4px); + z-index: 999; + pointer-events: none; + border-radius: 28px; + } + `; + document.head.appendChild(style); + } + } 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."); + } + } + } + + updateBlurOverlay() { + try { + let overlay = document.getElementById('blur-overlay'); + let n = this.navbar; + Object.assign(overlay.style, { + top: `${n.offsetTop}px`, + left: `${n.offsetLeft}px`, + width: `${n.offsetWidth}px`, + height: `${n.offsetHeight}px` + }); + } 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."); + } + } + } + + createSpacer() { + try { + let spacer = document.getElementById('navbar-spacer'); + let newHeight = this.navbar.offsetHeight + 24; + if (!spacer) { + spacer = document.createElement('div'); + spacer.id = 'navbar-spacer'; + spacer.style.height = `${newHeight}px`; + spacer.style.width = '100%'; + document.body.insertBefore(spacer, document.body.firstChild); + } else { + let currentHeight = parseInt(spacer.style.height, 10); + if (currentHeight !== newHeight) { + document.body.removeChild(spacer); + spacer = document.createElement('div'); + spacer.id = 'navbar-spacer'; + spacer.style.height = `${newHeight}px`; + spacer.style.width = '100%'; + document.body.insertBefore(spacer, document.body.firstChild); + } + } + } 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."); + } + } + } +} + +// Utility function to replace markdown images +export function replaceMarkdownImages(text, string) { + return text.replace(/!\[.*?\]\(.*?\)/g, string); +} + +// Main initialization function +export async function main() { + try { + if (location.href.startsWith('http://')) { + //use https + location.href = location.href.replace('http://', 'https://'); + } + if (location.host != "www.xmoj.tech") { + location.host = "www.xmoj.tech"; + } else { + if (location.href === 'https://www.xmoj.tech/open_contest_sign_up.php') { + return; + } + + // Get current username + let CurrentUsername = ""; + if (document.querySelector("#profile") !== null) { + CurrentUsername = document.querySelector("#profile").innerText; + CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); + } + + // Determine server URL + let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me"); + + document.body.classList.add("placeholder-glow"); + if (document.querySelector("#navbar") != null) { + if (document.querySelector("body > div > div.jumbotron") != null) { + document.querySelector("body > div > div.jumbotron").className = "mt-3"; + } + + if (UtilityEnabled("AutoLogin") && document.querySelector("#profile") != null && document.querySelector("#profile").innerHTML == "登录" && location.pathname != "/login.php" && location.pathname != "/loginpage.php" && location.pathname != "/lostpassword.php") { + localStorage.setItem("UserScript-LastPage", location.pathname + location.search); + location.href = "https://www.xmoj.tech/loginpage.php"; + } + + let Discussion = null; + if (UtilityEnabled("Discussion")) { + Discussion = document.createElement("li"); + document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); + Discussion.innerHTML = "讨论"; + } + if (UtilityEnabled("Translate")) { + document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a").innerText = "题库"; + } + //send analytics + RequestAPI("SendData", {}); + if (UtilityEnabled("ReplaceLinks")) { + document.body.innerHTML = String(document.body.innerHTML).replaceAll(/\[([^<]*)<\/a>\]/g, ""); + } + if (UtilityEnabled("ReplaceXM")) { + document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("海上", "上海"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小红", "徐师娘"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小粉", "彩虹"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("提交上节课的代码", "自动提交当年代码"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("高老师们", "我们"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("自高老师", "自我"); + document.title = String(document.title).replaceAll("小明", "高老师"); + } + + if (UtilityEnabled("NewBootstrap")) { + let Temp = document.querySelectorAll("link"); + for (var i = 0; i < Temp.length; i++) { + if (Temp[i].href.indexOf("bootstrap.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("white.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("semantic.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("bootstrap-theme.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("problem.css") != -1) { + Temp[i].remove(); + } + } + if (UtilityEnabled("DarkMode")) { + document.querySelector("html").setAttribute("data-bs-theme", "dark"); + } else { + document.querySelector("html").setAttribute("data-bs-theme", "light"); + } + var resources = [{ + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/darcula.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/merge/merge.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css', + rel: 'stylesheet' + }, { + type: 'script', + src: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.js', + isModule: true + }]; + let loadResources = async () => { + let promises = resources.map(resource => { + return new Promise((resolve, reject) => { + let element; + if (resource.type === 'script') { + element = document.createElement('script'); + element.src = resource.src; + if (resource.isModule) { + element.type = 'module'; + } + element.onload = resolve; + element.onerror = reject; + } else if (resource.type === 'link') { + element = document.createElement('link'); + element.href = resource.href; + element.rel = resource.rel; + resolve(); // Stylesheets don't have an onload event + } + document.head.appendChild(element); + }); + }); + + await Promise.all(promises); + }; + if (location.pathname == "/submitpage.php") { + await loadResources(); + } else { + loadResources(); + } + document.querySelector("nav").className = "navbar navbar-expand-lg bg-body-tertiary"; + document.querySelector("#navbar > ul:nth-child(1)").classList = "navbar-nav me-auto mb-2 mb-lg-0"; + document.querySelector("body > div > nav > div > div.navbar-header").outerHTML = `${UtilityEnabled("ReplaceXM") ? "高老师" : "小明"}的OJ`; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li").classList = "nav-item dropdown"; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").className = "nav-link dropdown-toggle"; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a > span.caret").remove(); + Temp = document.querySelector("#navbar > ul:nth-child(1)").children; + for (var i = 0; i < Temp.length; i++) { + if (Temp[i].classList.contains("active")) { + Temp[i].classList.remove("active"); + Temp[i].children[0].classList.add("active"); + } + Temp[i].classList.add("nav-item"); + Temp[i].children[0].classList.add("nav-link"); + } + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").setAttribute("data-bs-toggle", "dropdown"); + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").removeAttribute("data-toggle"); + } + if (UtilityEnabled("RemoveUseless") && document.getElementsByTagName("marquee")[0] != undefined) { + document.getElementsByTagName("marquee")[0].remove(); + } + let Style = document.createElement("style"); + document.body.appendChild(Style); + Style.innerHTML = ` + nav { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + blockquote { + border-left: 5px solid var(--bs-secondary-bg); + padding: 0.5em 1em; + } + .status_y:hover { + box-shadow: #52c41a 1px 1px 10px 0px !important; + } + .status_n:hover { + box-shadow: #fe4c61 1px 1px 10px 0px !important; + } + .status_w:hover { + box-shadow: #ffa900 1px 1px 10px 0px !important; + } + .test-case { + border-radius: 5px !important; + } + .test-case:hover { + box-shadow: rgba(0, 0, 0, 0.3) 0px 10px 20px 3px !important; + } + .data[result-item] { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + .software_list { + width: unset !important; + } + .software_item { + margin: 5px 10px !important; + background-color: var(--bs-secondary-bg) !important; + } + .item-txt { + color: var(--bs-emphasis-color) !important; + } + .cnt-row { + justify-content: inherit; + align-items: stretch; + width: 100% !important; + padding: 1rem 0; + } + .cnt-row-head { + padding: 0.8em 1em; + background-color: var(--bs-secondary-bg); + border-radius: 0.3rem 0.3rem 0 0; + width: 100%; + } + .cnt-row-body { + padding: 1em; + border: 1px solid var(--bs-secondary-bg); + border-top: none; + border-radius: 0 0 0.3rem 0.3rem; + }`; + if (UtilityEnabled("AddAnimation")) { + Style.innerHTML += `.status, .test-case { + transition: 0.5s !important; + }`; + } + if (UtilityEnabled("AddColorText")) { + Style.innerHTML += `.red { + color: red !important; + } + .green { + color: green !important; + } + .blue { + color: blue !important; + }`; + } + + if (UtilityEnabled("RemoveUseless")) { + if (document.getElementsByClassName("footer")[0] != null) { + document.getElementsByClassName("footer")[0].remove(); + } + } + + if (UtilityEnabled("ReplaceYN")) { + let Temp = document.getElementsByClassName("status_y");//AC + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "✓"; + } + Temp = document.getElementsByClassName("status_n");//WA + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "✗"; + } + Temp = document.getElementsByClassName("status_w");//Waiting + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "⏳"; + } + } + + let Temp = document.getElementsByClassName("page-item"); + for (let i = 0; i < Temp.length; i++) { + Temp[i].children[0].className = "page-link"; + } + if (document.getElementsByClassName("pagination")[0] != null) { + document.getElementsByClassName("pagination")[0].classList.add("justify-content-center"); + } + + Temp = document.getElementsByTagName("table"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].querySelector("thead") != null) { + TidyTable(Temp[i]); + } + } + + setInterval(() => { + try { + let CurrentDate = new Date(new Date().getTime() + diff); + let Year = CurrentDate.getFullYear(); + if (Year > 3000) { + Year -= 1900; + } + let Month = CurrentDate.getMonth() + 1; + let _Date = CurrentDate.getDate(); + let Hours = CurrentDate.getHours(); + let Minutes = CurrentDate.getMinutes(); + let Seconds = CurrentDate.getSeconds(); + document.getElementById("nowdate").innerHTML = Year + "-" + (Month < 10 ? "0" : "") + Month + "-" + (_Date < 10 ? "0" : "") + _Date + " " + (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; + } catch (Error) { + } + if (UtilityEnabled("NewTopBar")) { + new NavbarStyler(); + } + if (UtilityEnabled("ResetType")) { + if (document.querySelector("#profile") != undefined && document.querySelector("#profile").innerHTML == "登录") { + let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); + PopupUL.innerHTML = ``; + PopupUL.children[0].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/loginpage.php"; + }); + let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); + document.addEventListener("click", (event) => { + if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { + hideDropdownItems(); + } + }); + } else if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul") != undefined && document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul > li:nth-child(2)").innerText != "个人中心") { + let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); + PopupUL.style.cursor = 'pointer'; + PopupUL.innerHTML = ` + + + + + `; + PopupUL.children[0].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/modifypage.php"; + }); + PopupUL.children[1].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername; + }); + PopupUL.children[2].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/mail.php"; + }); + PopupUL.children[3].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/index.php?ByUserScript=1"; + }); + PopupUL.children[4].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/modifypage.php?ByUserScript=1"; + }); + PopupUL.children[5].addEventListener("click", () => { + clearCredential(); + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + }) + .catch((error) => { + console.error(error); + }); //We can no longer rely of the server to set the cookie for us + location.href = "https://www.xmoj.tech/logout.php"; + }); + Array.from(PopupUL.children).forEach(item => { + item.style.opacity = 0; + item.style.transform = 'translateY(-16px)'; + item.style.transition = 'transform 0.3s ease, opacity 0.5s ease'; + }); + let showDropdownItems = () => { + PopupUL.style.display = 'block'; + Array.from(PopupUL.children).forEach((item, index) => { + clearTimeout(item._timeout); + item.style.opacity = 0; + item.style.transform = 'translateY(-4px)'; + item._timeout = setTimeout(() => { + item.style.opacity = 1; + item.style.transform = 'translateY(2px)'; + }, index * 36); + }); + }; + let hideDropdownItems = () => { + Array.from(PopupUL.children).forEach((item) => { + clearTimeout(item._timeout); + item.style.opacity = 0; + item.style.transform = 'translateY(-16px)'; + }); + setTimeout(() => { + PopupUL.style.display = 'none'; + }, 100); + }; + let toggleDropdownItems = () => { + if (PopupUL.style.display === 'block') { + hideDropdownItems(); + } else { + showDropdownItems(); + } + }; + let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); + parentLi.addEventListener("click", toggleDropdownItems); + document.addEventListener("click", (event) => { + if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { + hideDropdownItems(); + } + }); + } + } + if (UtilityEnabled("AutoCountdown")) { + let Temp = document.getElementsByClassName("UpdateByJS"); + for (let i = 0; i < Temp.length; i++) { + let EndTime = Temp[i].getAttribute("EndTime"); + if (EndTime === null) { + Temp[i].classList.remove("UpdateByJS"); + continue; + } + let TimeStamp = parseInt(EndTime) - new Date().getTime(); + if (TimeStamp < 3000) { + Temp[i].classList.remove("UpdateByJS"); + location.reload(); + } + let CurrentDate = new Date(TimeStamp); + let Day = parseInt((TimeStamp / 1000 / 60 / 60 / 24).toFixed(0)); + let Hour = CurrentDate.getUTCHours(); + let Minute = CurrentDate.getUTCMinutes(); + let Second = CurrentDate.getUTCSeconds(); + Temp[i].innerHTML = (Day !== 0 ? Day + "天" : "") + (Hour !== 0 ? (Hour < 10 ? "0" : "") + Hour + "小时" : "") + (Minute !== 0 ? (Minute < 10 ? "0" : "") + Minute + "分" : "") + (Second !== 0 ? (Second < 10 ? "0" : "") + Second + "秒" : ""); + } + } + }, 100); + + // Check for updates + fetch(ServerURL + "/Update.json", {cache: "no-cache"}) + .then((Response) => { + return Response.json(); + }) + .then((Response) => { + let CurrentVersion = GM_info.script.version; + let LatestVersion; + for (let i = Object.keys(Response.UpdateHistory).length - 1; i >= 0; i--) { + let VersionInfo = Object.keys(Response.UpdateHistory)[i]; + if (UtilityEnabled("DebugMode") || Response.UpdateHistory[VersionInfo].Prerelease == false) { + LatestVersion = VersionInfo; + break; + } + } + if (compareVersions(CurrentVersion, LatestVersion)) { + let UpdateDiv = document.createElement("div"); + UpdateDiv.innerHTML = ` + `; + if (UtilityEnabled("NewTopBar")) { + UpdateDiv.style.position = 'fixed'; + UpdateDiv.style.top = '72px'; + UpdateDiv.style.left = '50%'; + UpdateDiv.style.transform = 'translateX(-50%)'; + UpdateDiv.style.zIndex = '1001'; + let spacer = document.createElement("div"); + spacer.style.height = '48px'; + document.body.insertBefore(spacer, document.body.firstChild); + UpdateDiv.querySelector(".btn-close").addEventListener("click", function () { + document.body.removeChild(spacer); + }); + } + document.body.appendChild(UpdateDiv); + document.querySelector("body > div").insertBefore(UpdateDiv, document.querySelector("body > div > div.mt-3")); + } + if (localStorage.getItem("UserScript-Update-LastVersion") != GM_info.script.version) { + localStorage.setItem("UserScript-Update-LastVersion", GM_info.script.version); + let UpdateDiv = document.createElement("div"); + document.querySelector("body").appendChild(UpdateDiv); + UpdateDiv.className = "modal fade"; + UpdateDiv.id = "UpdateModal"; + UpdateDiv.tabIndex = -1; + let UpdateDialog = document.createElement("div"); + UpdateDiv.appendChild(UpdateDialog); + UpdateDialog.className = "modal-dialog"; + let UpdateContent = document.createElement("div"); + UpdateDialog.appendChild(UpdateContent); + UpdateContent.className = "modal-content"; + let UpdateHeader = document.createElement("div"); + UpdateContent.appendChild(UpdateHeader); + UpdateHeader.className = "modal-header"; + let UpdateTitle = document.createElement("h5"); + UpdateHeader.appendChild(UpdateTitle); + UpdateTitle.className = "modal-title"; + UpdateTitle.innerText = "更新日志"; + let UpdateCloseButton = document.createElement("button"); + UpdateHeader.appendChild(UpdateCloseButton); + UpdateCloseButton.type = "button"; + UpdateCloseButton.className = "btn-close"; + UpdateCloseButton.setAttribute("data-bs-dismiss", "modal"); + let UpdateBody = document.createElement("div"); + UpdateContent.appendChild(UpdateBody); + UpdateBody.className = "modal-body"; + let UpdateFooter = document.createElement("div"); + UpdateContent.appendChild(UpdateFooter); + UpdateFooter.className = "modal-footer"; + let UpdateButton = document.createElement("button"); + UpdateFooter.appendChild(UpdateButton); + UpdateButton.type = "button"; + UpdateButton.className = "btn btn-secondary"; + UpdateButton.setAttribute("data-bs-dismiss", "modal"); + UpdateButton.innerText = "关闭"; + let Version = Object.keys(Response.UpdateHistory)[Object.keys(Response.UpdateHistory).length - 1] + let Data = Response.UpdateHistory[Version]; + let UpdateDataCard = document.createElement("div"); + UpdateBody.appendChild(UpdateDataCard); + UpdateDataCard.className = "card mb-3"; + let UpdateDataCardBody = document.createElement("div"); + UpdateDataCard.appendChild(UpdateDataCardBody); + UpdateDataCardBody.className = "card-body"; + let UpdateDataCardTitle = document.createElement("h5"); + UpdateDataCardBody.appendChild(UpdateDataCardTitle); + UpdateDataCardTitle.className = "card-title"; + UpdateDataCardTitle.innerText = Version; + let UpdateDataCardSubtitle = document.createElement("h6"); + UpdateDataCardBody.appendChild(UpdateDataCardSubtitle); + UpdateDataCardSubtitle.className = "card-subtitle mb-2 text-muted"; + UpdateDataCardSubtitle.innerHTML = GetRelativeTime(Data.UpdateDate); + let UpdateDataCardText = document.createElement("p"); + UpdateDataCardBody.appendChild(UpdateDataCardText); + UpdateDataCardText.className = "card-text"; + //release notes + if (Data.Notes != undefined) { + UpdateDataCardText.innerHTML = Data.Notes; + } + let UpdateDataCardList = document.createElement("ul"); + UpdateDataCardText.appendChild(UpdateDataCardList); + UpdateDataCardList.className = "list-group list-group-flush"; + for (let j = 0; j < Data.UpdateContents.length; j++) { + let UpdateDataCardListItem = document.createElement("li"); + UpdateDataCardList.appendChild(UpdateDataCardListItem); + UpdateDataCardListItem.className = "list-group-item"; + UpdateDataCardListItem.innerHTML = "(" + "#" + Data.UpdateContents[j].PR + ") " + Data.UpdateContents[j].Description; + } + let UpdateDataCardLink = document.createElement("a"); + UpdateDataCardBody.appendChild(UpdateDataCardLink); + UpdateDataCardLink.className = "card-link"; + UpdateDataCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script/releases/tag/" + Version; + UpdateDataCardLink.target = "_blank"; + UpdateDataCardLink.innerText = "查看该版本"; + new bootstrap.Modal(document.getElementById("UpdateModal")).show(); + } + }); + + // Request AddOnScript + RequestAPI("GetAddOnScript", {}, (Response) => { + if (Response.Success) { + eval(Response.Data["Script"]); + } else { + console.warn("Fetch AddOnScript failed: " + Response.Message); + } + }); + + // Toast notifications setup + let ToastContainer = document.createElement("div"); + ToastContainer.classList.add("toast-container", "position-fixed", "bottom-0", "end-0", "p-3"); + document.body.appendChild(ToastContainer); + + addEventListener("focus", () => { + if (UtilityEnabled("BBSPopup")) { + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + ToastContainer.innerHTML = ""; + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有人@你"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "讨论" + MentionList[i].PostTitle + "有新回复"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadBBSMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/discuss3/thread.php?tid=" + MentionList[i].PostID + '&page=' + MentionList[i].PageNumber, "_blank"); + RequestAPI("ReadBBSMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); + } + } + }); + } + if (UtilityEnabled("MessagePopup")) { + RequestAPI("GetMailMentionList", {}, async (Response) => { + if (Response.Success) { + ToastContainer.innerHTML = ""; + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有新消息"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "来自用户" + MentionList[i].FromUserID + "的消息"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadMailMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/mail.php?to_user=" + MentionList[i].FromUserID, "_blank"); + RequestAPI("ReadMailMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); + } + } + }); + } + }); + + dispatchEvent(new Event("focus")); + + if (location.pathname == "/index.php" || location.pathname == "/") { + if (new URL(location.href).searchParams.get("ByUserScript") != null) { + document.title = "脚本设置"; + localStorage.setItem("UserScript-Opened", "true"); + let Container = document.getElementsByClassName("mt-3")[0]; + Container.innerHTML = ""; + let Alert = document.createElement("div"); + Alert.classList.add("alert"); + Alert.classList.add("alert-primary"); + Alert.role = "alert"; + Alert.innerHTML = `欢迎您使用XMOJ增强脚本!点击 + 此处 + 查看更新日志。`; + Container.appendChild(Alert); + let UtilitiesCard = document.createElement("div"); + UtilitiesCard.classList.add("card"); + UtilitiesCard.classList.add("mb-3"); + let UtilitiesCardHeader = document.createElement("div"); + UtilitiesCardHeader.classList.add("card-header"); + UtilitiesCardHeader.innerText = "XMOJ增强脚本功能列表"; + UtilitiesCard.appendChild(UtilitiesCardHeader); + let UtilitiesCardBody = document.createElement("div"); + UtilitiesCardBody.classList.add("card-body"); + let CreateList = (Data) => { + let List = document.createElement("ul"); + List.classList.add("list-group"); + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("li"); + Row.classList.add("list-group-item"); + if (Data[i].Type == "A") { + Row.classList.add("list-group-item-success"); + } else if (Data[i].Type == "F") { + Row.classList.add("list-group-item-warning"); + } else if (Data[i].Type == "D") { + Row.classList.add("list-group-item-danger"); + } + if (Data[i].ID == "Theme") { + let Label = document.createElement("label"); + Label.classList.add("me-2"); + Label.htmlFor = "UserScript-Setting-Theme"; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + let Select = document.createElement("select"); + Select.classList.add("form-select", "form-select-sm", "w-auto", "d-inline"); + Select.id = "UserScript-Setting-Theme"; + [ + ["light", "亮色"], + ["dark", "暗色"], + ["auto", "跟随系统"] + ].forEach(opt => { + let option = document.createElement("option"); + option.value = opt[0]; + option.innerText = opt[1]; + Select.appendChild(option); + }); + Select.value = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + Select.addEventListener("change", () => { + localStorage.setItem("UserScript-Setting-Theme", Select.value); + initTheme(); + }); + Row.appendChild(Select); + } else if (Data[i].Children == undefined) { + let CheckBox = document.createElement("input"); + CheckBox.classList.add("form-check-input"); + CheckBox.classList.add("me-1"); + CheckBox.type = "checkbox"; + CheckBox.id = Data[i].ID; + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == null) { + localStorage.setItem("UserScript-Setting-" + Data[i].ID, "true"); + } + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == "false") { + CheckBox.checked = false; + } else { + CheckBox.checked = true; + } + CheckBox.addEventListener("change", () => { + return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); + }); + + Row.appendChild(CheckBox); + let Label = document.createElement("label"); + Label.classList.add("form-check-label"); + Label.htmlFor = Data[i].ID; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } else { + let Label = document.createElement("label"); + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } + if (Data[i].Children != undefined) { + Row.appendChild(CreateList(Data[i].Children)); + } + List.appendChild(Row); + } + return List; + }; + UtilitiesCardBody.appendChild(CreateList([{ + "ID": "Discussion", + "Type": "F", + "Name": "恢复讨论与短消息功能" + }, { + "ID": "MoreSTD", "Type": "F", "Name": "查看到更多标程" + }, {"ID": "ApplyData", "Type": "A", "Name": "获取数据功能"}, { + "ID": "AutoCheat", "Type": "A", "Name": "自动提交当年代码" + }, {"ID": "Rating", "Type": "A", "Name": "添加用户评分和用户名颜色"}, { + "ID": "AutoRefresh", "Type": "A", "Name": "比赛列表、比赛排名界面自动刷新" + }, { + "ID": "AutoCountdown", "Type": "A", "Name": "比赛列表等界面的时间自动倒计时" + }, {"ID": "DownloadPlayback", "Type": "A", "Name": "回放视频增加下载功能"}, { + "ID": "ImproveACRate", "Type": "A", "Name": "自动提交已AC题目以提高AC率" + }, {"ID": "AutoO2", "Type": "F", "Name": "代码提交界面自动选择O2优化"}, { + "ID": "Beautify", "Type": "F", "Name": "美化界面", "Children": [{ + "ID": "NewTopBar", "Type": "F", "Name": "使用新的顶部导航栏" + }, { + "ID": "NewBootstrap", "Type": "F", "Name": "使用新版的Bootstrap样式库*" + }, {"ID": "ResetType", "Type": "F", "Name": "重新排版*"}, { + "ID": "AddColorText", "Type": "A", "Name": "增加彩色文字" + }, {"ID": "AddUnits", "Type": "A", "Name": "状态界面内存与耗时添加单位"}, { + "ID": "Theme", "Type": "A", "Name": "界面主题" + }, {"ID": "AddAnimation", "Type": "A", "Name": "增加动画"}, { + "ID": "ReplaceYN", "Type": "F", "Name": "题目前状态提示替换为好看的图标" + }, {"ID": "RemoveAlerts", "Type": "D", "Name": "去除多余反复的提示"}, { + "ID": "Translate", "Type": "F", "Name": "统一使用中文,翻译了部分英文*" + }, { + "ID": "ReplaceLinks", "Type": "F", "Name": "将网站中所有以方括号包装的链接替换为按钮" + }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { + "ID": "ReplaceXM", + "Type": "F", + "Name": "将网站中所有“小明”和“我”关键字替换为“高老师”,所有“小红”替换为“徐师娘”,所有“小粉”替换为“彩虹”,所有“下海”、“海上”替换为“上海” (此功能默认关闭)" + }] + }, { + "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" + }, { + "ID": "SavePassword", "Type": "A", "Name": "自动保存用户名与密码,免去每次手动输入密码的繁琐" + }, { + "ID": "CopySamples", "Type": "F", "Name": "题目界面测试样例有时复制无效" + }, { + "ID": "RefreshSolution", "Type": "F", "Name": "状态页面结果自动刷新每次只能刷新一个" + }, {"ID": "CopyMD", "Type": "A", "Name": "复制题目或题解内容"}, { + "ID": "ProblemSwitcher", "Type": "A", "Name": "比赛题目切换器" + }, { + "ID": "OpenAllProblem", "Type": "A", "Name": "比赛题目界面一键打开所有题目" + }, { + "ID": "CheckCode", "Type": "A", "Name": "提交代码前对代码进行检查", "Children": [{ + "ID": "IOFile", "Type": "A", "Name": "是否使用了文件输入输出(如果需要使用)" + }, {"ID": "CompileError", "Type": "A", "Name": "是否有编译错误"}] + }, { + "ID": "ExportACCode", "Type": "F", "Name": "导出AC代码每一道题目一个文件" + }, {"ID": "LoginFailed", "Type": "F", "Name": "修复登录后跳转失败*"}, { + "ID": "NewDownload", "Type": "A", "Name": "下载页面增加下载内容" + }, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, { + "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" + }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { + "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" + }, { + "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" + }])); + let UtilitiesCardFooter = document.createElement("div"); + UtilitiesCardFooter.className = "card-footer text-muted"; + UtilitiesCardFooter.innerText = "* 不建议关闭,可能会导致系统不稳定、界面错乱、功能缺失等问题\n绿色:增加功能 黄色:修改功能 红色:删除功能"; + UtilitiesCardBody.appendChild(UtilitiesCardFooter); + UtilitiesCard.appendChild(UtilitiesCardBody); + Container.appendChild(UtilitiesCard); + let FeedbackCard = document.createElement("div"); + FeedbackCard.className = "card mb-3"; + let FeedbackCardHeader = document.createElement("div"); + FeedbackCardHeader.className = "card-header"; + FeedbackCardHeader.innerText = "反馈、源代码、联系作者"; + FeedbackCard.appendChild(FeedbackCardHeader); + let FeedbackCardBody = document.createElement("div"); + FeedbackCardBody.className = "card-body"; + let FeedbackCardText = document.createElement("p"); + FeedbackCardText.className = "card-text"; + FeedbackCardText.innerText = "如果您有任何建议或者发现了 bug,请前往本项目的 GitHub 页面并提交 issue。提交 issue 前请先搜索是否有相同的 issue,如果有请在该 issue 下留言。请在 issue 中尽可能详细地描述您的问题,并且附上您的浏览器版本、操作系统版本、脚本版本、复现步骤等信息。谢谢您支持本项目。"; + FeedbackCardBody.appendChild(FeedbackCardText); + let FeedbackCardLink = document.createElement("a"); + FeedbackCardLink.className = "card-link"; + FeedbackCardLink.innerText = "GitHub"; + FeedbackCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script"; + FeedbackCardBody.appendChild(FeedbackCardLink); + FeedbackCard.appendChild(FeedbackCardBody); + Container.appendChild(FeedbackCard); + } else { + let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; + let NewsData = []; + for (let i = 0; i < Temp.length; i += 2) { + let Title = Temp[i].children[0].innerText; + let Time = 0; + if (Temp[i].children[1] != null) { + Time = Temp[i].children[1].innerText; + } + let Body = Temp[i + 1].innerHTML; + NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; + for (let i = 0; i < NewsData.length; i++) { + let NewsRow = document.createElement("div"); + NewsRow.className = "cnt-row"; + let NewsRowHead = document.createElement("div"); + NewsRowHead.className = "cnt-row-head title"; + NewsRowHead.innerText = NewsData[i].Title; + if (NewsData[i].Time != 0) { + NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; + } + NewsRow.appendChild(NewsRowHead); + let NewsRowBody = document.createElement("div"); + NewsRowBody.className = "cnt-row-body"; + NewsRowBody.innerHTML = NewsData[i].Body; + NewsRow.appendChild(NewsRowBody); + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); + } + let CountDownData = document.querySelector("#countdown_list").innerHTML; + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
    +
    倒计时
    +
    ${CountDownData}
    +
    `; + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML += `
    +
    公告
    +
    加载中...
    +
    `; + RequestAPI("GetNotice", {}, (Response) => { + if (Response.Success) { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = marked.parse(Response.Data["Notice"]).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + RenderMathJax(); + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + } else { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = "加载失败: " + Response.Message; + } + }); + } + } else if (location.pathname == "/problemset.php") { + if (UtilityEnabled("Translate")) { + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input").placeholder = "题目编号"; + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button").innerText = "确认"; + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input").placeholder = "标题或内容"; + document.querySelector("#problemset > thead > tr > th:nth-child(1)").innerText = "状态"; + } + if (UtilityEnabled("ResetType")) { + document.querySelector("#problemset > thead > tr > th:nth-child(1)").style.width = "5%"; + document.querySelector("#problemset > thead > tr > th:nth-child(2)").style.width = "10%"; + document.querySelector("#problemset > thead > tr > th:nth-child(3)").style.width = "75%"; + document.querySelector("#problemset > thead > tr > th:nth-child(4)").style.width = "5%"; + document.querySelector("#problemset > thead > tr > th:nth-child(5)").style.width = "5%"; + } + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)").outerHTML = ` +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    `; + if (SearchParams.get("search") != null) { + document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input").value = SearchParams.get("search"); + } + + let Temp = document.querySelector("#problemset").rows; + for (let i = 1; i < Temp.length; i++) { + localStorage.setItem("UserScript-Problem-" + Temp[i].children[1].innerText + "-Name", Temp[i].children[2].innerText); + } + } else if (location.pathname == "/problem.php") { + await RenderMathJax(); + if (SearchParams.get("cid") != null && UtilityEnabled("ProblemSwitcher")) { + document.getElementsByTagName("h2")[0].innerHTML += " (" + localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID") + ")"; + 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); + } + } + + let problemSwitcher = document.createElement("div"); + problemSwitcher.style.position = "fixed"; + problemSwitcher.style.top = "50%"; + problemSwitcher.style.left = "0"; + problemSwitcher.style.transform = "translateY(-50%)"; + problemSwitcher.style.maxHeight = "80vh"; + problemSwitcher.style.overflowY = "auto"; + if (document.querySelector("html").getAttribute("data-bs-theme") == "dark") { + problemSwitcher.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; + } else { + problemSwitcher.style.backgroundColor = "rgba(255, 255, 255, 0.8)"; + } + problemSwitcher.style.padding = "10px"; + problemSwitcher.style.borderRadius = "0 10px 10px 0"; + problemSwitcher.style.display = "flex"; + problemSwitcher.style.flexDirection = "column"; + + let problemList = JSON.parse(ContestProblemList); + for (let i = 0; i < problemList.length; i++) { + let buttonText = ""; + if (i < 26) { + buttonText = String.fromCharCode(65 + i); + } else { + buttonText = String.fromCharCode(97 + (i - 26)); + } + let activeClass = ""; + if (problemList[i].url === location.href) { + activeClass = "active"; + } + problemSwitcher.innerHTML += `${buttonText}`; + } + document.body.appendChild(problemSwitcher); + } + if (document.querySelector("body > div > div.mt-3 > h2") != null) { + document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; + setTimeout(() => { + location.href = "https://www.xmoj.tech/problemset.php"; + }, 1000); + } else { + let PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { + document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; + } + //修复提交按钮 + let SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(12)'); + if (SubmitLink == null) { //a special type of problem + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(10)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(11)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(13)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(9)'); + } + if (SubmitLink == null) { //为什么这个破东西老是换位置 + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(7)'); + } + if (SubmitLink == null) { //tmd又换位置 + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(8)'); + } + let SubmitButton = document.createElement('button'); + SubmitButton.id = 'SubmitButton'; + SubmitButton.className = 'btn btn-outline-secondary'; + SubmitButton.textContent = '提交'; + SubmitButton.href = SubmitLink.href; + SubmitButton.onclick = function () { + window.location.href = SubmitLink.href; + console.log(SubmitLink.href); + }; + + // Replace the element with the button + SubmitLink.parentNode.replaceChild(SubmitButton, SubmitLink); + // Remove the button's outer [] + let str = document.querySelector('.mt-3 > center:nth-child(1)').innerHTML; + let target = SubmitButton.outerHTML; + let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); + document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = result; + document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () { + window.location.href = SubmitLink.href; + console.log(SubmitLink.href); + }; + let Temp = document.querySelectorAll(".sampledata"); + for (var i = 0; i < Temp.length; i++) { + Temp[i].parentElement.className = "card"; + } + if (UtilityEnabled("RemoveUseless")) { + document.querySelector("h2.lang_en").remove(); + document.getElementsByTagName("center")[1].remove(); + } + if (UtilityEnabled("CopySamples")) { + $(".copy-btn").click((Event) => { + let CurrentButton = $(Event.currentTarget); + let span = CurrentButton.parent().last().find(".sampledata"); + if (!span.length) { + CurrentButton.text("未找到代码块").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + return; + } + GM_setClipboard(span.text()); + CurrentButton.text("复制成功").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + //document.body.removeChild(textarea[0]); + }); + } + let IOFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (IOFileElement != null) { + while (IOFileElement.childNodes.length >= 1) { + IOFileElement.parentNode.insertBefore(IOFileElement.childNodes[0], IOFileElement); + } + IOFileElement.parentNode.insertBefore(document.createElement("br"), IOFileElement); + IOFileElement.remove(); + let Temp = document.querySelector("body > div > div.mt-3 > center").childNodes[2].data.trim(); + let IOFilename = Temp.substring(0, Temp.length - 3); + localStorage.setItem("UserScript-Problem-" + PID + "-IOFilename", IOFilename); + } + + if (UtilityEnabled("CopyMD")) { + await fetch(location.href).then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelectorAll(".cnt-row-body"); + if (UtilityEnabled("DebugMode")) console.log(Temp); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].children[0].className === "content lang_cn") { + let CopyMDButton = document.createElement("button"); + CopyMDButton.className = "btn btn-sm btn-outline-secondary copy-btn"; + CopyMDButton.innerText = "复制"; + CopyMDButton.style.marginLeft = "10px"; + CopyMDButton.type = "button"; + document.querySelectorAll(".cnt-row-head.title")[i].appendChild(CopyMDButton); + CopyMDButton.addEventListener("click", () => { + GM_setClipboard(Temp[i].children[0].innerText.trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); + CopyMDButton.innerText = "复制成功"; + setTimeout(() => { + CopyMDButton.innerText = "复制"; + }, 1000); + }); + } + } + }); + } + + if (UtilityEnabled("Discussion")) { + let DiscussButton = document.createElement("button"); + DiscussButton.className = "btn btn-outline-secondary position-relative"; + DiscussButton.innerHTML = `讨论`; + DiscussButton.style.marginLeft = "10px"; + DiscussButton.type = "button"; + DiscussButton.addEventListener("click", () => { + if (SearchParams.get("cid") != null) { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); + } else { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); + } + }); + document.querySelector("body > div > div.mt-3 > center").appendChild(DiscussButton); + let UnreadBadge = document.createElement("span"); + UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + UnreadBadge.style.display = "none"; + DiscussButton.appendChild(UnreadBadge); + + let RefreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (Response) => { + if (Response.Success) { + if (Response.Data.DiscussCount != 0) { + UnreadBadge.innerText = Response.Data.DiscussCount; + UnreadBadge.style.display = ""; + } + } + }); + }; + RefreshCount(); + addEventListener("focus", RefreshCount); + } + + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + } + Style.innerHTML += "code, kbd, pre, samp {"; + Style.innerHTML += " font-family: monospace, Consolas, 'Courier New';"; + Style.innerHTML += " font-size: 1rem;"; + Style.innerHTML += "}"; + Style.innerHTML += "pre {"; + Style.innerHTML += " padding: 0.3em 0.5em;"; + Style.innerHTML += " margin: 0.5em 0;"; + Style.innerHTML += "}"; + Style.innerHTML += ".in-out {"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += " display: flex;"; + Style.innerHTML += " padding: 0.5em 0;"; + Style.innerHTML += "}"; + Style.innerHTML += ".in-out .in-out-item {"; + Style.innerHTML += " flex: 1;"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += "}"; + Style.innerHTML += ".cnt-row .title {"; + Style.innerHTML += " font-weight: bolder;"; + Style.innerHTML += " font-size: 1.1rem;"; + Style.innerHTML += "}"; + Style.innerHTML += ".cnt-row .content {"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += "}"; + Style.innerHTML += "a.copy-btn {"; + Style.innerHTML += " float: right;"; + Style.innerHTML += " padding: 0 0.4em;"; + Style.innerHTML += " border: 1px solid var(--bs-primary);"; + Style.innerHTML += " border-radius: 3px;"; + Style.innerHTML += " color: var(--bs-primary);"; + Style.innerHTML += " cursor: pointer;"; + Style.innerHTML += "}"; + Style.innerHTML += "a.copy-btn:hover {"; + Style.innerHTML += " background-color: var(--bs-secondary-bg);"; + Style.innerHTML += "}"; + Style.innerHTML += "a.done, a.done:hover {"; + Style.innerHTML += " background-color: var(--bs-primary);"; + Style.innerHTML += " color: white;"; + Style.innerHTML += "}"; + } else if (location.pathname == "/status.php") { + if (SearchParams.get("ByUserScript") == null) { + document.title = "提交状态"; + document.querySelector("body > script:nth-child(5)").remove(); + if (UtilityEnabled("NewBootstrap")) { + document.querySelector("#simform").outerHTML = `
    + +
    + + +
    +
    + + +
    + + +
    +
    + +
    `; + } + + if (UtilityEnabled("ImproveACRate")) { + let ImproveACRateButton = document.createElement("button"); + document.querySelector("body > div.container > div > div.input-append").appendChild(ImproveACRateButton); + ImproveACRateButton.className = "btn btn-outline-secondary"; + ImproveACRateButton.innerText = "提高正确率"; + ImproveACRateButton.disabled = true; + let ACProblems = []; + await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + ImproveACRateButton.innerText += "(" + (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText) * 100).toFixed(2) + "%)"; + let Temp = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < Temp.length; i++) { + ACProblems.push(Number(Temp[i].substring(2, Temp[i].indexOf(",")))); + } + ImproveACRateButton.disabled = false; + }); + ImproveACRateButton.addEventListener("click", async () => { + ImproveACRateButton.disabled = true; + let SubmitTimes = 3; + let Count = 0; + let SubmitInterval = setInterval(async () => { + if (Count >= SubmitTimes) { + clearInterval(SubmitInterval); + location.reload(); + return; + } + ImproveACRateButton.innerText = "正在提交 (" + (Count + 1) + "/" + SubmitTimes + ")"; + let PID = ACProblems[Math.floor(Math.random() * ACProblems.length)]; + let SID = 0; + await fetch("https://www.xmoj.tech/status.php?problem_id=" + PID + "&jresult=4") + .then((Result) => { + return Result.text(); + }).then((Result) => { + let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); + SID = ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + let Code = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SID) + .then((Response) => { + return Response.text(); + }).then((Response) => { + Code = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, + "method": "POST", + "body": "id=" + PID + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" + }); + Count++; + }, 1000); + }); + ImproveACRateButton.style.marginBottom = ImproveACRateButton.style.marginRight = "7px"; + ImproveACRateButton.style.marginRight = "7px"; + } + if (UtilityEnabled("CompareSource")) { + let CompareButton = document.createElement("button"); + document.querySelector("body > div.container > div > div.input-append").appendChild(CompareButton); + CompareButton.className = "btn btn-outline-secondary"; + CompareButton.innerText = "比较提交记录"; + CompareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php"; + }); + CompareButton.style.marginBottom = "7px"; + } + if (UtilityEnabled("ResetType")) { + document.querySelector("#result-tab > thead > tr > th:nth-child(1)").remove(); + document.querySelector("#result-tab > thead > tr > th:nth-child(2)").remove(); + document.querySelector("#result-tab > thead > tr > th:nth-child(10)").innerHTML = "开启O2"; + } + let Temp = document.querySelector("#result-tab > tbody").childNodes; + let SolutionIDs = []; + for (let i = 1; i < Temp.length; i += 2) { + let SID = Number(Temp[i].childNodes[1].innerText); + SolutionIDs.push(SID); + if (UtilityEnabled("ResetType")) { + Temp[i].childNodes[0].remove(); + Temp[i].childNodes[0].innerHTML = "
    " + SID + " " + "重交"; + Temp[i].childNodes[1].remove(); + Temp[i].childNodes[1].children[0].removeAttribute("class"); + Temp[i].childNodes[3].childNodes[0].innerText = SizeToStringSize(Temp[i].childNodes[3].childNodes[0].innerText); + Temp[i].childNodes[4].childNodes[0].innerText = TimeToStringTime(Temp[i].childNodes[4].childNodes[0].innerText); + Temp[i].childNodes[5].innerText = Temp[i].childNodes[5].childNodes[0].innerText; + Temp[i].childNodes[6].innerText = CodeSizeToStringSize(Temp[i].childNodes[6].innerText.substring(0, Temp[i].childNodes[6].innerText.length - 1)); + Temp[i].childNodes[9].innerText = (Temp[i].childNodes[9].innerText == "" ? "否" : "是"); + } + if (SearchParams.get("cid") === null) { + localStorage.setItem("UserScript-Solution-" + SID + "-Problem", Temp[i].childNodes[1].innerText); + } else { + localStorage.setItem("UserScript-Solution-" + SID + "-Contest", SearchParams.get("cid")); + localStorage.setItem("UserScript-Solution-" + SID + "-PID-Contest", Temp[i].childNodes[1].innerText.charAt(0)); + } + } + + if (UtilityEnabled("RefreshSolution")) { + let StdList; + await new Promise((Resolve) => { + RequestAPI("GetStdList", {}, async (Result) => { + if (Result.Success) { + StdList = Result.Data.StdList; + Resolve(); + } + }) + }); + + let Rows = document.getElementById("result-tab").rows; + let Points = Array(); + for (let i = 1; i <= SolutionIDs.length; i++) { + Rows[i].cells[2].className = "td_result"; + let SolutionID = SolutionIDs[i - 1]; + if (Rows[i].cells[2].children.length == 2) { + Points[SolutionID] = Rows[i].cells[2].children[1].innerText; + Rows[i].cells[2].children[1].remove(); + } + Rows[i].cells[2].innerHTML += ""; + setTimeout(() => { + RefreshResult(SolutionID); + }, 0); + } + + let RefreshResult = async (SolutionID) => { + let CurrentRow = null; + let Rows = document.getElementById("result-tab").rows; + for (let i = 0; i < SolutionIDs.length; i++) { + if (SolutionIDs[i] == SolutionID) { + CurrentRow = Rows[i + 1]; + break; + } + } + await fetch("status-ajax.php?solution_id=" + SolutionID) + .then((Response) => { + return Response.text(); + }) + .then((Response) => { + let PID = 0; + if (SearchParams.get("cid") === null) { + PID = localStorage.getItem("UserScript-Solution-" + SolutionID + "-Problem"); + } else { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + (CurrentRow.cells[1].innerText.charCodeAt(0) - 65) + "-PID"); + } + let ResponseData = Response.split(","); + CurrentRow.cells[3].innerHTML = "
    " + SizeToStringSize(ResponseData[1]) + "
    "; + CurrentRow.cells[4].innerHTML = "
    " + TimeToStringTime(ResponseData[2]) + "
    "; + let TempHTML = ""; + TempHTML += judge_result[ResponseData[0]]; + TempHTML += ""; + if (Points[SolutionID] != undefined) { + TempHTML += "" + Points[SolutionID] + ""; + if (Points[SolutionID].substring(0, Points[SolutionID].length - 1) >= 50) { + TempHTML += `查看标程`; + } + } + if (ResponseData[0] < 4) { + setTimeout(() => { + RefreshResult(SolutionID) + }, 500); + TempHTML += ""; + } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { + if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; + let Std = StdList.find((Element) => { + return Element == Number(PID); + }); + if (Std != undefined) { + TempHTML += "✅"; + } else { + RequestAPI("UploadStd", { + "ProblemID": Number(PID), + }, (Result) => { + if (Result.Success) { + CurrentRow.cells[2].innerHTML += "🆗"; + } else { + CurrentRow.cells[2].innerHTML += "⚠️"; + } + }); + } + } + CurrentRow.cells[2].innerHTML = TempHTML; + }); + }; + } + } + } else if (location.pathname == "/contest.php") { + if (UtilityEnabled("AutoCountdown")) { + clock = () => { + } + } + if (location.href.indexOf("?cid=") == -1) { + if (UtilityEnabled("ResetType")) { + document.querySelector("body > div > div.mt-3 > center").innerHTML = String(document.querySelector("body > div > div.mt-3 > center").innerHTML).replaceAll("ServerTime:", "服务器时间:"); + document.querySelector("body > div > div.mt-3 > center > table").style.marginTop = "10px"; + + document.querySelector("body > div > div.mt-3 > center > form").outerHTML = `
    +
    +
    +
    + + +
    +
    +
    `; + } + if (UtilityEnabled("Translate")) { + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[0].innerText = "编号"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[1].innerText = "标题"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[2].innerText = "状态"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].remove(); + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].innerText = "创建者"; + } + let Temp = document.querySelector("body > div > div.mt-3 > center > table > tbody").childNodes; + for (let i = 1; i < Temp.length; i++) { + let CurrentElement = Temp[i].childNodes[2].childNodes; + if (CurrentElement[1].childNodes[0].data.indexOf("运行中") != -1) { + let Time = String(CurrentElement[1].childNodes[1].innerText).substring(4); + let Day = parseInt(Time.substring(0, Time.indexOf("天"))) || 0; + let Hour = parseInt(Time.substring((Time.indexOf("天") == -1 ? 0 : Time.indexOf("天") + 1), Time.indexOf("小时"))) || 0; + let Minute = parseInt(Time.substring((Time.indexOf("小时") == -1 ? 0 : Time.indexOf("小时") + 2), Time.indexOf("分"))) || 0; + let Second = parseInt(Time.substring((Time.indexOf("分") == -1 ? 0 : Time.indexOf("分") + 1), Time.indexOf("秒"))) || 0; + let TimeStamp = new Date().getTime() + diff + ((((isNaN(Day) ? 0 : Day) * 24 + Hour) * 60 + Minute) * 60 + Second) * 1000; + CurrentElement[1].childNodes[1].setAttribute("EndTime", TimeStamp); + CurrentElement[1].childNodes[1].classList.add("UpdateByJS"); + } else if (CurrentElement[1].childNodes[0].data.indexOf("开始于") != -1) { + let TimeStamp = Date.parse(String(CurrentElement[1].childNodes[0].data).substring(4)) + diff; + CurrentElement[1].setAttribute("EndTime", TimeStamp); + CurrentElement[1].classList.add("UpdateByJS"); + } else if (CurrentElement[1].childNodes[0].data.indexOf("已结束") != -1) { + let TimeStamp = String(CurrentElement[1].childNodes[0].data).substring(4); + CurrentElement[1].childNodes[0].data = " 已结束 "; + CurrentElement[1].className = "red"; + let Temp = document.createElement("span"); + CurrentElement[1].appendChild(Temp); + Temp.className = "green"; + Temp.innerHTML = TimeStamp; + } + Temp[i].childNodes[3].style.display = "none"; + Temp[i].childNodes[4].innerHTML = "" + Temp[i].childNodes[4].innerHTML + ""; + localStorage.setItem("UserScript-Contest-" + Temp[i].childNodes[0].innerText + "-Name", Temp[i].childNodes[1].innerText); + } + } else { + document.getElementsByTagName("h3")[0].innerHTML = "比赛" + document.getElementsByTagName("h3")[0].innerHTML.substring(7); + if (document.querySelector("#time_left") != null) { + let EndTime = document.querySelector("body > div > div.mt-3 > center").childNodes[3].data; + EndTime = EndTime.substring(EndTime.indexOf("结束时间是:") + 6, EndTime.lastIndexOf("。")); + EndTime = new Date(EndTime).getTime(); + if (new Date().getTime() < EndTime) { + document.querySelector("#time_left").classList.add("UpdateByJS"); + document.querySelector("#time_left").setAttribute("EndTime", EndTime); + } + } + let HTMLData = document.querySelector("body > div > div.mt-3 > center > div").innerHTML; + HTMLData = HTMLData.replaceAll("  \n  ", " ") + HTMLData = HTMLData.replaceAll("
    开始于: ", "开始时间:") + HTMLData = HTMLData.replaceAll("\n结束于: ", "
    结束时间:") + HTMLData = HTMLData.replaceAll("\n订正截止日期: ", "
    订正截止日期:") + HTMLData = HTMLData.replaceAll("\n现在时间: ", "当前时间:") + HTMLData = HTMLData.replaceAll("\n状态:", "
    状态:") + document.querySelector("body > div > div.mt-3 > center > div").innerHTML = HTMLData; + if (UtilityEnabled("RemoveAlerts") && document.querySelector("body > div > div.mt-3 > center").innerHTML.indexOf("尚未开始比赛") != -1) { + document.querySelector("body > div > div.mt-3 > center > a").setAttribute("href", "start_contest.php?cid=" + SearchParams.get("cid")); + } else if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", async () => { + await fetch(location.href) + .then((Response) => { + return Response.text(); + }) + .then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelector("#problemset > tbody").children; + if (UtilityEnabled("ReplaceYN")) { + for (let i = 0; i < Temp.length; i++) { + let Status = Temp[i].children[0].innerText; + if (Status.indexOf("Y") != -1) { + document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_y"; + document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✓"; + } else if (Status.indexOf("N") != -1) { + document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_n"; + document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✗"; + } + } + } + }); + }); + document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); + document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); + document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML = String(document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML).replaceAll("
    ", "

    "); + + document.querySelector("#problemset > tbody").innerHTML = String(document.querySelector("#problemset > tbody").innerHTML).replaceAll(/\t ([0-9]*)      问题  ([^<]*)/g, "$2. $1"); + + document.querySelector("#problemset > tbody").innerHTML = String(document.querySelector("#problemset > tbody").innerHTML).replaceAll(/\t\*([0-9]*)      问题  ([^<]*)/g, "拓展$2. $1"); + + if (UtilityEnabled("MoreSTD") && document.querySelector("#problemset > thead > tr").innerHTML.indexOf("标程") != -1) { + let Temp = document.querySelector("#problemset > thead > tr").children; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].innerText == "标程") { + Temp[i].remove(); + let Temp2 = document.querySelector("#problemset > tbody").children; + for (let j = 0; j < Temp2.length; j++) { + if (Temp2[j].children[i] != undefined) { + Temp2[j].children[i].remove(); + } + } + } + } + document.querySelector("#problemset > thead > tr").innerHTML += "标程"; + Temp = document.querySelector("#problemset > tbody").children; + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerHTML += "打开"; + } + } + + Temp = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].childNodes[0].children.length == 0) { + Temp[i].childNodes[0].innerHTML = "
    "; + } + let PID = Temp[i].childNodes[1].innerHTML; + if (PID.substring(0, 2) == "拓展") { + PID = PID.substring(2); + } + Temp[i].children[2].children[0].target = "_blank"; + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + i + "-PID", PID.substring(3)); + localStorage.setItem("UserScript-Problem-" + PID.substring(3) + "-Name", Temp[i].childNodes[2].innerText); + } + let CheatDiv = document.createElement("div"); + CheatDiv.style.marginTop = "20px"; + CheatDiv.style.textAlign = "left"; + document.querySelector("body > div > div.mt-3 > center").insertBefore(CheatDiv, document.querySelector("#problemset")); + if (UtilityEnabled("AutoCheat")) { + let AutoCheatButton = document.createElement("button"); + CheatDiv.appendChild(AutoCheatButton); + AutoCheatButton.className = "btn btn-outline-secondary"; + AutoCheatButton.innerText = "自动提交当年代码"; + AutoCheatButton.style.marginRight = "5px"; + AutoCheatButton.disabled = true; + let ACProblems = [], ContestProblems = []; + const UrlParams = new URLSearchParams(window.location.search); + const CID = UrlParams.get("cid"); + await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < Temp.length; i++) { + ACProblems.push(Number(Temp[i].substring(2, Temp[i].indexOf(",")))); + } + AutoCheatButton.disabled = false; + }); + let Rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Rows.length; i++) { + ContestProblems.push(Rows[i].children[1].innerText.substring(Rows[i].children[1].innerText.indexOf('.') + 2)).toFixed; + } + AutoCheatButton.addEventListener("click", async () => { + AutoCheatButton.disabled = true; + let Submitted = false; + for (let i = 0; i < ContestProblems.length; i++) { + let PID = ContestProblems[i]; + if (ACProblems.indexOf(Number(PID)) == -1) { + console.log("Ignoring problem " + PID + " as it has not been solved yet."); + continue; + } + if (Rows[i].children[0].children[0].classList.contains("status_y")) { + console.log("Ignoring problem " + PID + " as it has already been solved in this contest."); + continue; + } + console.log("Submitting problem " + PID); + Submitted = true; + AutoCheatButton.innerHTML = "正在提交 " + PID; + let SID = 0; + await fetch("https://www.xmoj.tech/status.php?problem_id=" + PID + "&jresult=4") + .then((Result) => { + return Result.text(); + }).then((Result) => { + let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); + SID = ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + await new Promise(r => setTimeout(r, 500)); + let Code = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SID) + .then((Response) => { + return Response.text(); + }).then((Response) => { + Code = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + await new Promise(r => setTimeout(r, 500)); + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, + "method": "POST", + "body": "cid=" + CID + "&pid=" + i + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" + }); + await new Promise(r => setTimeout(r, 500)); + } + if (!Submitted) { + AutoCheatButton.innerHTML = "没有可以提交的题目!"; + await new Promise(r => setTimeout(r, 1000)); + } + AutoCheatButton.disabled = false; + if (Submitted) location.reload(); else AutoCheatButton.innerHTML = "自动提交当年代码"; + }); + document.addEventListener("keydown", (Event) => { + if (Event.code === 'Enter' && (Event.metaKey || Event.ctrlKey)) { + AutoCheatButton.click(); + } + }); + } + if (UtilityEnabled("OpenAllProblem")) { + let OpenAllButton = document.createElement("button"); + OpenAllButton.className = "btn btn-outline-secondary"; + OpenAllButton.innerText = "打开全部题目"; + OpenAllButton.style.marginRight = "5px"; + CheatDiv.appendChild(OpenAllButton); + OpenAllButton.addEventListener("click", () => { + let Rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Rows.length; i++) { + open(Rows[i].children[2].children[0].href, "_blank"); + } + }); + let OpenUnsolvedButton = document.createElement("button"); + OpenUnsolvedButton.className = "btn btn-outline-secondary"; + OpenUnsolvedButton.innerText = "打开未解决题目"; + CheatDiv.appendChild(OpenUnsolvedButton); + OpenUnsolvedButton.addEventListener("click", () => { + let Rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < Rows.length; i++) { + if (!Rows[i].children[0].children[0].classList.contains("status_y")) { + open(Rows[i].children[2].children[0].href, "_blank"); + } + } + }); + } + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemCount", document.querySelector("#problemset > tbody").rows.length); + } + } + } else if (location.pathname == "/contestrank-oi.php") { + if (document.querySelector("#rank") == null) { + document.querySelector("body > div > div.mt-3").innerHTML = "

    比赛排名

    "; + } + if (SearchParams.get("ByUserScript") == null) { + if (document.querySelector("body > div > div.mt-3 > center > h3").innerText == "比赛排名") { + document.querySelector("#rank").innerText = "比赛暂时还没有排名"; + } else { + document.querySelector("body > div > div.mt-3 > center > h3").innerText = document.querySelector("body > div > div.mt-3 > center > h3").innerText.substring(document.querySelector("body > div > div.mt-3 > center > h3").innerText.indexOf(" -- ") + 4) + "(OI排名)"; + document.querySelector("#rank > thead > tr > :nth-child(1)").innerText = "排名"; + document.querySelector("#rank > thead > tr > :nth-child(2)").innerText = "用户"; + document.querySelector("#rank > thead > tr > :nth-child(3)").innerText = "昵称"; + document.querySelector("#rank > thead > tr > :nth-child(4)").innerText = "AC数"; + document.querySelector("#rank > thead > tr > :nth-child(5)").innerText = "得分"; + let RefreshOIRank = async () => { + await fetch(location.href) + .then((Response) => { + return Response.text() + }) + .then(async (Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + TidyTable(ParsedDocument.getElementById("rank")); + let Temp = ParsedDocument.getElementById("rank").rows; + for (var i = 1; i < Temp.length; i++) { + let MetalCell = Temp[i].cells[0]; + let Metal = document.createElement("span"); + Metal.innerText = MetalCell.innerText; + Metal.className = "badge text-bg-primary"; + MetalCell.innerText = ""; + MetalCell.appendChild(Metal); + GetUsernameHTML(Temp[i].cells[1], Temp[i].cells[1].innerText); + Temp[i].cells[2].innerHTML = Temp[i].cells[2].innerText; + Temp[i].cells[3].innerHTML = Temp[i].cells[3].innerText; + for (let j = 5; j < Temp[i].cells.length; j++) { + let InnerText = Temp[i].cells[j].innerText; + let BackgroundColor = Temp[i].cells[j].style.backgroundColor; + let Red = BackgroundColor.substring(4, BackgroundColor.indexOf(",")); + let Green = BackgroundColor.substring(BackgroundColor.indexOf(",") + 2, BackgroundColor.lastIndexOf(",")); + let Blue = BackgroundColor.substring(BackgroundColor.lastIndexOf(",") + 2, BackgroundColor.lastIndexOf(")")); + let NoData = (Red == 238 && Green == 238 && Blue == 238); + let FirstBlood = (Red == 170 && Green == 170 && Blue == 255); + let Solved = (Green == 255); + let ErrorCount = ""; + if (Solved) { + ErrorCount = (Blue == 170 ? 5 : (Blue - 51) / 32); + } else { + ErrorCount = (Blue == 22 ? 15 : (170 - Blue) / 10); + } + if (NoData) { + BackgroundColor = ""; + } else if (FirstBlood) { + BackgroundColor = "rgb(127, 127, 255)"; + } else if (Solved) { + BackgroundColor = "rgb(0, 255, 0, " + Math.max(1 / 10 * (10 - ErrorCount), 0.2) + ")"; + if (ErrorCount != 0) { + InnerText += " (" + (ErrorCount == 5 ? "4+" : ErrorCount) + ")"; + } + } else { + BackgroundColor = "rgba(255, 0, 0, " + Math.min(ErrorCount / 10 + 0.2, 1) + ")"; + if (ErrorCount != 0) { + InnerText += " (" + (ErrorCount == 15 ? "14+" : ErrorCount) + ")"; + } + } + Temp[i].cells[j].innerHTML = InnerText; + Temp[i].cells[j].style.backgroundColor = BackgroundColor; + Temp[i].cells[j].style.color = (UtilityEnabled("DarkMode") ? "white" : "black"); + } + } + document.querySelector("#rank > tbody").innerHTML = ParsedDocument.querySelector("#rank > tbody").innerHTML; + }); + }; + RefreshOIRank(); + document.title = document.querySelector("body > div.container > div > center > h3").innerText; + if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", RefreshOIRank); + } + } + } + Style.innerHTML += "td {"; + Style.innerHTML += " white-space: nowrap;"; + Style.innerHTML += "}"; + document.querySelector("body > div.container > div > center").style.paddingBottom = "10px"; + document.querySelector("body > div.container > div > center > a").style.display = "none"; + document.title = document.querySelector("body > div.container > div > center > h3").innerText; + } else if (location.pathname == "/contestrank-correct.php") { + if (document.querySelector("#rank") == null) { + document.querySelector("body > div > div.mt-3").innerHTML = "

    比赛排名

    "; + } + if (document.querySelector("body > div > div.mt-3 > center > h3").innerText == "比赛排名") { + document.querySelector("#rank").innerText = "比赛暂时还没有排名"; + } else { + if (UtilityEnabled("ResetType")) { + document.querySelector("body > div > div.mt-3 > center > h3").innerText = document.querySelector("body > div > div.mt-3 > center > h3").innerText.substring(document.querySelector("body > div > div.mt-3 > center > h3").innerText.indexOf(" -- ") + 4) + "(订正排名)"; + document.querySelector("body > div > div.mt-3 > center > a").remove(); + } + document.querySelector("#rank > thead > tr > :nth-child(1)").innerText = "排名"; + document.querySelector("#rank > thead > tr > :nth-child(2)").innerText = "用户"; + document.querySelector("#rank > thead > tr > :nth-child(3)").innerText = "昵称"; + document.querySelector("#rank > thead > tr > :nth-child(4)").innerText = "AC数"; + document.querySelector("#rank > thead > tr > :nth-child(5)").innerText = "得分"; + let RefreshCorrectRank = async () => { + await fetch(location.href) + .then((Response) => { + return Response.text() + }) + .then(async (Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + TidyTable(ParsedDocument.getElementById("rank")); + let Temp = ParsedDocument.getElementById("rank").rows; + for (var i = 1; i < Temp.length; i++) { + let MetalCell = Temp[i].cells[0]; + let Metal = document.createElement("span"); + Metal.innerText = MetalCell.innerText; + Metal.className = "badge text-bg-primary"; + MetalCell.innerText = ""; + MetalCell.appendChild(Metal); + GetUsernameHTML(Temp[i].cells[1], Temp[i].cells[1].innerText); + Temp[i].cells[2].innerHTML = Temp[i].cells[2].innerText; + Temp[i].cells[3].innerHTML = Temp[i].cells[3].innerText; + for (let j = 5; j < Temp[i].cells.length; j++) { + let InnerText = Temp[i].cells[j].innerText; + let BackgroundColor = Temp[i].cells[j].style.backgroundColor; + let Red = BackgroundColor.substring(4, BackgroundColor.indexOf(",")); + let Green = BackgroundColor.substring(BackgroundColor.indexOf(",") + 2, BackgroundColor.lastIndexOf(",")); + let Blue = BackgroundColor.substring(BackgroundColor.lastIndexOf(",") + 2, BackgroundColor.lastIndexOf(")")); + let NoData = (Red == 238 && Green == 238 && Blue == 238); + let FirstBlood = (Red == 170 && Green == 170 && Blue == 255); + let Solved = (Green == 255); + let ErrorCount = ""; + if (Solved) { + ErrorCount = (Blue == 170 ? "4+" : (Blue - 51) / 32); + } else { + ErrorCount = (Blue == 22 ? "14+" : (170 - Blue) / 10); + } + if (NoData) { + BackgroundColor = ""; + } else if (FirstBlood) { + BackgroundColor = "rgba(127, 127, 255, 0.5)"; + } else if (Solved) { + BackgroundColor = "rgba(0, 255, 0, 0.5)"; + if (ErrorCount != 0) { + InnerText += " (" + ErrorCount + ")"; + } + } else { + BackgroundColor = "rgba(255, 0, 0, 0.5)"; + if (ErrorCount != 0) { + InnerText += " (" + ErrorCount + ")"; + } + } + Temp[i].cells[j].innerHTML = InnerText; + Temp[i].cells[j].style.backgroundColor = BackgroundColor; + } + } + document.querySelector("#rank > tbody").innerHTML = ParsedDocument.querySelector("#rank > tbody").innerHTML; + }); + }; + RefreshCorrectRank(); + document.title = document.querySelector("body > div.container > div > center > h3").innerText; + if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", RefreshCorrectRank); + } + } + } else if (location.pathname == "/submitpage.php") { + document.title = "提交代码: " + (SearchParams.get("id") != null ? "题目" + Number(SearchParams.get("id")) : "比赛" + Number(SearchParams.get("cid"))); + document.querySelector("body > div > div.mt-3").innerHTML = `
    ` + `

    提交代码

    ` + (SearchParams.get("id") != null ? `题目${Number(SearchParams.get("id"))}` : `比赛${Number(SearchParams.get("cid")) + ` 题目` + String.fromCharCode(65 + parseInt(SearchParams.get("pid")))}`) + `
    + +
    + +
    + + +
    `; + if (UtilityEnabled("AutoO2")) { + document.querySelector("#enable_O2").checked = true; + } + let CodeMirrorElement; + (() => { + CodeMirrorElement = CodeMirror.fromTextArea(document.querySelector("#CodeInput"), { + lineNumbers: true, + matchBrackets: true, + mode: "text/x-c++src", + indentUnit: 4, + indentWithTabs: true, + enterMode: "keep", + tabMode: "shift", + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + extraKeys: { + "Ctrl-Space": "autocomplete", "Ctrl-Enter": function (instance) { + Submit.click(); + } + } + }) + })(); + CodeMirrorElement.setSize("100%", "auto"); + CodeMirrorElement.getWrapperElement().style.border = "1px solid #ddd"; + + if (SearchParams.get("sid") !== null) { + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("sid")) + .then((Response) => { + return Response.text() + }) + .then((Response) => { + CodeMirrorElement.setValue(Response.substring(0, Response.indexOf("/**************************************************************")).trim()); + }); + } + + PassCheck.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + document.querySelector("#Submit").disabled = true; + document.querySelector("#Submit").value = "正在提交..."; + let o2Switch = "&enable_O2=on"; + if (!document.querySelector("#enable_O2").checked) o2Switch = ""; + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + }).then(async (Response) => { + if (Response.redirected) { + location.href = Response.url; + } else { + const text = await Response.text(); + if (text.indexOf("没有这个比赛!") !== -1 && new URL(location.href).searchParams.get("pid") !== null) { + // Credit: https://github.com/boomzero/quicksubmit/blob/main/index.ts + // Also licensed under GPL-3.0 + const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + new URL(location.href).searchParams.get("cid")); + const res = await contestReq.text(); + if ( + contestReq.status !== 200 || + res.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1 + ) { + console.error(`Failed to get contest page!`); + return; + } + const parser = new DOMParser(); + const dom = parser.parseFromString(res, "text/html"); + const contestProblems = []; + const rows = (dom.querySelector( + "#problemset > tbody", + )).rows; + for (let i = 0; i < rows.length; i++) { + contestProblems.push( + rows[i].children[1].textContent.substring(2, 6).replaceAll( + "\t", + "", + ), + ); + } + rPID = contestProblems[new URL(location.href).searchParams.get("pid")]; + if (UtilityEnabled("DebugMode")) { + console.log("Contest Problems:", contestProblems); + console.log("Real PID:", rPID); + } + ErrorElement.style.display = "block"; + ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "比赛已结束, 正在尝试像题目 " + rPID + " 提交"; + console.log("比赛已结束, 正在尝试像题目 " + rPID + " 提交"); + let o2Switch = "&enable_O2=on"; + if (!document.querySelector("#enable_O2").checked) o2Switch = ""; + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch + }).then(async (Response) => { + if (Response.redirected) { + location.href = Response.url; + } + console.log(await Response.text()); + }); + + } + if (UtilityEnabled("DebugMode")) { + console.log("Submission failed! Response:", text); + } + ErrorElement.style.display = "block"; + ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "提交失败!请关闭脚本后重试!"; + Submit.disabled = false; + Submit.value = "提交"; + } + }) + }); + + Submit.addEventListener("click", async () => { + PassCheck.style.display = "none"; + ErrorElement.style.display = "none"; + document.querySelector("#Submit").disabled = true; + document.querySelector("#Submit").value = "正在检查..."; + let Source = CodeMirrorElement.getValue(); + let PID = 0; + let IOFilename = ""; + if (SearchParams.get("cid") != null && SearchParams.get("pid") != null) { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID") + } else { + PID = SearchParams.get("id"); + } + IOFilename = localStorage.getItem("UserScript-Problem-" + PID + "-IOFilename"); + if (UtilityEnabled("IOFile") && IOFilename != null) { + if (Source.indexOf(IOFilename) == -1) { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "此题输入输出文件名为" + IOFilename + ",请检查是否填错"; + + let freopenText = document.createElement('small'); + if (UtilityEnabled("DarkMode")) freopenText.style.color = "white"; else freopenText.style.color = "black"; + freopenText.textContent = '\n您也可以复制freopen语句。\n'; + document.getElementById('ErrorMessage').appendChild(freopenText); + let copyFreopenButton = document.createElement("button"); + copyFreopenButton.className = "btn btn-sm btn-outline-secondary copy-btn"; + copyFreopenButton.innerText = "复制代码"; + copyFreopenButton.style.marginLeft = "10px"; + copyFreopenButton.style.marginTop = "10px"; + copyFreopenButton.style.marginBottom = "10px"; + copyFreopenButton.type = "button"; + copyFreopenButton.addEventListener("click", () => { + navigator.clipboard.writeText('\n freopen("' + IOFilename + '.in", "r", stdin);\n freopen("' + IOFilename + '.out", "w", stdout);'); + copyFreopenButton.innerText = "复制成功"; + setTimeout(() => { + copyFreopenButton.innerText = "复制代码"; + }, 1500); + }); + document.getElementById('ErrorMessage').appendChild(copyFreopenButton); + let freopenCodeField = CodeMirror(document.getElementById('ErrorMessage'), { + value: 'freopen("' + IOFilename + '.in", "r", stdin);\nfreopen("' + IOFilename + '.out", "w", stdout);', + mode: 'text/x-c++src', + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + readOnly: true, + lineNumbers: true + }); + freopenCodeField.setSize("100%", "auto"); + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } else if (RegExp("//.*freopen").test(Source)) { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "请不要注释freopen语句"; + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } + } + if (Source == "") { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "源代码为空"; + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } + if (UtilityEnabled("CompileError")) { + let ResponseData = await new Promise((Resolve) => { + GM_xmlhttpRequest({ + method: "POST", url: "https://cppinsights.io/api/v1/transform", headers: { + "content-type": "application/json;charset=UTF-8" + }, referrer: "https://cppinsights.io/", data: JSON.stringify({ + "insightsOptions": ["cpp14"], "code": Source + }), onload: (Response) => { + Resolve(Response); + } + }); + }); + let Response = JSON.parse(ResponseData.responseText); + if (Response.returncode) { + PassCheck.style.display = ""; + ErrorElement.style.display = "block"; + if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; + ErrorMessage.innerText = "编译错误:\n" + Response.stderr.trim(); + document.querySelector("#Submit").disabled = false; + document.querySelector("#Submit").value = "提交"; + return false; + } else { + PassCheck.click(); + } + } else { + PassCheck.click(); + } + }); + } else if (location.pathname == "/modifypage.php") { + if (SearchParams.get("ByUserScript") != null) { + document.title = "XMOJ-Script 更新日志"; + document.querySelector("body > div > div.mt-3").innerHTML = ""; + await fetch(ServerURL + "/Update.json", {cache: "no-cache"}) + .then((Response) => { + return Response.json(); + }) + .then((Response) => { + for (let i = Object.keys(Response.UpdateHistory).length - 1; i >= 0; i--) { + let Version = Object.keys(Response.UpdateHistory)[i]; + let Data = Response.UpdateHistory[Version]; + let UpdateDataCard = document.createElement("div"); + document.querySelector("body > div > div.mt-3").appendChild(UpdateDataCard); + UpdateDataCard.className = "card mb-3"; + if (Data.Prerelease) UpdateDataCard.classList.add("text-secondary"); + let UpdateDataCardBody = document.createElement("div"); + UpdateDataCard.appendChild(UpdateDataCardBody); + UpdateDataCardBody.className = "card-body"; + let UpdateDataCardTitle = document.createElement("h5"); + UpdateDataCardBody.appendChild(UpdateDataCardTitle); + UpdateDataCardTitle.className = "card-title"; + UpdateDataCardTitle.innerText = Version; + if (Data.Prerelease) { + UpdateDataCardTitle.innerHTML += "(预览版)"; + } + let UpdateDataCardSubtitle = document.createElement("h6"); + UpdateDataCardBody.appendChild(UpdateDataCardSubtitle); + UpdateDataCardSubtitle.className = "card-subtitle mb-2 text-muted"; + UpdateDataCardSubtitle.innerHTML = GetRelativeTime(Data.UpdateDate); + let UpdateDataCardText = document.createElement("p"); + UpdateDataCardBody.appendChild(UpdateDataCardText); + UpdateDataCardText.className = "card-text"; + //release notes + if (Data.Notes != undefined) { + UpdateDataCardText.innerHTML = Data.Notes; + } + let UpdateDataCardList = document.createElement("ul"); + UpdateDataCardText.appendChild(UpdateDataCardList); + UpdateDataCardList.className = "list-group list-group-flush"; + for (let j = 0; j < Data.UpdateContents.length; j++) { + let UpdateDataCardListItem = document.createElement("li"); + UpdateDataCardList.appendChild(UpdateDataCardListItem); + UpdateDataCardListItem.className = "list-group-item"; + UpdateDataCardListItem.innerHTML = "(" + "#" + Data.UpdateContents[j].PR + ") " + Data.UpdateContents[j].Description; + } + let UpdateDataCardLink = document.createElement("a"); + UpdateDataCardBody.appendChild(UpdateDataCardLink); + UpdateDataCardLink.className = "card-link"; + UpdateDataCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script/releases/tag/" + Version; + UpdateDataCardLink.target = "_blank"; + UpdateDataCardLink.innerText = "查看该版本"; + } + }); + } else { + document.title = "修改账号"; + let Nickname = document.getElementsByName("nick")[0].value; + let School = document.getElementsByName("school")[0].value; + let EmailAddress = document.getElementsByName("email")[0].value; + let CodeforcesAccount = document.getElementsByName("acc_cf")[0].value; + let AtcoderAccount = document.getElementsByName("acc_atc")[0].value; + let USACOAccount = document.getElementsByName("acc_usaco")[0].value; + let LuoguAccount = document.getElementsByName("acc_luogu")[0].value; + document.querySelector("body > div > div").innerHTML = `
    +
    +
    +
    +
    +
    +
    + + 修改头像 +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    `; + document.getElementById("Nickname").value = Nickname; + document.getElementById("School").value = School; + document.getElementById("EmailAddress").value = EmailAddress; + document.getElementById("CodeforcesAccount").value = CodeforcesAccount; + document.getElementById("AtcoderAccount").value = AtcoderAccount; + document.getElementById("USACOAccount").value = USACOAccount; + document.getElementById("LuoguAccount").value = LuoguAccount; + RequestAPI("GetBadge", { + "UserID": String(CurrentUsername) + }, (Response) => { + if (Response.Success) { + BadgeRow.style.display = ""; + BadgeContent.value = Response.Data.Content; + BadgeBackgroundColor.value = Response.Data.BackgroundColor; + BadgeColor.value = Response.Data.Color; + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-" + CurrentUsername + "-Badge-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + } + }); + ModifyInfo.addEventListener("click", async () => { + ModifyInfo.disabled = true; + ModifyInfo.querySelector("span").style.display = ""; + ErrorElement.style.display = "none"; + SuccessElement.style.display = "none"; + let BadgeContent = document.querySelector("#BadgeContent").value; + let BadgeBackgroundColor = document.querySelector("#BadgeBackgroundColor").value; + let BadgeColor = document.querySelector("#BadgeColor").value; + await new Promise((Resolve) => { + RequestAPI("EditBadge", { + "UserID": String(CurrentUsername), + "Content": String(BadgeContent), + "BackgroundColor": String(BadgeBackgroundColor), + "Color": String(BadgeColor) + }, (Response) => { + if (Response.Success) { + Resolve(); + } else { + ModifyInfo.disabled = false; + ModifyInfo.querySelector("span").style.display = "none"; + ErrorElement.style.display = "block"; + ErrorElement.innerText = Response.Message; + } + }); + }); + let Nickname = document.querySelector("#Nickname").value; + let OldPassword = document.querySelector("#OldPassword").value; + let NewPassword = document.querySelector("#NewPassword").value; + let NewPasswordAgain = document.querySelector("#NewPasswordAgain").value; + let School = document.querySelector("#School").value; + let EmailAddress = document.querySelector("#EmailAddress").value; + let CodeforcesAccount = document.querySelector("#CodeforcesAccount").value; + let AtcoderAccount = document.querySelector("#AtcoderAccount").value; + let USACOAccount = document.querySelector("#USACOAccount").value; + let LuoguAccount = document.querySelector("#LuoguAccount").value; + await fetch("https://www.xmoj.tech/modify.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": location.href, + "method": "POST", + "body": "nick=" + encodeURIComponent(Nickname) + "&" + "opassword=" + encodeURIComponent(OldPassword) + "&" + "npassword=" + encodeURIComponent(NewPassword) + "&" + "rptpassword=" + encodeURIComponent(NewPasswordAgain) + "&" + "school=" + encodeURIComponent(School) + "&" + "email=" + encodeURIComponent(EmailAddress) + "&" + "acc_cf=" + encodeURIComponent(CodeforcesAccount) + "&" + "acc_atc=" + encodeURIComponent(AtcoderAccount) + "&" + "acc_usaco=" + encodeURIComponent(USACOAccount) + "&" + "acc_luogu=" + encodeURIComponent(LuoguAccount) + }); + ModifyInfo.disabled = false; + ModifyInfo.querySelector("span").style.display = "none"; + SuccessElement.style.display = "block"; + }); + if (UtilityEnabled("ExportACCode")) { + let ExportACCode = document.createElement("button"); + document.querySelector("body > div.container > div").appendChild(ExportACCode); + ExportACCode.innerText = "导出AC代码"; + ExportACCode.className = "btn btn-outline-secondary"; + ExportACCode.addEventListener("click", () => { + ExportACCode.disabled = true; + ExportACCode.innerText = "正在导出..."; + let Request = new XMLHttpRequest(); + Request.addEventListener("readystatechange", () => { + if (Request.readyState == 4) { + if (Request.status == 200) { + let Response = Request.responseText; + let ACCode = Response.split("------------------------------------------------------\r\n"); + let ScriptElement = document.createElement("script"); + ScriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; + document.head.appendChild(ScriptElement); + ScriptElement.onload = () => { + var Zip = new JSZip(); + for (let i = 0; i < ACCode.length; i++) { + let CurrentCode = ACCode[i]; + if (CurrentCode != "") { + let CurrentQuestionID = CurrentCode.substring(7, 11); + CurrentCode = CurrentCode.substring(14); + CurrentCode = CurrentCode.replaceAll("\r", ""); + Zip.file(CurrentQuestionID + ".cpp", CurrentCode); + } + } + ExportACCode.innerText = "正在生成压缩包……"; + Zip.generateAsync({type: "blob"}) + .then(function (Content) { + saveAs(Content, "ACCodes.zip"); + ExportACCode.innerText = "AC代码导出成功"; + ExportACCode.disabled = false; + setTimeout(() => { + ExportACCode.innerText = "导出AC代码"; + }, 1000); + }); + }; + } else { + ExportACCode.disabled = false; + ExportACCode.innerText = "AC代码导出失败"; + setTimeout(() => { + ExportACCode.innerText = "导出AC代码"; + }, 1000); + } + } + }); + Request.open("GET", "https://www.xmoj.tech/export_ac_code.php", true); + Request.send(); + }); + } + } + } else if (location.pathname == "/userinfo.php") { + if (SearchParams.get("ByUserScript") === null) { + if (UtilityEnabled("RemoveUseless")) { + let Temp = document.getElementById("submission").childNodes; + for (let i = 0; i < Temp.length; i++) { + Temp[i].remove(); + } + } + eval(document.querySelector("body > script:nth-child(5)").innerHTML); + document.querySelector("#statics > tbody > tr:nth-child(1)").remove(); + + let Temp = document.querySelector("#statics > tbody").children; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].children[0] != undefined) { + if (Temp[i].children[0].innerText == "Statistics") { + Temp[i].children[0].innerText = "统计"; + } else if (Temp[i].children[0].innerText == "Email:") { + Temp[i].children[0].innerText = "电子邮箱"; + } + Temp[i].children[1].removeAttribute("align"); + } + } + + Temp = document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)").childNodes; + let ACProblems = []; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].tagName == "A" && Temp[i].href.indexOf("problem.php?id=") != -1) { + ACProblems.push(Number(Temp[i].innerText.trim())); + } + } + document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)").remove(); + + let UserID, UserNick; + [UserID, UserNick] = document.querySelector("#statics > caption").childNodes[0].data.trim().split("--"); + document.querySelector("#statics > caption").remove(); + document.title = "用户 " + UserID + " 的个人中心"; + let Row = document.createElement("div"); + Row.className = "row"; + let LeftDiv = document.createElement("div"); + LeftDiv.className = "col-md-5"; + Row.appendChild(LeftDiv); + + let LeftTopDiv = document.createElement("div"); + LeftTopDiv.className = "row mb-2"; + LeftDiv.appendChild(LeftTopDiv); + let AvatarContainer = document.createElement("div"); + AvatarContainer.classList.add("col-auto"); + let AvatarElement = document.createElement("img"); + let UserEmailHash = (await GetUserInfo(UserID)).EmailHash; + if (UserEmailHash == undefined) { + AvatarElement.src = `https://cravatar.cn/avatar/00000000000000000000000000000000?d=mp&f=y`; + } else { + AvatarElement.src = `https://cravatar.cn/avatar/${UserEmailHash}?d=retro`; + } + AvatarElement.classList.add("rounded", "me-2"); + AvatarElement.style.height = "120px"; + AvatarContainer.appendChild(AvatarElement); + LeftTopDiv.appendChild(AvatarContainer); + + let UserInfoElement = document.createElement("div"); + UserInfoElement.classList.add("col-auto"); + UserInfoElement.style.lineHeight = "40px"; + UserInfoElement.innerHTML += "用户名:" + UserID + "
    "; + UserInfoElement.innerHTML += "昵称:" + UserNick + "
    "; + if (UtilityEnabled("Rating")) { + UserInfoElement.innerHTML += "评分:" + ((await GetUserInfo(UserID)).Rating) + "
    "; + } + // Create a placeholder for the last online time + let lastOnlineElement = document.createElement('div'); + lastOnlineElement.innerHTML = "最后在线:加载中...
    "; + UserInfoElement.appendChild(lastOnlineElement); + let BadgeInfo = await GetUserBadge(UserID); + if (IsAdmin) { + if (BadgeInfo.Content !== "") { + let DeleteBadgeButton = document.createElement("button"); + DeleteBadgeButton.className = "btn btn-outline-danger btn-sm"; + DeleteBadgeButton.innerText = "删除标签"; + DeleteBadgeButton.addEventListener("click", async () => { + if (confirm("您确定要删除此标签吗?")) { + RequestAPI("DeleteBadge", { + "UserID": UserID + }, (Response) => { + if (UtilityEnabled("DebugMode")) console.log(Response); + if (Response.Success) { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-" + UserID + "-Badge-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + window.location.reload(); + } else { + SmartAlert(Response.Message); + } + }); + } + }); + UserInfoElement.appendChild(DeleteBadgeButton); + } else { + let AddBadgeButton = document.createElement("button"); + AddBadgeButton.className = "btn btn-outline-primary btn-sm"; + AddBadgeButton.innerText = "添加标签"; + AddBadgeButton.addEventListener("click", async () => { + RequestAPI("NewBadge", { + "UserID": UserID + }, (Response) => { + if (Response.Success) { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-" + UserID + "-Badge-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + window.location.reload(); + } else { + SmartAlert(Response.Message); + } + }); + }); + UserInfoElement.appendChild(AddBadgeButton); + } + } + RequestAPI("LastOnline", {"Username": UserID}, (result) => { + if (result.Success) { + if (UtilityEnabled("DebugMode")) { + console.log('lastOnline:' + result.Data.logintime); + } + lastOnlineElement.innerHTML = "最后在线:" + GetRelativeTime(result.Data.logintime) + "
    "; + } else { + lastOnlineElement.innerHTML = "最后在线:近三个月内从未
    "; + } + }); + LeftTopDiv.appendChild(UserInfoElement); + LeftDiv.appendChild(LeftTopDiv); + + let LeftTable = document.querySelector("body > div > div > center > table"); + LeftDiv.appendChild(LeftTable); + let RightDiv = document.createElement("div"); + RightDiv.className = "col-md-7"; + Row.appendChild(RightDiv); + RightDiv.innerHTML = "
    已解决题目
    "; + for (let i = 0; i < ACProblems.length; i++) { + RightDiv.innerHTML += "" + ACProblems[i] + " "; + } + document.querySelector("body > div > div").innerHTML = ""; + document.querySelector("body > div > div").appendChild(Row); + } else { + document.title = "上传标程"; + document.querySelector("body > div > div.mt-3").innerHTML = ` + +
    +
    0%
    +
    +

    + 您必须要上传标程以后才能使用“查看标程”功能。点击“上传标程”按钮以后,系统会自动上传标程,请您耐心等待。
    + 首次上传标程可能会比较慢,请耐心等待。后续将可以自动上传AC代码。
    + 系统每过30天会自动提醒您上传标程,您必须要上传标程,否则将会被禁止使用“查看标程”功能。
    +

    `; + UploadStd.addEventListener("click", async () => { + UploadStd.disabled = true; + ErrorElement.style.display = "none"; + ErrorElement.innerText = ""; + UploadProgress.classList.remove("bg-success"); + UploadProgress.classList.remove("bg-warning"); + UploadProgress.classList.remove("bg-danger"); + UploadProgress.classList.add("progress-bar-animated"); + UploadProgress.style.width = "0%"; + UploadProgress.innerText = "0%"; + let ACList = []; + await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let ScriptData = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText; + ScriptData = ScriptData.substr(ScriptData.indexOf("}") + 1).trim(); + ScriptData = ScriptData.split(";"); + for (let i = 0; i < ScriptData.length; i++) { + ACList.push(Number(ScriptData[i].substring(2, ScriptData[i].indexOf(",")))); + } + }); + RequestAPI("GetStdList", {}, async (Result) => { + if (Result.Success) { + let StdList = Result.Data.StdList; + for (let i = 0; i < ACList.length; i++) { + if (StdList.indexOf(ACList[i]) === -1 && ACList[i] !== 0) { + await new Promise((Resolve) => { + RequestAPI("UploadStd", { + "ProblemID": Number(ACList[i]) + }, (Result) => { + if (!Result.Success) { + ErrorElement.style.display = "block"; + ErrorElement.innerText += Result.Message + "\n"; + UploadProgress.classList.add("bg-warning"); + } + UploadProgress.innerText = (i / ACList.length * 100).toFixed(1) + "% (" + ACList[i] + ")"; + UploadProgress.style.width = (i / ACList.length * 100) + "%"; + Resolve(); + }); + }); + } + } + UploadProgress.classList.add("bg-success"); + UploadProgress.classList.remove("progress-bar-animated"); + UploadProgress.innerText = "100%"; + UploadProgress.style.width = "100%"; + UploadStd.disabled = false; + localStorage.setItem("UserScript-LastUploadedStdTime", new Date().getTime()); + } else { + ErrorElement.style.display = "block"; + ErrorElement.innerText = Result.Message; + UploadStd.disabled = false; + } + }); + }); + } + } else if (location.pathname == "/comparesource.php") { + if (UtilityEnabled("CompareSource")) { + if (location.search == "") { + document.querySelector("body > div.container > div").innerHTML = ""; + let LeftCodeText = document.createElement("span"); + document.querySelector("body > div.container > div").appendChild(LeftCodeText); + LeftCodeText.innerText = "左侧代码的运行编号:"; + let LeftCode = document.createElement("input"); + document.querySelector("body > div.container > div").appendChild(LeftCode); + LeftCode.classList.add("form-control"); + LeftCode.style.width = "40%"; + LeftCode.style.marginBottom = "5px"; + let RightCodeText = document.createElement("span"); + document.querySelector("body > div.container > div").appendChild(RightCodeText); + RightCodeText.innerText = "右侧代码的运行编号:"; + let RightCode = document.createElement("input"); + document.querySelector("body > div.container > div").appendChild(RightCode); + RightCode.classList.add("form-control"); + RightCode.style.width = "40%"; + RightCode.style.marginBottom = "5px"; + let CompareButton = document.createElement("button"); + document.querySelector("body > div.container > div").appendChild(CompareButton); + CompareButton.innerText = "比较"; + CompareButton.className = "btn btn-primary"; + CompareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php?left=" + Number(LeftCode.value) + "&right=" + Number(RightCode.value); + }); + } else { + document.querySelector("body > div > div.mt-3").innerHTML = ` +
    + + +
    +
    `; + + let LeftCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("left")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + LeftCode = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + let RightCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("right")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + RightCode = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + + let MergeViewElement = CodeMirror.MergeView(CompareElement, { + value: LeftCode, + origLeft: null, + orig: RightCode, + lineNumbers: true, + mode: "text/x-c++src", + collapseIdentical: "true", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + revertButtons: false, + ignoreWhitespace: true + }); + + IgnoreWhitespace.addEventListener("change", () => { + MergeViewElement.ignoreWhitespace = ignorews.checked; + }); + } + } + } else if (location.pathname == "/loginpage.php") { + if (UtilityEnabled("NewBootstrap")) { + document.querySelector("#login").innerHTML = `
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    `; + } + let ErrorText = document.createElement("div"); + ErrorText.style.color = "red"; + ErrorText.style.marginBottom = "5px"; + document.querySelector("#login").appendChild(ErrorText); + let LoginButton = document.getElementsByName("submit")[0]; + LoginButton.addEventListener("click", async () => { + let Username = document.getElementsByName("user_id")[0].value; + let Password = document.getElementsByName("password")[0].value; + if (Username == "" || Password == "") { + ErrorText.innerText = "用户名或密码不能为空"; + } else { + await fetch("https://www.xmoj.tech/login.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: "user_id=" + encodeURIComponent(Username) + "&password=" + hex_md5(Password) + }) + .then((Response) => { + return Response.text(); + }) + .then(async (Response) => { + if (UtilityEnabled("LoginFailed")) { + if (Response.indexOf("history.go(-2);") != -1) { + if (UtilityEnabled("SavePassword")) { + await storeCredential(Username, Password); + } + let NewPage = localStorage.getItem("UserScript-LastPage"); + if (NewPage == null) { + NewPage = "https://www.xmoj.tech/index.php"; + } + location.href = NewPage; + } else { + if (UtilityEnabled("SavePassword")) { + clearCredential(); + } + Response = Response.substring(Response.indexOf("alert('") + 7); + Response = Response.substring(0, Response.indexOf("');")); + if (Response == "UserName or Password Wrong!") { + ErrorText.innerText = "用户名或密码错误!"; + } else { + ErrorText.innerText = Response; + } + } + } else { + document.innerHTML = Response; + } + }); + } + }); + if (UtilityEnabled("SavePassword")) { + (async () => { + let Credential = await getCredential(); + if (Credential) { + document.querySelector("#login > div:nth-child(1) > div > input").value = Credential.id; + document.querySelector("#login > div:nth-child(2) > div > input").value = Credential.password; + LoginButton.click(); + } + })(); + } + } else if (location.pathname == "/contest_video.php" || location.pathname == "/problem_video.php") { + let ScriptData = document.querySelector("body > div > div.mt-3 > center > script").innerHTML; + if (document.getElementById("J_prismPlayer0").innerHTML != "") { + document.getElementById("J_prismPlayer0").innerHTML = ""; + if (player) { + player.dispose(); + } + eval(ScriptData); + } + if (UtilityEnabled("DownloadPlayback")) { + ScriptData = ScriptData.substring(ScriptData.indexOf("{")); + ScriptData = ScriptData.substring(0, ScriptData.indexOf("}") + 1); + ScriptData = ScriptData.replace(/([a-zA-Z0-9]+) ?:/g, "\"$1\":"); + ScriptData = ScriptData.replace(/'/g, "\""); + let VideoData = JSON.parse(ScriptData); + let RandomUUID = () => { + let t = "0123456789abcdef"; + let e = []; + for (let r = 0; r < 36; r++) e[r] = t.substr(Math.floor(16 * Math.random()), 1); + e[14] = "4"; + e[19] = t.substr(3 & e[19] | 8, 1); + e[8] = e[13] = e[18] = e[23] = "-"; + return e.join(""); + }; + let URLParams = new URLSearchParams({ + "AccessKeyId": VideoData.accessKeyId, + "Action": "GetPlayInfo", + "VideoId": VideoData.vid, + "Formats": "", + "AuthTimeout": 7200, + "Rand": RandomUUID(), + "SecurityToken": VideoData.securityToken, + "StreamType": "video", + "Format": "JSON", + "Version": "2017-03-21", + "SignatureMethod": "HMAC-SHA1", + "SignatureVersion": "1.0", + "SignatureNonce": RandomUUID(), + "PlayerVersion": "2.9.3", + "Channel": "HTML5" + }); + URLParams.sort(); + await fetch("https://vod." + VideoData.region + ".aliyuncs.com/?" + URLParams.toString() + "&Signature=" + encodeURIComponent(CryptoJS.HmacSHA1("GET&%2F&" + encodeURIComponent(URLParams.toString()), VideoData.accessKeySecret + "&").toString(CryptoJS.enc.Base64))) + .then((Response) => { + return Response.json(); + }) + .then((Response) => { + let DownloadButton = document.createElement("a"); + DownloadButton.className = "btn btn-outline-secondary"; + DownloadButton.innerText = "下载"; + DownloadButton.href = Response.PlayInfoList.PlayInfo[0].PlayURL; + DownloadButton.download = Response.VideoBase.Title; + document.querySelector("body > div > div.mt-3 > center").appendChild(DownloadButton); + }); + } + } else if (location.pathname == "/reinfo.php") { + document.title = "测试点信息: " + SearchParams.get("sid"); + if (document.querySelector("#results > div") == undefined) { + document.querySelector("#results").parentElement.innerHTML = "没有测试点信息"; + } else { + for (let i = 0; i < document.querySelector("#results > div").children.length; i++) { + let CurrentElement = document.querySelector("#results > div").children[i].children[0].children[0].children[0]; + let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/"); + CurrentElement.innerText = TimeToStringTime(Temp[0]) + "/" + SizeToStringSize(Temp[1]); + } + if (document.getElementById("apply_data")) { + let ApplyDiv = document.getElementById("apply_data").parentElement; + console.log("启动!!!"); + if (UtilityEnabled("ApplyData")) { + let GetDataButton = document.createElement("button"); + GetDataButton.className = "ms-2 btn btn-outline-secondary"; + GetDataButton.innerText = "获取数据"; + console.log("按钮创建成功"); + ApplyDiv.appendChild(GetDataButton); + GetDataButton.addEventListener("click", async () => { + GetDataButton.disabled = true; + GetDataButton.innerText = "正在获取数据..."; + let PID = localStorage.getItem("UserScript-Solution-" + SearchParams.get("sid") + "-Problem"); + if (PID == null) { + GetDataButton.innerText = "失败! 无法获取PID"; + GetDataButton.disabled = false; + await new Promise((resolve) => { + setTimeout(resolve, 800); + }); + GetDataButton.innerText = "获取数据"; + return; + } + let Code = ""; + if (localStorage.getItem(`UserScript-Problem-${PID}-IOFilename`) !== null) { + Code = `#define IOFile "${localStorage.getItem(`UserScript-Problem-${PID}-IOFilename`)}"\n`; + } + Code += `//XMOJ-Script 获取数据代码 + #include +using namespace std; +string Base64Encode(string Input) +{ + const string Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + string Output; + for (int i = 0; i < Input.length(); i += 3) + { + Output.push_back(i + 0 > Input.length() ? '=' : Base64Chars[(Input[i + 0] & 0xfc) >> 2]); + Output.push_back(i + 1 > Input.length() ? '=' : Base64Chars[((Input[i + 0] & 0x03) << 4) + ((Input[i + 1] & 0xf0) >> 4)]); + Output.push_back(i + 2 > Input.length() ? '=' : Base64Chars[((Input[i + 1] & 0x0f) << 2) + ((Input[i + 2] & 0xc0) >> 6)]); + Output.push_back(i + 3 > Input.length() ? '=' : Base64Chars[Input[i + 2] & 0x3f]); + } + return Output; +} +int main() +{ +#ifdef IOFile + freopen(IOFile ".in", "r", stdin); + freopen(IOFile ".out", "w", stdout); +#endif + string Input; + while (1) + { + char Data = getchar(); + if (Data == EOF) + break; + Input.push_back(Data); + } + throw logic_error("[" + Base64Encode(Input.c_str()) + "]"); + return 0; +}`; + + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, + "method": "POST", + "body": "id=" + PID + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" + }); + + let SID = await fetch("https://www.xmoj.tech/status.php").then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + return ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + + await new Promise((Resolve) => { + let Interval = setInterval(async () => { + await fetch("status-ajax.php?solution_id=" + SID).then((Response) => { + return Response.text(); + }).then((Response) => { + if (Response.split(",")[0] >= 4) { + clearInterval(Interval); + Resolve(); + } + }); + }, 500); + }); + + await fetch(`https://www.xmoj.tech/reinfo.php?sid=${SID}`).then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let ErrorData = ParsedDocument.getElementById("errtxt").innerText; + let MatchResult = ErrorData.match(/\what\(\): \[([A-Za-z0-9+\/=]+)\]/g); + if (MatchResult === null) { + GetDataButton.innerText = "获取数据失败"; + GetDataButton.disabled = false; + return; + } + for (let i = 0; i < MatchResult.length; i++) { + let Data = CryptoJS.enc.Base64.parse(MatchResult[i].substring(10, MatchResult[i].length - 1)).toString(CryptoJS.enc.Utf8); + ApplyDiv.appendChild(document.createElement("hr")); + ApplyDiv.appendChild(document.createTextNode("数据" + (i + 1) + ":")); + let CodeElement = document.createElement("div"); + ApplyDiv.appendChild(CodeElement); + CodeMirror(CodeElement, { + value: Data, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + lineNumbers: true, + readOnly: true + }).setSize("100%", "auto"); + } + GetDataButton.innerText = "获取数据成功"; + GetDataButton.disabled = false; + }); + }); + } + document.getElementById("apply_data").addEventListener("click", () => { + let ApplyElements = document.getElementsByClassName("data"); + for (let i = 0; i < ApplyElements.length; i++) { + ApplyElements[i].style.display = (ApplyElements[i].style.display == "block" ? "" : "block"); + } + }); + } + let ApplyElements = document.getElementsByClassName("data"); + for (let i = 0; i < ApplyElements.length; i++) { + ApplyElements[i].addEventListener("click", async () => { + await fetch("https://www.xmoj.tech/data_distribute_ajax_apply.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: "user_id=" + CurrentUsername + "&" + "solution_id=" + SearchParams.get("sid") + "&" + "name=" + ApplyElements[i].getAttribute("name") + }).then((Response) => { + return Response.json(); + }).then((Response) => { + ApplyElements[i].innerText = Response.msg; + setTimeout(() => { + ApplyElements[i].innerText = "申请数据"; + }, 1000); + }); + }); + } + } + } else if (location.pathname == "/downloads.php") { + let SoftwareList = document.querySelector("body > div > ul"); + SoftwareList.remove(); + SoftwareList = document.createElement("ul"); + SoftwareList.className = "software_list"; + let Container = document.createElement("div"); + document.querySelector("body > div").appendChild(Container); + Container.className = "mt-3"; + Container.appendChild(SoftwareList); + if (UtilityEnabled("NewDownload")) { + let Softwares = [{ + "Name": "Bloodshed Dev-C++", + "Image": "https://a.fsdn.com/allura/p/dev-cpp/icon", + "URL": "https://sourceforge.net/projects/dev-cpp/" + }, { + "Name": "DevC++ 5.11 TDM-GCC 4.9.2", + "Image": "https://www.xmoj.tech/image/devcpp.png", + "URL": "https://www.xmoj.tech/downloads/Dev-Cpp+5.11+TDM-GCC+4.9.2+Setup.exe" + }, { + "Name": "Orwell Dev-C++", + "Image": "https://a.fsdn.com/allura/p/orwelldevcpp/icon", + "URL": "https://sourceforge.net/projects/orwelldevcpp/" + }, { + "Name": "Embarcadero Dev-C++", + "Image": "https://a.fsdn.com/allura/s/embarcadero-dev-cpp/icon", + "URL": "https://sourceforge.net/software/product/Embarcadero-Dev-Cpp/" + }, { + "Name": "RedPanda C++", + "Image": "https://a.fsdn.com/allura/p/redpanda-cpp/icon", + "URL": "https://sourceforge.net/projects/redpanda-cpp/" + }, { + "Name": "CP Editor", + "Image": "https://a.fsdn.com/allura/mirror/cp-editor/icon?c35437565079e4135a985ba557ef2fdbe97de6bafb27aceafd76bc54490c26e3?&w=90", + "URL": "https://cpeditor.org/zh/download/" + }, { + "Name": "CLion", + "Image": "https://resources.jetbrains.com/storage/products/company/brand/logos/CLion_icon.png", + "URL": "https://www.jetbrains.com/clion/download" + }, { + "Name": "CP Editor", + "Image": "https://a.fsdn.com/allura/mirror/cp-editor/icon", + "URL": "https://sourceforge.net/projects/cp-editor.mirror/" + }, { + "Name": "Code::Blocks", + "Image": "https://a.fsdn.com/allura/p/codeblocks/icon", + "URL": "https://sourceforge.net/projects/codeblocks/" + }, { + "Name": "Visual Studio Code", + "Image": "https://code.visualstudio.com/favicon.ico", + "URL": "https://code.visualstudio.com/Download" + }, { + "Name": "Lazarus", + "Image": "https://a.fsdn.com/allura/p/lazarus/icon", + "URL": "https://sourceforge.net/projects/lazarus/" + }, { + "Name": "Geany", + "Image": "https://www.geany.org/static/img/geany.svg", + "URL": "https://www.geany.org/download/releases/" + }, { + "Name": "NOI Linux", + "Image": "https://www.noi.cn/upload/resources/image/2021/07/16/163780.jpg", + "URL": "https://www.noi.cn/gynoi/jsgz/2021-07-16/732450.shtml" + }, { + "Name": "VirtualBox", + "Image": "https://www.virtualbox.org/graphics/vbox_logo2_gradient.png", + "URL": "https://www.virtualbox.org/wiki/Downloads" + }, { + "Name": "MinGW", + "Image": "https://www.mingw-w64.org/logo.svg", + "URL": "https://sourceforge.net/projects/mingw/" + }]; + for (let i = 0; i < Softwares.length; i++) { + SoftwareList.innerHTML += "
  • " + "" + "
    " + "
    " + "\"点击下载\"" + "
    " + "
    " + Softwares[i].Name + "
    " + "
    " + "
    " + "
  • "; + } + } + } else if (location.pathname == "/problemstatus.php") { + document.querySelector("body > div > div.mt-3 > center").insertBefore(document.querySelector("#statics"), document.querySelector("body > div > div.mt-3 > center > table")); + document.querySelector("body > div > div.mt-3 > center").insertBefore(document.querySelector("#problemstatus"), document.querySelector("body > div > div.mt-3 > center > table")); + + document.querySelector("body > div > div.mt-3 > center > table:nth-child(3)").remove(); + let Temp = document.querySelector("#statics").rows; + for (let i = 0; i < Temp.length; i++) { + Temp[i].removeAttribute("class"); + } + + document.querySelector("#problemstatus > thead > tr").innerHTML = document.querySelector("#problemstatus > thead > tr").innerHTML.replaceAll("td", "th"); + document.querySelector("#problemstatus > thead > tr > th:nth-child(2)").innerText = "运行编号"; + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); + Temp = document.querySelector("#problemstatus > thead > tr").children; + for (let i = 0; i < Temp.length; i++) { + Temp[i].removeAttribute("class"); + } + Temp = document.querySelector("#problemstatus > tbody").children; + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].children[5].children[0] != null) { + Temp[i].children[1].innerHTML = `${escapeHTML(Temp[i].children[1].innerText.trim())}`; + } + GetUsernameHTML(Temp[i].children[2], Temp[i].children[2].innerText); + Temp[i].children[3].remove(); + Temp[i].children[3].remove(); + Temp[i].children[3].remove(); + Temp[i].children[3].remove(); + } + + + let CurrentPage = parseInt(SearchParams.get("page") || 0); + let PID = Number(SearchParams.get("id")); + document.title = "问题 " + PID + " 状态"; + let Pagination = ``; + document.querySelector("body > div > div.mt-3 > center").innerHTML += Pagination; + } else if (location.pathname == "/problem_solution.php") { + if (UtilityEnabled("RemoveUseless")) { + document.querySelector("h2.lang_en").remove(); //fixes #332 + } + if (UtilityEnabled("CopyMD")) { + await fetch(location.href).then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let CopyMDButton = document.createElement("button"); + CopyMDButton.className = "btn btn-sm btn-outline-secondary copy-btn"; + CopyMDButton.innerText = "复制"; + CopyMDButton.style.marginLeft = "10px"; + CopyMDButton.type = "button"; + document.querySelector("body > div > div.mt-3 > center > h2").appendChild(CopyMDButton); + CopyMDButton.addEventListener("click", () => { + GM_setClipboard(ParsedDocument.querySelector("body > div > div > div").innerText.trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); + CopyMDButton.innerText = "复制成功"; + setTimeout(() => { + CopyMDButton.innerText = "复制"; + }, 1000); + }); + }); + } + let Temp = document.getElementsByClassName("prettyprint"); + for (let i = 0; i < Temp.length; i++) { + let Code = Temp[i].innerText; + Temp[i].outerHTML = ``; + Temp[i].value = Code; + } + for (let i = 0; i < Temp.length; i++) { + CodeMirror.fromTextArea(Temp[i], { + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + } + } else if (location.pathname == "/open_contest.php") { + let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; + let NewsData = []; + for (let i = 0; i < Temp.length; i += 2) { + let Title = Temp[i].children[0].innerText; + let Time = 0; + if (Temp[i].children[1] != null) { + Time = Temp[i].children[1].innerText; + } + let Body = Temp[i + 1].innerHTML; + NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; + for (let i = 0; i < NewsData.length; i++) { + let NewsRow = document.createElement("div"); + NewsRow.className = "cnt-row"; + let NewsRowHead = document.createElement("div"); + NewsRowHead.className = "cnt-row-head title"; + NewsRowHead.innerText = NewsData[i].Title; + if (NewsData[i].Time.getTime() != 0) { + NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; + } + NewsRow.appendChild(NewsRowHead); + let NewsRowBody = document.createElement("div"); + NewsRowBody.className = "cnt-row-body"; + NewsRowBody.innerHTML = NewsData[i].Body; + NewsRow.appendChild(NewsRowBody); + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); + } + let MyContestData = document.querySelector("body > div > div.mt-3 > div > div.col-md-4 > div:nth-child(2)").innerHTML; + let CountDownData = document.querySelector("#countdown_list").innerHTML; + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
    +
    我的月赛
    +
    ${MyContestData}
    +
    +
    +
    倒计时
    +
    ${CountDownData}
    +
    `; + } else if (location.pathname == "/showsource.php") { + let Code = ""; + if (SearchParams.get("ByUserScript") == null) { + document.title = "查看代码: " + SearchParams.get("id"); + await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("id")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + Code = Response.replace("\n\n", ""); + }); + } else { + document.title = "查看标程: " + SearchParams.get("pid"); + if (localStorage.getItem("UserScript-LastUploadedStdTime") === undefined || new Date().getTime() - localStorage.getItem("UserScript-LastUploadedStdTime") > 1000 * 60 * 60 * 24 * 30) { + location.href = "https://www.xmoj.tech/userinfo.php?ByUserScript=1"; + } + await new Promise((Resolve) => { + RequestAPI("GetStd", { + "ProblemID": Number(SearchParams.get("pid")) + }, (Response) => { + if (Response.Success) { + Code = Response.Data.StdCode; + } else { + Code = Response.Message; + } + Resolve(); + }); + }); + } + document.querySelector("body > div > div.mt-3").innerHTML = ``; + CodeMirror.fromTextArea(document.querySelector("body > div > div.mt-3 > textarea"), { + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + } else if (location.pathname == "/ceinfo.php") { + await fetch(location.href) + .then((Result) => { + return Result.text(); + }).then((Result) => { + let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); + document.querySelector("body > div > div.mt-3").innerHTML = ""; + let CodeElement = document.createElement("div"); + CodeElement.className = "mb-3"; + document.querySelector("body > div > div.mt-3").appendChild(CodeElement); + CodeMirror(CodeElement, { + value: ParsedDocument.getElementById("errtxt").innerHTML.replaceAll("<", "<").replaceAll(">", ">"), + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + }); + } else if (location.pathname == "/problem_std.php") { + await fetch("https://www.xmoj.tech/problem_std.php?cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.getElementsByTagName("pre"); + document.querySelector("body > div > div.mt-3").innerHTML = ""; + for (let i = 0; i < Temp.length; i++) { + let CodeElement = document.createElement("div"); + CodeElement.className = "mb-3"; + document.querySelector("body > div > div.mt-3").appendChild(CodeElement); + CodeMirror(CodeElement, { + value: Temp[i].innerText, + lineNumbers: true, + mode: "text/x-c++src", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") + }).setSize("100%", "auto"); + } + }); + } else if (location.pathname == "/mail.php") { + if (SearchParams.get("to_user") == null) { + document.querySelector("body > div > div.mt-3").innerHTML = `
    +
    + + +
    +
    + +
    +
    + + + + + + + + + +
    接收者最新消息最后联系时间
    + `; + let RefreshMessageList = (Silent = true) => { + if (!Silent) { + ReceiveTable.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + ReceiveTable.children[1].appendChild(Row); + for (let j = 0; j < 3; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("GetMailList", {}, async (ResponseData) => { + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + let Data = ResponseData.Data.MailList; + ReceiveTable.children[1].innerHTML = ""; + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("tr"); + ReceiveTable.children[1].appendChild(Row); + let UsernameCell = document.createElement("td"); + Row.appendChild(UsernameCell); + let UsernameSpan = document.createElement("span"); + UsernameCell.appendChild(UsernameSpan); + GetUsernameHTML(UsernameSpan, Data[i].OtherUser, false, "https://www.xmoj.tech/mail.php?to_user="); + if (Data[i].UnreadCount != 0) { + let UnreadCountSpan = document.createElement("span"); + UsernameCell.appendChild(UnreadCountSpan); + UnreadCountSpan.className = "ms-1 badge text-bg-danger"; + UnreadCountSpan.innerText = Data[i].UnreadCount; + } + let LastsMessageCell = document.createElement("td"); + Row.appendChild(LastsMessageCell); + LastsMessageCell.innerText = replaceMarkdownImages(Data[i].LastsMessage, '[image]'); + let SendTimeCell = document.createElement("td"); + Row.appendChild(SendTimeCell); + SendTimeCell.innerHTML = GetRelativeTime(Data[i].SendTime); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }; + Username.addEventListener("input", () => { + Username.classList.remove("is-invalid"); + }); + AddUser.addEventListener("click", () => { + let UsernameData = Username.value; + if (UsernameData == "") { + Username.classList.add("is-invalid"); + return; + } + AddUser.children[0].style.display = ""; + AddUser.disabled = true; + RequestAPI("SendMail", { + "ToUser": String(UsernameData), + "Content": String("您好,我是" + CurrentUsername) + }, (ResponseData) => { + AddUser.children[0].style.display = "none"; + AddUser.disabled = false; + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + RefreshMessageList(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + RefreshMessageList(false); + addEventListener("focus", RefreshMessageList); + } else { + document.querySelector("body > div > div.mt-3").innerHTML = `
    +
    +
    + +
    +
    + + +
    +
    + + + + + + + + + + + + +
    发送者内容发送时间阅读状态
    `; + GetUsernameHTML(ToUser, SearchParams.get("to_user")); + let RefreshMessage = (Silent = true) => { + if (!Silent) { + MessageTable.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + MessageTable.children[1].appendChild(Row); + for (let j = 0; j < 4; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("ReadUserMailMention", { + "UserID": String(SearchParams.get("to_user")) + }); + RequestAPI("GetMail", { + "OtherUser": String(SearchParams.get("to_user")) + }, async (ResponseData) => { + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + let Data = ResponseData.Data.Mail; + MessageTable.children[1].innerHTML = ""; + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("tr"); + MessageTable.children[1].appendChild(Row); + if (!Data[i].IsRead && Data[i].FromUser != CurrentUsername) { + Row.className = "table-info"; + } + let UsernameCell = document.createElement("td"); + Row.appendChild(UsernameCell); + GetUsernameHTML(UsernameCell, Data[i].FromUser); + let ContentCell = document.createElement("td"); + let ContentDiv = document.createElement("div"); + ContentDiv.style.display = "flex"; + ContentDiv.style.maxWidth = window.innerWidth - 300 + "px"; + ContentDiv.style.maxHeight = "500px"; + ContentDiv.style.overflowX = "auto"; + ContentDiv.style.overflowY = "auto"; + ContentDiv.innerHTML = PurifyHTML(marked.parse(Data[i].Content)); + let mediaElements = ContentDiv.querySelectorAll('img, video'); + for (let media of mediaElements) { + media.style.objectFit = 'contain'; + media.style.maxWidth = '100%'; + media.style.maxHeight = '100%'; + } + ContentCell.appendChild(ContentDiv); + Row.appendChild(ContentCell); + let SendTimeCell = document.createElement("td"); + Row.appendChild(SendTimeCell); + SendTimeCell.innerHTML = GetRelativeTime(Data[i].SendTime); + let IsReadCell = document.createElement("td"); + Row.appendChild(IsReadCell); + IsReadCell.innerHTML = (Data[i].IsRead ? "已读" : "未读"); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }; + Content.addEventListener("input", () => { + Content.classList.remove("is-invalid"); + }); + Content.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = Content.value.substring(0, Content.selectionStart); + let After = Content.value.substring(Content.selectionEnd, Content.value.length); + const UploadMessage = "![正在上传图片...]()"; + Content.value = Before + UploadMessage + After; + Content.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + Content.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + Content.dispatchEvent(new Event("input")); + } else { + Content.value = Before + `![上传失败!` + ResponseData.Message + `]()` + After; + Content.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + Content.addEventListener("keydown", (Event) => { + if (Event.keyCode == 13) { + Send.click(); + } + }); + Send.addEventListener("click", () => { + if (Content.value == "") { + Content.classList.add("is-invalid"); + return; + } + Send.disabled = true; + Send.children[0].style.display = ""; + let ContentData = Content.value; + RequestAPI("SendMail", { + "ToUser": String(SearchParams.get("to_user")), "Content": String(ContentData) + }, (ResponseData) => { + Send.disabled = false; + Send.children[0].style.display = "none"; + if (ResponseData.Success) { + ErrorElement.style.display = "none"; + Content.value = ""; + RefreshMessage(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + RefreshMessage(false); + addEventListener("focus", RefreshMessage); + } + } else if (location.pathname.indexOf("/discuss3") != -1) { + if (UtilityEnabled("Discussion")) { + Discussion.classList.add("active"); + if (location.pathname == "/discuss3/discuss.php") { + document.title = "讨论列表"; + let ProblemID = parseInt(SearchParams.get("pid")); + let BoardID = parseInt(SearchParams.get("bid")); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    讨论列表${(isNaN(ProblemID) ? "" : ` - 题目` + ProblemID)}

    + + +
    + + + + + + + + + + + + + + + +
    编号标题作者题目编号发布时间回复数最后回复
    `; + NewPost.addEventListener("click", () => { + if (!isNaN(ProblemID)) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?pid=" + ProblemID; + } else if (SearchParams.get("bid") != null) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?bid=" + SearchParams.get("bid"); + } else { + location.href = "https://www.xmoj.tech/discuss3/newpost.php"; + } + }); + const RefreshPostList = (Silent = true) => { + if (!Silent) { + PostList.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + for (let j = 0; j < 7; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("GetPosts", { + "ProblemID": Number(ProblemID || 0), + "Page": Number(Page), + "BoardID": Number(SearchParams.get("bid") || -1) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + ErrorElement.style.display = "none"; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + } + let Posts = ResponseData.Data.Posts; + PostList.children[1].innerHTML = ""; + if (Posts.length == 0) { + PostList.children[1].innerHTML = `暂无数据`; + } + for (let i = 0; i < Posts.length; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + let IDCell = document.createElement("td"); + Row.appendChild(IDCell); + IDCell.innerText = Posts[i].PostID + " " + Posts[i].BoardName; + let TitleCell = document.createElement("td"); + Row.appendChild(TitleCell); + let TitleLink = document.createElement("a"); + TitleCell.appendChild(TitleLink); + TitleLink.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + Posts[i].PostID; + if (Posts[i].Lock.Locked) { + TitleLink.classList.add("link-secondary"); + TitleLink.innerHTML = "🔒 "; + } + TitleLink.innerHTML += Posts[i].Title; + let AuthorCell = document.createElement("td"); + Row.appendChild(AuthorCell); + GetUsernameHTML(AuthorCell, Posts[i].UserID); + let ProblemIDCell = document.createElement("td"); + Row.appendChild(ProblemIDCell); + if (Posts[i].ProblemID != 0) { + let ProblemIDLink = document.createElement("a"); + ProblemIDCell.appendChild(ProblemIDLink); + ProblemIDLink.href = "https://www.xmoj.tech/problem.php?id=" + Posts[i].ProblemID; + ProblemIDLink.innerText = Posts[i].ProblemID; + } + let PostTimeCell = document.createElement("td"); + Row.appendChild(PostTimeCell); + PostTimeCell.innerHTML = GetRelativeTime(Posts[i].PostTime); + let ReplyCountCell = document.createElement("td"); + Row.appendChild(ReplyCountCell); + ReplyCountCell.innerText = Posts[i].ReplyCount; + let LastReplyTimeCell = document.createElement("td"); + Row.appendChild(LastReplyTimeCell); + LastReplyTimeCell.innerHTML = GetRelativeTime(Posts[i].LastReplyTime); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }; + RefreshPostList(false); + addEventListener("focus", RefreshPostList); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php"; + LinkElement.classList.add("me-2"); + LinkElement.innerText = "全部"; + GotoBoard.appendChild(LinkElement); + for (let i = 0; i < ResponseData.Data.Boards.length; i++) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php?bid=" + ResponseData.Data.Boards[i].BoardID; + LinkElement.classList.add("me-2"); + LinkElement.innerText = ResponseData.Data.Boards[i].BoardName; + GotoBoard.appendChild(LinkElement); + } + } + }); + } else if (location.pathname == "/discuss3/newpost.php") { + let ProblemID = parseInt(SearchParams.get("pid")); + document.querySelector("body > div > div").innerHTML = `

    发布新讨论` + (!isNaN(ProblemID) ? ` - 题目` + ProblemID : ``) + `

    +
    + +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + ContentElement.classList.remove("is-invalid"); + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + TitleElement.addEventListener("input", () => { + TitleElement.classList.remove("is-invalid"); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + let Title = TitleElement.value; + let Content = ContentElement.value; + let ProblemID = parseInt(SearchParams.get("pid")); + if (Title === "") { + TitleElement.classList.add("is-invalid"); + return; + } + if (Content === "") { + ContentElement.classList.add("is-invalid"); + return; + } + if (document.querySelector("#Board input:checked") === null) { + ErrorElement.innerText = "请选择要发布的板块"; + ErrorElement.style.display = "block"; + return; + } + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewPost", { + "Title": String(Title), + "Content": String(Content), + "ProblemID": Number(isNaN(ProblemID) ? 0 : ProblemID), + "CaptchaSecretKey": String(CaptchaSecretKey), + "BoardID": Number(document.querySelector("#Board input:checked").value) + }, (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ResponseData.Data.PostID; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let Data = ResponseData.Data.Boards; + for (let i = 0; i < Data.length; i++) { + let RadioElement = document.createElement("div"); + RadioElement.className = "col-auto form-check form-check-inline"; + let RadioInput = document.createElement("input"); + RadioInput.className = "form-check-input"; + RadioInput.type = "radio"; + RadioInput.name = "Board"; + RadioInput.id = "Board" + Data[i].BoardID; + RadioInput.value = Data[i].BoardID; + RadioElement.appendChild(RadioInput); + if (SearchParams.get("bid") !== null && SearchParams.get("bid") == Data[i].BoardID) { + RadioInput.checked = true; + } + if (!isNaN(ProblemID)) { + RadioInput.disabled = true; + } + if (Data[i].BoardID == 4) { + if (!isNaN(ProblemID)) RadioInput.checked = true; + RadioInput.disabled = true; + } + let RadioLabel = document.createElement("label"); + RadioLabel.className = "form-check-label"; + RadioLabel.htmlFor = "Board" + Data[i].BoardID; + RadioLabel.innerText = Data[i].BoardName; + RadioElement.appendChild(RadioLabel); + Board.appendChild(RadioElement); + } + } + }); + } else if (location.pathname == "/discuss3/thread.php") { + if (SearchParams.get("tid") == null) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + let ThreadID = SearchParams.get("tid"); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    +
    + 作者:
    + 发布时间: + 板块: + + + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + let RefreshReply = (Silent = true) => { + if (!Silent) { + PostTitle.innerHTML = ``; + PostAuthor.innerHTML = ``; + PostTime.innerHTML = ``; + PostBoard.innerHTML = ``; + PostReplies.innerHTML = ""; + for (let i = 0; i < 10; i++) { + PostReplies.innerHTML += `
    +
    +
    + + +
    +
    + + + +
    +
    `; + } + } + RequestAPI("GetPost", { + "PostID": Number(ThreadID), "Page": Number(Page) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + let OldScrollTop = document.documentElement.scrollTop; + let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { + Delete.style.display = ""; + } + } + PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); + document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; + PostAuthor.innerHTML = ""; + GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); + PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); + PostBoard.innerHTML = ResponseData.Data.BoardName; + let Replies = ResponseData.Data.Reply; + PostReplies.innerHTML = ""; + for (let i = 0; i < Replies.length; i++) { + let CardElement = document.createElement("div"); + PostReplies.appendChild(CardElement); + CardElement.className = "card mb-3"; + let CardBodyElement = document.createElement("div"); + CardElement.appendChild(CardBodyElement); + CardBodyElement.className = "card-body row"; + let CardBodyRowElement = document.createElement("div"); + CardBodyElement.appendChild(CardBodyRowElement); + CardBodyRowElement.className = "row mb-3"; + let AuthorElement = document.createElement("span"); + CardBodyRowElement.appendChild(AuthorElement); + AuthorElement.className = "col-4 text-muted"; + let AuthorSpanElement = document.createElement("span"); + AuthorElement.appendChild(AuthorSpanElement); + AuthorSpanElement.innerText = "作者:"; + let AuthorUsernameElement = document.createElement("span"); + AuthorElement.appendChild(AuthorUsernameElement); + GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); + let SendTimeElement = document.createElement("span"); + CardBodyRowElement.appendChild(SendTimeElement); + SendTimeElement.className = "col-4 text-muted"; + SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); + + let OKButton; + if (!LockButtons) { + let ButtonsElement = document.createElement("span"); + CardBodyRowElement.appendChild(ButtonsElement); + ButtonsElement.className = "col-4"; + let ReplyButton = document.createElement("button"); + ButtonsElement.appendChild(ReplyButton); + ReplyButton.type = "button"; + ReplyButton.className = "btn btn-sm btn-info"; + ReplyButton.innerText = "回复"; + ReplyButton.addEventListener("click", () => { + let Content = Replies[i].Content; + Content = Content.split("\n").map((Line) => { + // Count the number of '>' characters at the beginning of the line + let nestingLevel = 0; + while (Line.startsWith(">")) { + nestingLevel++; + Line = Line.substring(1).trim(); + } + // If the line is nested more than 2 levels deep, skip it + if (nestingLevel > 2) { + return null; + } + // Reconstruct the line with the appropriate number of '>' characters + return "> ".repeat(nestingLevel + 1) + Line; + }).filter(Line => Line !== null) // Remove null entries + .join("\n"); + ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; + ContentElement.focus(); + }); + let DeleteButton = document.createElement("button"); + ButtonsElement.appendChild(DeleteButton); + DeleteButton.type = "button"; + DeleteButton.className = "btn btn-sm btn-danger ms-1"; + DeleteButton.innerText = "删除"; + DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + DeleteButton.addEventListener("click", () => { + DeleteButton.disabled = true; + DeleteButton.lastChild.style.display = ""; + RequestAPI("DeleteReply", { + "ReplyID": Number(Replies[i].ReplyID) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + DeleteButton.disabled = false; + DeleteButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let DeleteSpin = document.createElement("div"); + DeleteButton.appendChild(DeleteSpin); + DeleteSpin.className = "spinner-border spinner-border-sm"; + DeleteSpin.role = "status"; + DeleteSpin.style.display = "none"; + OKButton = document.createElement("button"); + ButtonsElement.appendChild(OKButton); + OKButton.type = "button"; + OKButton.style.display = "none"; + OKButton.className = "btn btn-sm btn-success ms-1"; + OKButton.innerText = "确认"; + let OKSpin = document.createElement("div"); + OKButton.appendChild(OKSpin); + OKSpin.className = "spinner-border spinner-border-sm"; + OKSpin.role = "status"; + OKSpin.style.display = "none"; + OKButton.addEventListener("click", () => { + OKButton.disabled = true; + OKButton.lastChild.style.display = ""; + RequestAPI("EditReply", { + ReplyID: Number(Replies[i].ReplyID), + Content: String(ContentEditor.value) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + OKButton.disabled = false; + OKButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let CancelButton = document.createElement("button"); + ButtonsElement.appendChild(CancelButton); + CancelButton.type = "button"; + CancelButton.style.display = "none"; + CancelButton.className = "btn btn-sm btn-secondary ms-1"; + CancelButton.innerText = "取消"; + CancelButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = ""; + CardBodyElement.children[3].style.display = "none"; + EditButton.style.display = ""; + OKButton.style.display = "none"; + CancelButton.style.display = "none"; + }); + let EditButton = document.createElement("button"); + ButtonsElement.appendChild(EditButton); + EditButton.type = "button"; + EditButton.className = "btn btn-sm btn-warning ms-1"; + EditButton.innerText = "编辑"; + EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + EditButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = "none"; + CardBodyElement.children[3].style.display = ""; + EditButton.style.display = "none"; + OKButton.style.display = ""; + CancelButton.style.display = ""; + }); + } + + let CardBodyHRElement = document.createElement("hr"); + CardBodyElement.appendChild(CardBodyHRElement); + + let ReplyContentElement = document.createElement("div"); + CardBodyElement.appendChild(ReplyContentElement); + ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + if (Replies[i].EditTime != null) { + if (Replies[i].EditPerson == Replies[i].UserID) { + ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } else { + ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } + } + let ContentEditElement = document.createElement("div"); + CardBodyElement.appendChild(ContentEditElement); + ContentEditElement.classList.add("input-group"); + ContentEditElement.style.display = "none"; + let ContentEditor = document.createElement("textarea"); + ContentEditElement.appendChild(ContentEditor); + ContentEditor.className = "form-control col-6"; + ContentEditor.rows = 3; + ContentEditor.value = Replies[i].Content; + if (ContentEditor.value.indexOf("
    ") != -1) { + ContentEditor.value = ContentEditor.value.substring(0, ContentEditor.value.indexOf("
    ")); + } + ContentEditor.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + OKButton.click(); + } + }); + let PreviewTab = document.createElement("div"); + ContentEditElement.appendChild(PreviewTab); + PreviewTab.className = "form-control col-6"; + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + ContentEditor.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + RenderMathJax(); + }); + ContentEditor.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentEditor.value.substring(0, ContentEditor.selectionStart); + let After = ContentEditor.value.substring(ContentEditor.selectionEnd, ContentEditor.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentEditor.value = Before + UploadMessage + After; + ContentEditor.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentEditor.dispatchEvent(new Event("input")); + } else { + ContentEditor.value = Before + `![上传失败!]()` + After; + ContentEditor.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + } + + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + + let CodeElements = document.querySelectorAll("#PostReplies > div > div > div:nth-child(3) > pre > code"); + for (let i = 0; i < CodeElements.length; i++) { + let ModeName = "text/x-c++src"; + if (CodeElements[i].className == "language-c") { + ModeName = "text/x-csrc"; + } else if (CodeElements[i].className == "language-cpp") { + ModeName = "text/x-c++src"; + } + CodeMirror(CodeElements[i].parentElement, { + value: CodeElements[i].innerText, + mode: ModeName, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + lineNumbers: true, + readOnly: true + }).setSize("100%", "auto"); + CodeElements[i].remove(); + } + + if (LockButtons) { + let LockElement = ContentElement.parentElement.parentElement; + LockElement.innerHTML = "讨论已于 " + await GetRelativeTime(ResponseData.Data.Lock.LockTime) + " 被 "; + let LockUsernameSpan = document.createElement("span"); + LockElement.appendChild(LockUsernameSpan); + GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); + LockElement.innerHTML += " 锁定"; + LockElement.classList.add("mb-5"); + } + + if (IsAdmin) { + ToggleLock.style.display = "inline-block"; + ToggleLockButton.checked = ResponseData.Data.Lock.Locked; + ToggleLockButton.onclick = () => { + ToggleLockButton.disabled = true; + ErrorElement.style.display = "none"; + RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { + "PostID": Number(ThreadID) + }, (LockResponseData) => { + ToggleLockButton.disabled = false; + if (LockResponseData.Success) { + RefreshReply(); + } else { + ErrorElement.style.display = ""; + ErrorElement.innerText = "错误:" + LockResponseData.Message; + ToggleLockButton.checked = !ToggleLockButton.checked; + } + }); + }; + } + + Style.innerHTML += "img {"; + Style.innerHTML += " width: 50%;"; + Style.innerHTML += "}"; + + RenderMathJax(); + + if (Silent) { + scrollTo({ + top: OldScrollTop, behavior: "instant" + }); + } + } else { + PostTitle.innerText = "错误:" + ResponseData.Message; + } + }); + }; + Delete.addEventListener("click", () => { + Delete.disabled = true; + Delete.children[0].style.display = "inline-block"; + RequestAPI("DeletePost", { + "PostID": Number(SearchParams.get("tid")) + }, (ResponseData) => { + Delete.disabled = false; + Delete.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewReply", { + "PostID": Number(SearchParams.get("tid")), + "Content": String(ContentElement.value), + "CaptchaSecretKey": String(CaptchaSecretKey) + }, async (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + RefreshReply(); + ContentElement.value = ""; + PreviewTab.innerHTML = ""; + while (PostReplies.innerHTML.indexOf("placeholder") != -1) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + ContentElement.focus(); + ContentElement.scrollIntoView(); + turnstile.reset(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RefreshReply(false); + addEventListener("focus", RefreshReply); + } + } + } + } + } + } + } 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."); + } + } +} diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 00000000..37bcebd4 --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,24 @@ +/** + * Feature configuration and utility enabling/disabling + */ + +/** + * Check if a utility/feature is enabled + * @param {string} Name - The name of the utility/feature + * @returns {boolean} True if enabled, false otherwise + */ +export let UtilityEnabled = (Name) => { + try { + if (localStorage.getItem("UserScript-Setting-" + Name) == null) { + const defaultOffItems = ["DebugMode", "SuperDebug", "ReplaceXM"]; + localStorage.setItem("UserScript-Setting-" + Name, defaultOffItems.includes(Name) ? "false" : "true"); + } + return localStorage.getItem("UserScript-Setting-" + Name) == "true"; + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + const { SmartAlert } = require('./alerts'); + 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."); + } + } +}; diff --git a/src/core/constants.js b/src/core/constants.js new file mode 100644 index 00000000..e795424b --- /dev/null +++ b/src/core/constants.js @@ -0,0 +1,8 @@ +/** + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + +export const CaptchaSiteKey = "0x4AAAAAAALBT58IhyDViNmv"; +export const AdminUserList = ["zhuchenrui2", "shanwenxiao", "chenlangning", "admin"]; diff --git a/src/core/menu.js b/src/core/menu.js new file mode 100644 index 00000000..44486b83 --- /dev/null +++ b/src/core/menu.js @@ -0,0 +1,28 @@ +/** + * Menu command registrations + */ + +/** + * Register Greasemonkey menu commands + */ +export function registerMenuCommands() { + GM_registerMenuCommand("清除缓存", () => { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + location.reload(); + }); + + GM_registerMenuCommand("重置数据", () => { + if (confirm("确定要重置数据吗?")) { + localStorage.clear(); + location.reload(); + } + }); +} diff --git a/src/features/index-page.js b/src/features/index-page.js new file mode 100644 index 00000000..a92c12cf --- /dev/null +++ b/src/features/index-page.js @@ -0,0 +1,256 @@ +// Index page handler +import { UtilityEnabled } from '../utils/settings.js'; +import { RequestAPI } from '../utils/api.js'; +import { TidyTable } from '../utils/dom.js'; +import { RenderMathJax } from '../utils/mathjax.js'; +import { GetUsernameHTML } from '../utils/username.js'; + +/** + * Handle index page (/index.php or /) + * @param {Object} context - Execution context + */ +export async function handleIndexPage(context) { + const { location, SearchParams, initTheme, marked } = context; + + if (location.pathname == "/index.php" || location.pathname == "/") { + if (new URL(location.href).searchParams.get("ByUserScript") != null) { + // Script settings page + document.title = "脚本设置"; + localStorage.setItem("UserScript-Opened", "true"); + let Container = document.getElementsByClassName("mt-3")[0]; + Container.innerHTML = ""; + let Alert = document.createElement("div"); + Alert.classList.add("alert"); + Alert.classList.add("alert-primary"); + Alert.role = "alert"; + Alert.innerHTML = `欢迎您使用XMOJ增强脚本!点击 + 此处 + 查看更新日志。`; + Container.appendChild(Alert); + let UtilitiesCard = document.createElement("div"); + UtilitiesCard.classList.add("card"); + UtilitiesCard.classList.add("mb-3"); + let UtilitiesCardHeader = document.createElement("div"); + UtilitiesCardHeader.classList.add("card-header"); + UtilitiesCardHeader.innerText = "XMOJ增强脚本功能列表"; + UtilitiesCard.appendChild(UtilitiesCardHeader); + let UtilitiesCardBody = document.createElement("div"); + UtilitiesCardBody.classList.add("card-body"); + let CreateList = (Data) => { + let List = document.createElement("ul"); + List.classList.add("list-group"); + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("li"); + Row.classList.add("list-group-item"); + if (Data[i].Type == "A") { + Row.classList.add("list-group-item-success"); + } else if (Data[i].Type == "F") { + Row.classList.add("list-group-item-warning"); + } else if (Data[i].Type == "D") { + Row.classList.add("list-group-item-danger"); + } + if (Data[i].ID == "Theme") { + let Label = document.createElement("label"); + Label.classList.add("me-2"); + Label.htmlFor = "UserScript-Setting-Theme"; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + let Select = document.createElement("select"); + Select.classList.add("form-select", "form-select-sm", "w-auto", "d-inline"); + Select.id = "UserScript-Setting-Theme"; + [ + ["light", "亮色"], + ["dark", "暗色"], + ["auto", "跟随系统"] + ].forEach(opt => { + let option = document.createElement("option"); + option.value = opt[0]; + option.innerText = opt[1]; + Select.appendChild(option); + }); + Select.value = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + Select.addEventListener("change", () => { + localStorage.setItem("UserScript-Setting-Theme", Select.value); + initTheme(); + }); + Row.appendChild(Select); + } else if (Data[i].Children == undefined) { + let CheckBox = document.createElement("input"); + CheckBox.classList.add("form-check-input"); + CheckBox.classList.add("me-1"); + CheckBox.type = "checkbox"; + CheckBox.id = Data[i].ID; + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == null) { + localStorage.setItem("UserScript-Setting-" + Data[i].ID, "true"); + } + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == "false") { + CheckBox.checked = false; + } else { + CheckBox.checked = true; + } + CheckBox.addEventListener("change", () => { + return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); + }); + + Row.appendChild(CheckBox); + let Label = document.createElement("label"); + Label.classList.add("form-check-label"); + Label.htmlFor = Data[i].ID; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } else { + let Label = document.createElement("label"); + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } + if (Data[i].Children != undefined) { + Row.appendChild(CreateList(Data[i].Children)); + } + List.appendChild(Row); + } + return List; + }; + UtilitiesCardBody.appendChild(CreateList([{ + "ID": "Discussion", + "Type": "F", + "Name": "恢复讨论与短消息功能" + }, { + "ID": "MoreSTD", "Type": "F", "Name": "查看到更多标程" + }, {"ID": "ApplyData", "Type": "A", "Name": "获取数据功能"}, { + "ID": "AutoCheat", "Type": "A", "Name": "自动提交当年代码" + }, {"ID": "Rating", "Type": "A", "Name": "添加用户评分和用户名颜色"}, { + "ID": "AutoRefresh", "Type": "A", "Name": "比赛列表、比赛排名界面自动刷新" + }, { + "ID": "AutoCountdown", "Type": "A", "Name": "比赛列表等界面的时间自动倒计时" + }, {"ID": "DownloadPlayback", "Type": "A", "Name": "回放视频增加下载功能"}, { + "ID": "ImproveACRate", "Type": "A", "Name": "自动提交已AC题目以提高AC率" + }, {"ID": "AutoO2", "Type": "F", "Name": "代码提交界面自动选择O2优化"}, { + "ID": "Beautify", "Type": "F", "Name": "美化界面", "Children": [{ + "ID": "NewTopBar", "Type": "F", "Name": "使用新的顶部导航栏" + }, { + "ID": "NewBootstrap", "Type": "F", "Name": "使用新版的Bootstrap样式库*" + }, {"ID": "ResetType", "Type": "F", "Name": "重新排版*"}, { + "ID": "AddColorText", "Type": "A", "Name": "增加彩色文字" + }, {"ID": "AddUnits", "Type": "A", "Name": "状态界面内存与耗时添加单位"}, { + "ID": "Theme", "Type": "A", "Name": "界面主题" + }, {"ID": "AddAnimation", "Type": "A", "Name": "增加动画"}, { + "ID": "ReplaceYN", "Type": "F", "Name": "题目前状态提示替换为好看的图标" + }, {"ID": "RemoveAlerts", "Type": "D", "Name": "去除多余反复的提示"}, { + "ID": "Translate", "Type": "F", "Name": "统一使用中文,翻译了部分英文*" + }, { + "ID": "ReplaceLinks", "Type": "F", "Name": "将网站中所有以方括号包装的链接替换为按钮" + }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { + "ID": "ReplaceXM", + "Type": "F", + "Name": "将网站中所有"小明"和"我"关键字替换为"高老师",所有"小红"替换为"徐师娘",所有"小粉"替换为"彩虹",所有"下海"、"海上"替换为"上海" (此功能默认关闭)" + }] + }, { + "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" + }, { + "ID": "SavePassword", "Type": "A", "Name": "自动保存用户名与密码,免去每次手动输入密码的繁琐" + }, { + "ID": "CopySamples", "Type": "F", "Name": "题目界面测试样例有时复制无效" + }, { + "ID": "RefreshSolution", "Type": "F", "Name": "状态页面结果自动刷新每次只能刷新一个" + }, {"ID": "CopyMD", "Type": "A", "Name": "复制题目或题解内容"}, { + "ID": "ProblemSwitcher", "Type": "A", "Name": "比赛题目切换器" + }, { + "ID": "OpenAllProblem", "Type": "A", "Name": "比赛题目界面一键打开所有题目" + }, { + "ID": "CheckCode", "Type": "A", "Name": "提交代码前对代码进行检查", "Children": [{ + "ID": "IOFile", "Type": "A", "Name": "是否使用了文件输入输出(如果需要使用)" + }, {"ID": "CompileError", "Type": "A", "Name": "是否有编译错误"}] + }, { + "ID": "ExportACCode", "Type": "F", "Name": "导出AC代码每一道题目一个文件" + }, {"ID": "LoginFailed", "Type": "F", "Name": "修复登录后跳转失败*"}, { + "ID": "NewDownload", "Type": "A", "Name": "下载页面增加下载内容" + }, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, { + "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" + }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { + "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" + }, { + "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" + }])); + let UtilitiesCardFooter = document.createElement("div"); + UtilitiesCardFooter.className = "card-footer text-muted"; + UtilitiesCardFooter.innerText = "* 不建议关闭,可能会导致系统不稳定、界面错乱、功能缺失等问题\n绿色:增加功能 黄色:修改功能 红色:删除功能"; + UtilitiesCardBody.appendChild(UtilitiesCardFooter); + UtilitiesCard.appendChild(UtilitiesCardBody); + Container.appendChild(UtilitiesCard); + let FeedbackCard = document.createElement("div"); + FeedbackCard.className = "card mb-3"; + let FeedbackCardHeader = document.createElement("div"); + FeedbackCardHeader.className = "card-header"; + FeedbackCardHeader.innerText = "反馈、源代码、联系作者"; + FeedbackCard.appendChild(FeedbackCardHeader); + let FeedbackCardBody = document.createElement("div"); + FeedbackCardBody.className = "card-body"; + let FeedbackCardText = document.createElement("p"); + FeedbackCardText.className = "card-text"; + FeedbackCardText.innerText = "如果您有任何建议或者发现了 bug,请前往本项目的 GitHub 页面并提交 issue。提交 issue 前请先搜索是否有相同的 issue,如果有请在该 issue 下留言。请在 issue 中尽可能详细地描述您的问题,并且附上您的浏览器版本、操作系统版本、脚本版本、复现步骤等信息。谢谢您支持本项目。"; + FeedbackCardBody.appendChild(FeedbackCardText); + let FeedbackCardLink = document.createElement("a"); + FeedbackCardLink.className = "card-link"; + FeedbackCardLink.innerText = "GitHub"; + FeedbackCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script"; + FeedbackCardBody.appendChild(FeedbackCardLink); + FeedbackCard.appendChild(FeedbackCardBody); + Container.appendChild(FeedbackCard); + } else { + // Normal index page + let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; + let NewsData = []; + for (let i = 0; i < Temp.length; i += 2) { + let Title = Temp[i].children[0].innerText; + let Time = 0; + if (Temp[i].children[1] != null) { + Time = Temp[i].children[1].innerText; + } + let Body = Temp[i + 1].innerHTML; + NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; + for (let i = 0; i < NewsData.length; i++) { + let NewsRow = document.createElement("div"); + NewsRow.className = "cnt-row"; + let NewsRowHead = document.createElement("div"); + NewsRowHead.className = "cnt-row-head title"; + NewsRowHead.innerText = NewsData[i].Title; + if (NewsData[i].Time != 0) { + NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; + } + NewsRow.appendChild(NewsRowHead); + let NewsRowBody = document.createElement("div"); + NewsRowBody.className = "cnt-row-body"; + NewsRowBody.innerHTML = NewsData[i].Body; + NewsRow.appendChild(NewsRowBody); + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); + } + let CountDownData = document.querySelector("#countdown_list").innerHTML; + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
    +
    倒计时
    +
    ${CountDownData}
    +
    `; + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML += `
    +
    公告
    +
    加载中...
    +
    `; + RequestAPI("GetNotice", {}, (Response) => { + if (Response.Success) { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = marked.parse(Response.Data["Notice"]).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + RenderMathJax(); + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + } else { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = "加载失败: " + Response.Message; + } + }); + } + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..67170eb5 --- /dev/null +++ b/src/main.js @@ -0,0 +1,56 @@ +/** + * Main entry point for XMOJ Script + * This file imports all utilities and features, then initializes the application + */ + +// Core imports +import { UtilityEnabled } from './core/config.js'; +import { AdminUserList } from './core/constants.js'; + +// Utility imports +import { escapeHTML, PurifyHTML } from './utils/html.js'; +import { SmartAlert } from './utils/alerts.js'; +import { GetRelativeTime, SecondsToString, StringToSeconds, TimeToStringTime } from './utils/time.js'; +import { SizeToStringSize, CodeSizeToStringSize } from './utils/format.js'; +import { compareVersions } from './utils/version.js'; +import { RequestAPI } from './utils/api.js'; +import { storeCredential, getCredential, clearCredential } from './utils/credentials.js'; +import { RenderMathJax } from './utils/mathjax.js'; +import { TidyTable } from './utils/table.js'; +import { GetUserInfo, GetUserBadge, GetUsernameHTML } from './utils/user.js'; + +// Core application imports +import { initTheme, NavbarStyler, replaceMarkdownImages, main } from './core/bootstrap.js'; +import { registerMenuCommands } from './core/menu.js'; + +// Make utilities globally available (for compatibility with inline code) +window.escapeHTML = escapeHTML; +window.PurifyHTML = PurifyHTML; +window.SmartAlert = SmartAlert; +window.GetRelativeTime = GetRelativeTime; +window.SecondsToString = SecondsToString; +window.StringToSeconds = StringToSeconds; +window.TimeToStringTime = TimeToStringTime; +window.SizeToStringSize = SizeToStringSize; +window.CodeSizeToStringSize = CodeSizeToStringSize; +window.compareVersions = compareVersions; +window.RequestAPI = RequestAPI; +window.storeCredential = storeCredential; +window.getCredential = getCredential; +window.clearCredential = clearCredential; +window.RenderMathJax = RenderMathJax; +window.TidyTable = TidyTable; +window.GetUserInfo = GetUserInfo; +window.GetUserBadge = GetUserBadge; +window.GetUsernameHTML = GetUsernameHTML; +window.UtilityEnabled = UtilityEnabled; +window.AdminUserList = AdminUserList; + +// Register menu commands +registerMenuCommands(); + +// Initialize theme +initTheme(); + +// Start the main application +main(); diff --git a/src/utils/alerts.js b/src/utils/alerts.js new file mode 100644 index 00000000..0b939faf --- /dev/null +++ b/src/utils/alerts.js @@ -0,0 +1,14 @@ +/** + * Alert utilities + */ + +/** + * Shows an alert only if the message has changed + * @param {string} Message - The message to display + */ +export let SmartAlert = (Message) => { + if (localStorage.getItem("UserScript-Alert") !== Message) { + alert(Message); + } + localStorage.setItem("UserScript-Alert", Message); +}; diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 00000000..36694a73 --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,82 @@ +/** + * API request utilities + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Make an API request to the backend + * @param {string} Action - The API action + * @param {Object} Data - The data to send + * @param {Function} CallBack - Callback function to handle response + */ +export let RequestAPI = (Action, Data, CallBack) => { + try { + let Session = ""; + let Temp = document.cookie.split(";"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].includes("PHPSESSID")) { + Session = Temp[i].split("=")[1]; + } + } + if (Session === "") { //The cookie is httpOnly + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + location.reload(); //Refresh the page to auth with the new PHPSESSID + }) + .catch((error) => { + console.error(error); + }); + } + + // Get current username from profile + let CurrentUsername = ""; + if (document.querySelector("#profile") !== null) { + CurrentUsername = document.querySelector("#profile").innerText; + CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); + } + + let PostData = { + "Authentication": { + "SessionID": Session, "Username": CurrentUsername, + }, "Data": Data, "Version": GM_info.script.version, "DebugMode": UtilityEnabled("DebugMode") + }; + let DataString = JSON.stringify(PostData); + if (UtilityEnabled("DebugMode")) { + console.log("Sent for", Action + ":", DataString); + } + GM_xmlhttpRequest({ + method: "POST", + url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-bbs.me/") + Action, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "XMOJ-UserID": CurrentUsername, + "XMOJ-Script-Version": GM_info.script.version, + "DebugMode": UtilityEnabled("DebugMode") + }, + data: DataString, + onload: (Response) => { + if (UtilityEnabled("DebugMode")) { + console.log("Received for", Action + ":", Response.responseText); + } + try { + CallBack(JSON.parse(Response.responseText)); + } catch (Error) { + console.log(Response.responseText); + } + } + }); + } 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."); + } + } +}; diff --git a/src/utils/credentials.js b/src/utils/credentials.js new file mode 100644 index 00000000..a45542a0 --- /dev/null +++ b/src/utils/credentials.js @@ -0,0 +1,47 @@ +/** + * Credential storage utilities using the Credentials API + */ + +/** + * Store user credentials + * @param {string} username - Username + * @param {string} password - Password + */ +export let storeCredential = async (username, password) => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + const credential = new PasswordCredential({id: username, password: password}); + await navigator.credentials.store(credential); + } catch (e) { + console.error(e); + } + } +}; + +/** + * Get stored credentials + * @returns {Promise} The stored credentials or null + */ +export let getCredential = async () => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + return await navigator.credentials.get({password: true, mediation: 'optional'}); + } catch (e) { + console.error(e); + } + } + return null; +}; + +/** + * Clear stored credentials + */ +export let clearCredential = async () => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + await navigator.credentials.preventSilentAccess(); + } catch (e) { + console.error(e); + } + } +}; diff --git a/src/utils/format.js b/src/utils/format.js new file mode 100644 index 00000000..b43a0cb9 --- /dev/null +++ b/src/utils/format.js @@ -0,0 +1,62 @@ +/** + * Formatting utilities for sizes and other values + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Converts a memory size in bytes to a human-readable string representation. + * @param {number} Memory - The memory size in bytes. + * @returns {string} The human-readable string representation of the memory size. + */ +export let SizeToStringSize = (Memory) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Memory < 1024) { + return Memory + "KB"; + } else if (Memory < 1024 * 1024) { + return (Memory / 1024).toFixed(2) + "MB"; + } else if (Memory < 1024 * 1024 * 1024) { + return (Memory / 1024 / 1024).toFixed(2) + "GB"; + } else { + return (Memory / 1024 / 1024 / 1024).toFixed(2) + "TB"; + } + } else { + return Memory; + } + } 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."); + } + } +}; + +/** + * Converts a code size in bytes to a human-readable string representation. + * @param {number} Memory - The code size in bytes. + * @returns {string} The human-readable string representation of the code size. + */ +export let CodeSizeToStringSize = (Memory) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Memory < 1024) { + return Memory + "B"; + } else if (Memory < 1024 * 1024) { + return (Memory / 1024).toFixed(2) + "KB"; + } else if (Memory < 1024 * 1024 * 1024) { + return (Memory / 1024 / 1024).toFixed(2) + "MB"; + } else { + return (Memory / 1024 / 1024 / 1024).toFixed(2) + "GB"; + } + } else { + return Memory; + } + } 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."); + } + } +}; diff --git a/src/utils/html.js b/src/utils/html.js new file mode 100644 index 00000000..c8db56aa --- /dev/null +++ b/src/utils/html.js @@ -0,0 +1,43 @@ +/** + * HTML utilities for escaping and purifying HTML content + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Escapes HTML special characters + * @param {string} str - The string to escape + * @returns {string} The escaped string + */ +export let escapeHTML = (str) => { + return str.replace(/[&<>"']/g, function (match) { + const escape = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return escape[match]; + }); +}; + +/** + * Purifies HTML content using DOMPurify + * @param {string} Input - The HTML content to purify + * @returns {string} The purified HTML content + */ +export let PurifyHTML = (Input) => { + try { + return DOMPurify.sanitize(Input, { + "ALLOWED_TAGS": ["a", "b", "big", "blockquote", "br", "code", "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "hr", "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "ul", "var"], + "ALLOWED_ATTR": ["abbr", "accept", "accept-charset", "accesskey", "action", "align", "alt", "axis", "border", "cellpadding", "cellspacing", "char", "charoff", "charset", "checked", "cite", "clear", "color", "cols", "colspan", "compact", "coords", "datetime", "dir", "disabled", "enctype", "for", "frame", "headers", "height", "href", "hreflang", "hspace", "ismap", "itemprop", "label", "lang", "longdesc", "maxlength", "media", "method", "multiple", "name", "nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev", "rows", "rowspan", "rules", "scope", "selected", "shape", "size", "span", "src", "start", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "vspace", "width"] + }); + } 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."); + } + } +}; diff --git a/src/utils/mathjax.js b/src/utils/mathjax.js new file mode 100644 index 00000000..e90aa65e --- /dev/null +++ b/src/utils/mathjax.js @@ -0,0 +1,30 @@ +/** + * MathJax rendering utilities + */ + +/** + * Render MathJax on the page + */ +export let RenderMathJax = async () => { + try { + if (document.getElementById("MathJax-script") === null) { + var ScriptElement = document.createElement("script"); + ScriptElement.id = "MathJax-script"; + ScriptElement.type = "text/javascript"; + ScriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.0.5/es5/tex-chtml.js"; + document.body.appendChild(ScriptElement); + await new Promise((Resolve) => { + ScriptElement.onload = () => { + Resolve(); + }; + }); + } + if (typeof MathJax !== 'undefined') { //If there is a Math expression + MathJax.startup.input[0].findTeX.options.inlineMath.push(["$", "$"]); + MathJax.startup.input[0].findTeX.getPatterns(); + MathJax.typeset(); + } + } catch (e) { + console.error(e); + } +}; diff --git a/src/utils/table.js b/src/utils/table.js new file mode 100644 index 00000000..c4970810 --- /dev/null +++ b/src/utils/table.js @@ -0,0 +1,24 @@ +/** + * Table utilities for styling and tidying up tables + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Tidies up the given table by applying Bootstrap styling and removing unnecessary attributes. + * + * @param {HTMLElement} Table - The table element to be tidied up. + */ +export let TidyTable = (Table) => { + try { + if (UtilityEnabled("NewBootstrap") && Table != null) { + Table.className = "table table-hover"; + } + } 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."); + } + } +}; diff --git a/src/utils/time.js b/src/utils/time.js new file mode 100644 index 00000000..18665024 --- /dev/null +++ b/src/utils/time.js @@ -0,0 +1,101 @@ +/** + * Time utilities for formatting and converting time values + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Calculates the relative time based on the input date. + * @param {string|Date} Input - The input date. + * @returns {string} The relative time in a formatted string. + */ +export let GetRelativeTime = (Input) => { + try { + Input = new Date(parseInt(Input)); + let Now = new Date().getTime(); + let Delta = Now - Input.getTime(); + let RelativeName = ""; + if (Delta < 0) { + RelativeName = "未来"; + } else if (Delta <= 1000 * 60) { + RelativeName = "刚刚"; + } else if (Delta <= 1000 * 60 * 60) { + RelativeName = Math.floor((Now - Input) / 1000 / 60) + "分钟前"; + } else if (Delta <= 1000 * 60 * 60 * 24) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60) + "小时前"; + } else if (Delta <= 1000 * 60 * 60 * 24 * 31) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24) + "天前"; + } else if (Delta <= 1000 * 60 * 60 * 24 * 365) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 31) + "个月前"; + } else { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 365) + "年前"; + } + return "" + RelativeName + ""; + } 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."); + } + } +}; + +/** + * Converts the given number of seconds to a formatted string representation of hours, minutes, and seconds. + * @param {number} InputSeconds - The number of seconds to convert. + * @returns {string} The formatted string representation of the input seconds. + */ +export let SecondsToString = (InputSeconds) => { + try { + let Hours = Math.floor(InputSeconds / 3600); + let Minutes = Math.floor((InputSeconds % 3600) / 60); + let Seconds = InputSeconds % 60; + return (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; + } 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."); + } + } +}; + +/** + * Converts a string in the format "hh:mm:ss" to the equivalent number of seconds. + * @param {string} InputString - The input string to convert. + * @returns {number} The number of seconds equivalent to the input string. + */ +export let StringToSeconds = (InputString) => { + try { + let SplittedString = InputString.split(":"); + return parseInt(SplittedString[0]) * 60 * 60 + parseInt(SplittedString[1]) * 60 + parseInt(SplittedString[2]); + } 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."); + } + } +}; + +/** + * Converts a time value to a string representation. + * @param {number} Time - The time value to convert. + * @returns {string|number} - The converted time value as a string, or the original value if UtilityEnabled("AddUnits") is false. + */ +export let TimeToStringTime = (Time) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Time < 1000) { + return Time + "ms"; + } else if (Time < 1000 * 60) { + return (Time / 1000).toFixed(2) + "s"; + } + } else { + return Time; + } + } 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."); + } + } +}; diff --git a/src/utils/user.js b/src/utils/user.js new file mode 100644 index 00000000..997d4865 --- /dev/null +++ b/src/utils/user.js @@ -0,0 +1,172 @@ +/** + * User information utilities + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; +import { RequestAPI } from './api.js'; +import { AdminUserList } from '../core/constants.js'; + +/** + * Get user information + * @param {string} Username - The username + * @returns {Promise} User info object with Rating and EmailHash + */ +export 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) { + return { + "Rating": localStorage.getItem("UserScript-User-" + Username + "-UserRating"), + "EmailHash": localStorage.getItem("UserScript-User-" + Username + "-EmailHash") + } + } + return await fetch("https://www.xmoj.tech/userinfo.php?user=" + Username).then((Response) => { + return Response.text(); + }).then((Response) => { + if (Response.indexOf("No such User!") !== -1) { + return null; + } + const ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Rating = (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; + let Temp = ParsedDocument.querySelector("#statics > tbody").children; + let Email = Temp[Temp.length - 1].children[1].innerText.trim(); + let EmailHash = CryptoJS.MD5(Email).toString(); + localStorage.setItem("UserScript-User-" + Username + "-UserRating", Rating); + if (Email == "") { + EmailHash = undefined; + } else { + localStorage.setItem("UserScript-User-" + Username + "-EmailHash", EmailHash); + } + localStorage.setItem("UserScript-User-" + Username + "-LastUpdateTime", new Date().getTime()); + return { + "Rating": Rating, "EmailHash": EmailHash + } + }); + } 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."); + } + } +}; + +/** + * Retrieves the badge information for a given user. + * + * @param {string} Username - The username of the user. + * @returns {Promise} - A promise that resolves to an object containing the badge information. + * @property {string} BackgroundColor - The background color of the badge. + * @property {string} Color - The color of the badge. + * @property {string} Content - The content of the badge. + */ +export let GetUserBadge = async (Username) => { + try { + if (localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime") != null && new Date().getTime() - parseInt(localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime")) < 1000 * 60 * 60 * 24) { + return { + "BackgroundColor": localStorage.getItem("UserScript-User-" + Username + "-Badge-BackgroundColor"), + "Color": localStorage.getItem("UserScript-User-" + Username + "-Badge-Color"), + "Content": localStorage.getItem("UserScript-User-" + Username + "-Badge-Content") + } + } else { + let BackgroundColor = ""; + let Color = ""; + let Content = ""; + await new Promise((Resolve) => { + RequestAPI("GetBadge", { + "UserID": String(Username) + }, (Response) => { + if (Response.Success) { + BackgroundColor = Response.Data.BackgroundColor; + Color = Response.Data.Color; + Content = Response.Data.Content; + } + Resolve(); + }); + }); + localStorage.setItem("UserScript-User-" + Username + "-Badge-BackgroundColor", BackgroundColor); + localStorage.setItem("UserScript-User-" + Username + "-Badge-Color", Color); + localStorage.setItem("UserScript-User-" + Username + "-Badge-Content", Content); + localStorage.setItem("UserScript-User-" + Username + "-Badge-LastUpdateTime", String(new Date().getTime())); + return { + "BackgroundColor": BackgroundColor, "Color": Color, "Content": Content + } + } + } 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."); + } + } +}; + +/** + * Sets the HTML content of an element to display a username with optional additional information. + * @param {HTMLElement} Element - The element to set the HTML content. + * @param {string} Username - The username to display. + * @param {boolean} [Simple=false] - Indicates whether to display additional information or not. + * @param {string} [Href="https://www.xmoj.tech/userinfo.php?user="] - The URL to link the username to. + * @returns {Promise} - A promise that resolves when the HTML content is set. + */ +export let GetUsernameHTML = async (Element, Username, Simple = false, Href = "https://www.xmoj.tech/userinfo.php?user=") => { + try { + //Username = Username.replaceAll(/[^a-zA-Z0-9]/g, ""); + let ID = "Username-" + Username + "-" + Math.random(); + Element.id = ID; + Element.innerHTML = `
    `; + Element.appendChild(document.createTextNode(Username)); + let UserInfo = await GetUserInfo(Username); + if (UserInfo === null) { + document.getElementById(ID).innerHTML = ""; + document.getElementById(ID).appendChild(document.createTextNode(Username)); + return; + } + let HTMLData = ""; + if (!Simple) { + HTMLData += ``; + } + HTMLData += ` 500) { + HTMLData += "link-danger"; + } else if (Rating >= 400) { + HTMLData += "link-warning"; + } else if (Rating >= 300) { + HTMLData += "link-success"; + } else { + HTMLData += "link-info"; + } + } else { + HTMLData += "link-info"; + } + HTMLData += `\";">`; + if (!Simple) { + if (AdminUserList.includes(Username)) { + HTMLData += `脚本管理员`; + } + let BadgeInfo = await GetUserBadge(Username); + if (BadgeInfo.Content != "") { + HTMLData += `${BadgeInfo.Content}`; + } + } + if (document.getElementById(ID) !== null) { + document.getElementById(ID).innerHTML = HTMLData; + document.getElementById(ID).getElementsByTagName("a")[0].appendChild(document.createTextNode(Username)); + } + } 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."); + } + } +}; diff --git a/src/utils/version.js b/src/utils/version.js new file mode 100644 index 00000000..d86aa324 --- /dev/null +++ b/src/utils/version.js @@ -0,0 +1,26 @@ +/** + * Version comparison utilities + */ + +/** + * Compares two version strings + * @param {string} currVer - Current version + * @param {string} remoteVer - Remote version + * @returns {boolean} True if update is needed + */ +export function compareVersions(currVer, remoteVer) { + const currParts = currVer.split('.').map(Number); + const remoteParts = remoteVer.split('.').map(Number); + + const maxLen = Math.max(currParts.length, remoteParts.length); + for (let i = 0; i < maxLen; i++) { + const curr = currParts[i] !== undefined ? currParts[i] : 0; + const remote = remoteParts[i] !== undefined ? remoteParts[i] : 0; + if (remote > curr) { + return true; // update needed + } else if (remote < curr) { + return false; // no update needed + } + } + return false; // versions are equal +} From 43ba2179e9859d0e01e8d9cedd6c7d6a88146fff Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Nov 2025 10:40:49 +0000 Subject: [PATCH 05/26] Extract individual features into separate modules This commit demonstrates the feature extraction pattern by extracting 5 major features into separate, maintainable modules: Extracted Features: - AutoLogin: Automatic redirection to login page - Discussion: Complete forum system with 777 lines of code - CopySamples: Fix copy functionality for test samples - CompareSource: Side-by-side code comparison with diff - RemoveUseless: Remove unwanted page elements Structure Added: - src/features/auto-login.js - src/features/discussion.js - src/features/copy-samples.js - src/features/compare-source.js - src/features/remove-useless.js - src/features/index.js (feature loader) Each feature module follows the pattern: - Imports UtilityEnabled from config - Exports an init() function - Self-checks if enabled before executing - Proper ES6 module structure Updated Files: - src/main.js: Added feature initialization - README_REFACTORING.md: Added feature extraction documentation Remaining Features: 36 features remain in bootstrap.js and can be extracted following the same pattern. The infrastructure is in place for incremental extraction. Note: Features currently exist in both bootstrap.js and feature modules for compatibility. Future work can remove duplicates from bootstrap.js. --- README_REFACTORING.md | 64 +- dist/XMOJ.user.js | 1307 ++++++++++++++++++++++++++++++++ src/features/auto-login.js | 44 ++ src/features/compare-source.js | 171 +++++ src/features/copy-samples.js | 54 ++ src/features/discussion.js | 888 ++++++++++++++++++++++ src/features/index.js | 64 ++ src/features/remove-useless.js | 85 +++ src/main.js | 12 + 9 files changed, 2679 insertions(+), 10 deletions(-) create mode 100644 src/features/auto-login.js create mode 100644 src/features/compare-source.js create mode 100644 src/features/copy-samples.js create mode 100644 src/features/discussion.js create mode 100644 src/features/index.js create mode 100644 src/features/remove-useless.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md index 492d627a..093dca11 100644 --- a/README_REFACTORING.md +++ b/README_REFACTORING.md @@ -93,19 +93,63 @@ The userscript includes the following features (controlled via `UtilityEnabled`) - Translate - Translation features - UploadStd - Upload standard solutions -## Future Improvements +## Extracted Features -Individual features should be extracted into separate modules under `src/features/` for better organization and maintainability. Each feature module should: +The following features have been extracted into separate modules under `src/features/`: -1. Export an initialization function -2. Only activate when `UtilityEnabled("FeatureName")` returns true -3. Contain all code specific to that feature +### Currently Extracted +- **AutoLogin** (`auto-login.js`) - Automatically redirects to login page when not authenticated +- **Discussion** (`discussion.js`) - Complete forum system with post creation, viewing, and replies +- **CopySamples** (`copy-samples.js`) - Fixes copy functionality for test samples +- **CompareSource** (`compare-source.js`) - Side-by-side code comparison with diff highlighting +- **RemoveUseless** (`remove-useless.js`) - Removes unwanted page elements (marquees, footers, etc.) -This allows for: -- Better code organization -- Easier testing of individual features -- Ability to enable/disable features independently -- Simpler debugging +### Feature Extraction Pattern + +Each feature module follows this pattern: + +```javascript +import { UtilityEnabled } from '../core/config.js'; +// Import other needed utilities... + +/** + * Initialize the FeatureName feature + */ +export function init(context) { + // Check if feature is enabled + if (!UtilityEnabled("FeatureName")) { + return; + } + + // Feature implementation... +} +``` + +### Adding New Features + +To extract additional features from `bootstrap.js`: + +1. Identify code that checks `UtilityEnabled("FeatureName")` +2. Create `src/features/feature-name.js` with the extracted code +3. Add import to `src/features/index.js` +4. Add initialization call in `initializeFeatures()` +5. Test the build with `npm run build` + +### Remaining Features to Extract + +The following features remain in `bootstrap.js` and can be extracted following the pattern above: + +- AddAnimation, AddColorText, AddUnits +- ApplyData, AutoCheat, AutoCountdown, AutoO2, AutoRefresh +- BBSPopup, CompileError, CopyMD +- DarkMode (theme already extracted, check for additional code) +- DebugMode, DownloadPlayback, ExportACCode +- IOFile, ImproveACRate, LoginFailed, MessagePopup +- MoreSTD, NewBootstrap, NewDownload, NewTopBar +- OpenAllProblem, ProblemSwitcher +- RefreshSolution, RemoveAlerts +- ReplaceLinks, ReplaceXM, ReplaceYN, ResetType +- SavePassword, Translate, UploadStd ## Development diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js index 61c66ab6..26b3637f 100644 --- a/dist/XMOJ.user.js +++ b/dist/XMOJ.user.js @@ -5084,6 +5084,1304 @@ int main() }); } + /** + * Auto Login Feature + * Automatically redirects to login page when user is not logged in + */ + + + /** + * Initialize auto login feature + * Checks if user is logged in and redirects to login page if necessary + */ + function init$4() { + // Only execute if AutoLogin feature is enabled + if (!UtilityEnabled("AutoLogin")) { + return; + } + + // Check if navbar exists (indicates page is loaded) + if (document.querySelector("#navbar") === null) { + return; + } + + // Check if profile element exists + const profileElement = document.querySelector("#profile"); + if (profileElement === null) { + return; + } + + // Check if user is not logged in (profile shows "登录" = "Login") + const isNotLoggedIn = profileElement.innerHTML === "登录"; + + // Exclude login-related pages from auto-redirect + const excludedPaths = ["/login.php", "/loginpage.php", "/lostpassword.php"]; + const isExcludedPath = excludedPaths.includes(location.pathname); + + // If user is not logged in and not already on a login page, redirect + if (isNotLoggedIn && !isExcludedPath) { + // Save current page to return after login + localStorage.setItem("UserScript-LastPage", location.pathname + location.search); + + // Redirect to login page + location.href = "https://www.xmoj.tech/loginpage.php"; + } + } + + /** + * Discussion Feature + * Provides discussion forum functionality including discussion list, threads, and replies + * Feature ID: Discussion + * Type: A (Add) + * Description: Adds discussion forum with navbar link, problem page integration, and full forum pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 205-210: Navbar link creation + * - Lines 1284-1317: Discussion button on problem pages + * - Lines 3609-4386: Discussion forum pages (list, new post, thread view) + */ + + + /** + * Initialize discussion feature + * @param {Object} context - Context object containing required dependencies + * @param {string} context.CurrentUsername - Current logged in username + * @param {URLSearchParams} context.SearchParams - URL search parameters + * @param {boolean} context.IsAdmin - Whether current user is admin + * @param {HTMLStyleElement} context.Style - Style element for adding CSS + * @param {string} context.CaptchaSiteKey - Cloudflare turnstile site key + * @param {Function} context.GetUsernameHTML - Function to render username with profile link + * @param {Function} context.PurifyHTML - Function to sanitize HTML content + * @param {Function} context.RenderMathJax - Function to render math formulas + */ + function init$3(context) { + // Only execute if Discussion feature is enabled + if (!UtilityEnabled("Discussion")) { + return; + } + + const { + CurrentUsername, + SearchParams, + IsAdmin, + Style, + CaptchaSiteKey, + GetUsernameHTML, + PurifyHTML, + RenderMathJax + } = context; + + // Part 1: Create navbar link (lines 205-210) + let Discussion = null; + if (document.querySelector("#navbar > ul:nth-child(1)")) { + Discussion = document.createElement("li"); + document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); + Discussion.innerHTML = "讨论"; + } + + // Part 2: Discussion button on problem pages (lines 1284-1317) + if (location.pathname === "/problem.php") { + const centerElement = document.querySelector("body > div > div.mt-3 > center"); + if (centerElement) { + let PID = null; + if (SearchParams.get("cid") != null) { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + } else { + PID = SearchParams.get("id"); + } + + if (PID) { + let DiscussButton = document.createElement("button"); + DiscussButton.className = "btn btn-outline-secondary position-relative"; + DiscussButton.innerHTML = `讨论`; + DiscussButton.style.marginLeft = "10px"; + DiscussButton.type = "button"; + DiscussButton.addEventListener("click", () => { + if (SearchParams.get("cid") != null) { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); + } else { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); + } + }); + centerElement.appendChild(DiscussButton); + + let UnreadBadge = document.createElement("span"); + UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + UnreadBadge.style.display = "none"; + DiscussButton.appendChild(UnreadBadge); + + let RefreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (Response) => { + if (Response.Success) { + if (Response.Data.DiscussCount != 0) { + UnreadBadge.innerText = Response.Data.DiscussCount; + UnreadBadge.style.display = ""; + } + } + }); + }; + RefreshCount(); + addEventListener("focus", RefreshCount); + } + } + } + + // Part 3: Discussion forum pages (lines 3609-4386) + if (location.pathname.indexOf("/discuss3") != -1) { + if (Discussion) { + Discussion.classList.add("active"); + } + + // Discussion list page + if (location.pathname == "/discuss3/discuss.php") { + document.title = "讨论列表"; + let ProblemID = parseInt(SearchParams.get("pid")); + let BoardID = parseInt(SearchParams.get("bid")); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    讨论列表${(isNaN(ProblemID) ? "" : ` - 题目` + ProblemID)}

    + + +
    + + + + + + + + + + + + + + + +
    编号标题作者题目编号发布时间回复数最后回复
    `; + NewPost.addEventListener("click", () => { + if (!isNaN(ProblemID)) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?pid=" + ProblemID; + } else if (SearchParams.get("bid") != null) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?bid=" + SearchParams.get("bid"); + } else { + location.href = "https://www.xmoj.tech/discuss3/newpost.php"; + } + }); + const RefreshPostList = (Silent = true) => { + if (!Silent) { + PostList.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + for (let j = 0; j < 7; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("GetPosts", { + "ProblemID": Number(ProblemID || 0), + "Page": Number(Page), + "BoardID": Number(SearchParams.get("bid") || -1) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + ErrorElement.style.display = "none"; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + } + let Posts = ResponseData.Data.Posts; + PostList.children[1].innerHTML = ""; + if (Posts.length == 0) { + PostList.children[1].innerHTML = `暂无数据`; + } + for (let i = 0; i < Posts.length; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + let IDCell = document.createElement("td"); + Row.appendChild(IDCell); + IDCell.innerText = Posts[i].PostID + " " + Posts[i].BoardName; + let TitleCell = document.createElement("td"); + Row.appendChild(TitleCell); + let TitleLink = document.createElement("a"); + TitleCell.appendChild(TitleLink); + TitleLink.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + Posts[i].PostID; + if (Posts[i].Lock.Locked) { + TitleLink.classList.add("link-secondary"); + TitleLink.innerHTML = "🔒 "; + } + TitleLink.innerHTML += Posts[i].Title; + let AuthorCell = document.createElement("td"); + Row.appendChild(AuthorCell); + GetUsernameHTML(AuthorCell, Posts[i].UserID); + let ProblemIDCell = document.createElement("td"); + Row.appendChild(ProblemIDCell); + if (Posts[i].ProblemID != 0) { + let ProblemIDLink = document.createElement("a"); + ProblemIDCell.appendChild(ProblemIDLink); + ProblemIDLink.href = "https://www.xmoj.tech/problem.php?id=" + Posts[i].ProblemID; + ProblemIDLink.innerText = Posts[i].ProblemID; + } + let PostTimeCell = document.createElement("td"); + Row.appendChild(PostTimeCell); + PostTimeCell.innerHTML = GetRelativeTime(Posts[i].PostTime); + let ReplyCountCell = document.createElement("td"); + Row.appendChild(ReplyCountCell); + ReplyCountCell.innerText = Posts[i].ReplyCount; + let LastReplyTimeCell = document.createElement("td"); + Row.appendChild(LastReplyTimeCell); + LastReplyTimeCell.innerHTML = GetRelativeTime(Posts[i].LastReplyTime); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }; + RefreshPostList(false); + addEventListener("focus", RefreshPostList); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php"; + LinkElement.classList.add("me-2"); + LinkElement.innerText = "全部"; + GotoBoard.appendChild(LinkElement); + for (let i = 0; i < ResponseData.Data.Boards.length; i++) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php?bid=" + ResponseData.Data.Boards[i].BoardID; + LinkElement.classList.add("me-2"); + LinkElement.innerText = ResponseData.Data.Boards[i].BoardName; + GotoBoard.appendChild(LinkElement); + } + } + }); + } else if (location.pathname == "/discuss3/newpost.php") { + // New post page implementation + let ProblemID = parseInt(SearchParams.get("pid")); + document.querySelector("body > div > div").innerHTML = `

    发布新讨论` + (!isNaN(ProblemID) ? ` - 题目` + ProblemID : ``) + `

    +
    + +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + ContentElement.classList.remove("is-invalid"); + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + TitleElement.addEventListener("input", () => { + TitleElement.classList.remove("is-invalid"); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + let Title = TitleElement.value; + let Content = ContentElement.value; + let ProblemID = parseInt(SearchParams.get("pid")); + if (Title === "") { + TitleElement.classList.add("is-invalid"); + return; + } + if (Content === "") { + ContentElement.classList.add("is-invalid"); + return; + } + if (document.querySelector("#Board input:checked") === null) { + ErrorElement.innerText = "请选择要发布的板块"; + ErrorElement.style.display = "block"; + return; + } + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewPost", { + "Title": String(Title), + "Content": String(Content), + "ProblemID": Number(isNaN(ProblemID) ? 0 : ProblemID), + "CaptchaSecretKey": String(CaptchaSecretKey), + "BoardID": Number(document.querySelector("#Board input:checked").value) + }, (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ResponseData.Data.PostID; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let Data = ResponseData.Data.Boards; + for (let i = 0; i < Data.length; i++) { + let RadioElement = document.createElement("div"); + RadioElement.className = "col-auto form-check form-check-inline"; + let RadioInput = document.createElement("input"); + RadioInput.className = "form-check-input"; + RadioInput.type = "radio"; + RadioInput.name = "Board"; + RadioInput.id = "Board" + Data[i].BoardID; + RadioInput.value = Data[i].BoardID; + RadioElement.appendChild(RadioInput); + if (SearchParams.get("bid") !== null && SearchParams.get("bid") == Data[i].BoardID) { + RadioInput.checked = true; + } + if (!isNaN(ProblemID)) { + RadioInput.disabled = true; + } + if (Data[i].BoardID == 4) { + if (!isNaN(ProblemID)) RadioInput.checked = true; + RadioInput.disabled = true; + } + let RadioLabel = document.createElement("label"); + RadioLabel.className = "form-check-label"; + RadioLabel.htmlFor = "Board" + Data[i].BoardID; + RadioLabel.innerText = Data[i].BoardName; + RadioElement.appendChild(RadioLabel); + Board.appendChild(RadioElement); + } + } + }); + } else if (location.pathname == "/discuss3/thread.php") { + // Thread view page implementation + if (SearchParams.get("tid") == null) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + let ThreadID = SearchParams.get("tid"); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    +
    + 作者:
    + 发布时间: + 板块: + + + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + let RefreshReply = (Silent = true) => { + if (!Silent) { + PostTitle.innerHTML = ``; + PostAuthor.innerHTML = ``; + PostTime.innerHTML = ``; + PostBoard.innerHTML = ``; + PostReplies.innerHTML = ""; + for (let i = 0; i < 10; i++) { + PostReplies.innerHTML += `
    +
    +
    + + +
    +
    + + + +
    +
    `; + } + } + RequestAPI("GetPost", { + "PostID": Number(ThreadID), "Page": Number(Page) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + let OldScrollTop = document.documentElement.scrollTop; + let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { + Delete.style.display = ""; + } + } + PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); + document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; + PostAuthor.innerHTML = ""; + GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); + PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); + PostBoard.innerHTML = ResponseData.Data.BoardName; + let Replies = ResponseData.Data.Reply; + PostReplies.innerHTML = ""; + for (let i = 0; i < Replies.length; i++) { + let CardElement = document.createElement("div"); + PostReplies.appendChild(CardElement); + CardElement.className = "card mb-3"; + let CardBodyElement = document.createElement("div"); + CardElement.appendChild(CardBodyElement); + CardBodyElement.className = "card-body row"; + let CardBodyRowElement = document.createElement("div"); + CardBodyElement.appendChild(CardBodyRowElement); + CardBodyRowElement.className = "row mb-3"; + let AuthorElement = document.createElement("span"); + CardBodyRowElement.appendChild(AuthorElement); + AuthorElement.className = "col-4 text-muted"; + let AuthorSpanElement = document.createElement("span"); + AuthorElement.appendChild(AuthorSpanElement); + AuthorSpanElement.innerText = "作者:"; + let AuthorUsernameElement = document.createElement("span"); + AuthorElement.appendChild(AuthorUsernameElement); + GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); + let SendTimeElement = document.createElement("span"); + CardBodyRowElement.appendChild(SendTimeElement); + SendTimeElement.className = "col-4 text-muted"; + SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); + + let OKButton; + if (!LockButtons) { + let ButtonsElement = document.createElement("span"); + CardBodyRowElement.appendChild(ButtonsElement); + ButtonsElement.className = "col-4"; + let ReplyButton = document.createElement("button"); + ButtonsElement.appendChild(ReplyButton); + ReplyButton.type = "button"; + ReplyButton.className = "btn btn-sm btn-info"; + ReplyButton.innerText = "回复"; + ReplyButton.addEventListener("click", () => { + let Content = Replies[i].Content; + Content = Content.split("\n").map((Line) => { + // Count the number of '>' characters at the beginning of the line + let nestingLevel = 0; + while (Line.startsWith(">")) { + nestingLevel++; + Line = Line.substring(1).trim(); + } + // If the line is nested more than 2 levels deep, skip it + if (nestingLevel > 2) { + return null; + } + // Reconstruct the line with the appropriate number of '>' characters + return "> ".repeat(nestingLevel + 1) + Line; + }).filter(Line => Line !== null) // Remove null entries + .join("\n"); + ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; + ContentElement.focus(); + }); + let DeleteButton = document.createElement("button"); + ButtonsElement.appendChild(DeleteButton); + DeleteButton.type = "button"; + DeleteButton.className = "btn btn-sm btn-danger ms-1"; + DeleteButton.innerText = "删除"; + DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + DeleteButton.addEventListener("click", () => { + DeleteButton.disabled = true; + DeleteButton.lastChild.style.display = ""; + RequestAPI("DeleteReply", { + "ReplyID": Number(Replies[i].ReplyID) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + DeleteButton.disabled = false; + DeleteButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let DeleteSpin = document.createElement("div"); + DeleteButton.appendChild(DeleteSpin); + DeleteSpin.className = "spinner-border spinner-border-sm"; + DeleteSpin.role = "status"; + DeleteSpin.style.display = "none"; + OKButton = document.createElement("button"); + ButtonsElement.appendChild(OKButton); + OKButton.type = "button"; + OKButton.style.display = "none"; + OKButton.className = "btn btn-sm btn-success ms-1"; + OKButton.innerText = "确认"; + let OKSpin = document.createElement("div"); + OKButton.appendChild(OKSpin); + OKSpin.className = "spinner-border spinner-border-sm"; + OKSpin.role = "status"; + OKSpin.style.display = "none"; + OKButton.addEventListener("click", () => { + OKButton.disabled = true; + OKButton.lastChild.style.display = ""; + RequestAPI("EditReply", { + ReplyID: Number(Replies[i].ReplyID), + Content: String(ContentEditor.value) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + OKButton.disabled = false; + OKButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let CancelButton = document.createElement("button"); + ButtonsElement.appendChild(CancelButton); + CancelButton.type = "button"; + CancelButton.style.display = "none"; + CancelButton.className = "btn btn-sm btn-secondary ms-1"; + CancelButton.innerText = "取消"; + CancelButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = ""; + CardBodyElement.children[3].style.display = "none"; + EditButton.style.display = ""; + OKButton.style.display = "none"; + CancelButton.style.display = "none"; + }); + let EditButton = document.createElement("button"); + ButtonsElement.appendChild(EditButton); + EditButton.type = "button"; + EditButton.className = "btn btn-sm btn-warning ms-1"; + EditButton.innerText = "编辑"; + EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + EditButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = "none"; + CardBodyElement.children[3].style.display = ""; + EditButton.style.display = "none"; + OKButton.style.display = ""; + CancelButton.style.display = ""; + }); + } + + let CardBodyHRElement = document.createElement("hr"); + CardBodyElement.appendChild(CardBodyHRElement); + + let ReplyContentElement = document.createElement("div"); + CardBodyElement.appendChild(ReplyContentElement); + ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + if (Replies[i].EditTime != null) { + if (Replies[i].EditPerson == Replies[i].UserID) { + ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } else { + ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } + } + let ContentEditElement = document.createElement("div"); + CardBodyElement.appendChild(ContentEditElement); + ContentEditElement.classList.add("input-group"); + ContentEditElement.style.display = "none"; + let ContentEditor = document.createElement("textarea"); + ContentEditElement.appendChild(ContentEditor); + ContentEditor.className = "form-control col-6"; + ContentEditor.rows = 3; + ContentEditor.value = Replies[i].Content; + if (ContentEditor.value.indexOf("
    ") != -1) { + ContentEditor.value = ContentEditor.value.substring(0, ContentEditor.value.indexOf("
    ")); + } + ContentEditor.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + OKButton.click(); + } + }); + let PreviewTab = document.createElement("div"); + ContentEditElement.appendChild(PreviewTab); + PreviewTab.className = "form-control col-6"; + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + ContentEditor.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + RenderMathJax(); + }); + ContentEditor.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentEditor.value.substring(0, ContentEditor.selectionStart); + let After = ContentEditor.value.substring(ContentEditor.selectionEnd, ContentEditor.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentEditor.value = Before + UploadMessage + After; + ContentEditor.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentEditor.dispatchEvent(new Event("input")); + } else { + ContentEditor.value = Before + `![上传失败!]()` + After; + ContentEditor.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + } + + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + + let CodeElements = document.querySelectorAll("#PostReplies > div > div > div:nth-child(3) > pre > code"); + for (let i = 0; i < CodeElements.length; i++) { + let ModeName = "text/x-c++src"; + if (CodeElements[i].className == "language-c") { + ModeName = "text/x-csrc"; + } else if (CodeElements[i].className == "language-cpp") { + ModeName = "text/x-c++src"; + } + CodeMirror(CodeElements[i].parentElement, { + value: CodeElements[i].innerText, + mode: ModeName, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + lineNumbers: true, + readOnly: true + }).setSize("100%", "auto"); + CodeElements[i].remove(); + } + + if (LockButtons) { + let LockElement = ContentElement.parentElement.parentElement; + LockElement.innerHTML = "讨论已于 " + await GetRelativeTime(ResponseData.Data.Lock.LockTime) + " 被 "; + let LockUsernameSpan = document.createElement("span"); + LockElement.appendChild(LockUsernameSpan); + GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); + LockElement.innerHTML += " 锁定"; + LockElement.classList.add("mb-5"); + } + + if (IsAdmin) { + ToggleLock.style.display = "inline-block"; + ToggleLockButton.checked = ResponseData.Data.Lock.Locked; + ToggleLockButton.onclick = () => { + ToggleLockButton.disabled = true; + ErrorElement.style.display = "none"; + RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { + "PostID": Number(ThreadID) + }, (LockResponseData) => { + ToggleLockButton.disabled = false; + if (LockResponseData.Success) { + RefreshReply(); + } else { + ErrorElement.style.display = ""; + ErrorElement.innerText = "错误:" + LockResponseData.Message; + ToggleLockButton.checked = !ToggleLockButton.checked; + } + }); + }; + } + + Style.innerHTML += "img {"; + Style.innerHTML += " width: 50%;"; + Style.innerHTML += "}"; + + RenderMathJax(); + + if (Silent) { + scrollTo({ + top: OldScrollTop, behavior: "instant" + }); + } + } else { + PostTitle.innerText = "错误:" + ResponseData.Message; + } + }); + }; + Delete.addEventListener("click", () => { + Delete.disabled = true; + Delete.children[0].style.display = "inline-block"; + RequestAPI("DeletePost", { + "PostID": Number(SearchParams.get("tid")) + }, (ResponseData) => { + Delete.disabled = false; + Delete.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewReply", { + "PostID": Number(SearchParams.get("tid")), + "Content": String(ContentElement.value), + "CaptchaSecretKey": String(CaptchaSecretKey) + }, async (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + RefreshReply(); + ContentElement.value = ""; + PreviewTab.innerHTML = ""; + while (PostReplies.innerHTML.indexOf("placeholder") != -1) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + ContentElement.focus(); + ContentElement.scrollIntoView(); + turnstile.reset(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RefreshReply(false); + addEventListener("focus", RefreshReply); + } + } + } + } + + /** + * Copy Samples Feature + * Fixes copy functionality for test samples in problem pages + * Feature ID: CopySamples + * Type: F (Fix) + * Description: 题目界面测试样例有时复制无效 + */ + + + /** + * Initialize copy samples feature + * Adds click handlers to copy buttons that copy sample data to clipboard + * + * This feature fixes issues where copy buttons on the problem page don't work + * properly. It intercepts clicks on .copy-btn elements and copies the associated + * .sampledata content to the clipboard using GM_setClipboard. + * + * Expected DOM structure: + * - Button with class "copy-btn" + * - Parent element containing a .sampledata element with the text to copy + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 + */ + function init$2() { + // Only execute if CopySamples feature is enabled + if (!UtilityEnabled("CopySamples")) { + return; + } + + // Attach click handlers to all copy buttons + $(".copy-btn").click((Event) => { + let CurrentButton = $(Event.currentTarget); + let span = CurrentButton.parent().last().find(".sampledata"); + + // Check if sample data element was found + if (!span.length) { + CurrentButton.text("未找到代码块").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + return; + } + + // Copy sample data to clipboard + GM_setClipboard(span.text()); + + // Show success feedback + CurrentButton.text("复制成功").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + }); + } + + /** + * Compare Source Feature + * Allows users to compare two source code submissions side-by-side using CodeMirror MergeView + * + * Extracted from bootstrap.js: + * - Lines 985: Feature definition + * - Lines 1465-1474: Button on problem page + * - Lines 2720-2787: Main comparison interface + */ + + + /** + * Initialize compare source feature + * - Adds a "Compare Submissions" button on problem pages + * - Creates comparison interface on comparesource.php page + */ + async function init$1() { + // Only execute if CompareSource feature is enabled + if (!UtilityEnabled("CompareSource")) { + return; + } + + // Add "Compare Submissions" button on problem pages + addCompareButtonOnProblemPage(); + + // Handle comparison interface on comparesource.php page + if (location.pathname === "/comparesource.php") { + await handleCompareSourcePage(); + } + } + + /** + * Adds a "Compare Submissions" button on problem pages + * Button navigates to comparesource.php when clicked + */ + function addCompareButtonOnProblemPage() { + const inputAppend = document.querySelector("body > div.container > div > div.input-append"); + if (!inputAppend) { + return; + } + + const compareButton = document.createElement("button"); + inputAppend.appendChild(compareButton); + compareButton.className = "btn btn-outline-secondary"; + compareButton.innerText = "比较提交记录"; + compareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php"; + }); + compareButton.style.marginBottom = "7px"; + } + + /** + * Handles the compare source page functionality + * Creates either a form to input submission IDs or displays a side-by-side comparison + */ + async function handleCompareSourcePage() { + const searchParams = new URLSearchParams(location.search); + + // If no search parameters, show the input form + if (location.search === "") { + createComparisonForm(); + } else { + // If search parameters exist, fetch and display the comparison + await createComparisonView(searchParams); + } + } + + /** + * Creates the form to input submission IDs for comparison + */ + function createComparisonForm() { + const container = document.querySelector("body > div.container > div"); + if (!container) { + return; + } + + container.innerHTML = ""; + + // Left code input + const leftCodeText = document.createElement("span"); + container.appendChild(leftCodeText); + leftCodeText.innerText = "左侧代码的运行编号:"; + + const leftCode = document.createElement("input"); + container.appendChild(leftCode); + leftCode.classList.add("form-control"); + leftCode.style.width = "40%"; + leftCode.style.marginBottom = "5px"; + + // Right code input + const rightCodeText = document.createElement("span"); + container.appendChild(rightCodeText); + rightCodeText.innerText = "右侧代码的运行编号:"; + + const rightCode = document.createElement("input"); + container.appendChild(rightCode); + rightCode.classList.add("form-control"); + rightCode.style.width = "40%"; + rightCode.style.marginBottom = "5px"; + + // Compare button + const compareButton = document.createElement("button"); + container.appendChild(compareButton); + compareButton.innerText = "比较"; + compareButton.className = "btn btn-primary"; + compareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php?left=" + + Number(leftCode.value) + "&right=" + Number(rightCode.value); + }); + } + + /** + * Creates the side-by-side comparison view using CodeMirror MergeView + * @param {URLSearchParams} searchParams - URL parameters containing left and right submission IDs + */ + async function createComparisonView(searchParams) { + const mtElement = document.querySelector("body > div > div.mt-3"); + if (!mtElement) { + return; + } + + // Create comparison interface with checkbox and comparison element + mtElement.innerHTML = ` +
    + + +
    +
    `; + + // Fetch left code + let leftCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("left")) + .then((response) => { + return response.text(); + }) + .then((response) => { + leftCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Fetch right code + let rightCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("right")) + .then((response) => { + return response.text(); + }) + .then((response) => { + rightCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Create CodeMirror MergeView for side-by-side comparison + const compareElement = document.getElementById("CompareElement"); + const mergeViewElement = CodeMirror.MergeView(compareElement, { + value: leftCode, + origLeft: null, + orig: rightCode, + lineNumbers: true, + mode: "text/x-c++src", + collapseIdentical: "true", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + revertButtons: false, + ignoreWhitespace: true + }); + + // Add event listener for ignore whitespace checkbox + const ignoreWhitespace = document.getElementById("IgnoreWhitespace"); + ignoreWhitespace.addEventListener("change", () => { + mergeViewElement.ignoreWhitespace = ignoreWhitespace.checked; + }); + } + + /** + * Remove Useless Feature + * Removes unwanted elements from various pages + * Feature ID: RemoveUseless + * Type: U (Utility) + * Description: 移除页面中的无用元素 + */ + + + /** + * Initialize remove useless feature + * Removes various unwanted elements from the page based on current location + * + * This feature removes several unnecessary or distracting elements: + * - Marquee elements (scrolling text banners) + * - Footer elements + * - English title headers (h2.lang_en) on problem and solution pages + * - Submission nodes on user info pages + * - Center tags on problem pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Line 320-322: Remove marquee elements + * - Line 398-402: Remove footer + * - Line 1222-1225: Remove h2.lang_en and second center tag on problem pages + * - Line 2500-2505: Remove submission child nodes on userinfo page + * - Line 3209-3211: Remove h2.lang_en on problem_solution page + */ + function init() { + // Only execute if RemoveUseless feature is enabled + if (!UtilityEnabled("RemoveUseless")) { + return; + } + + // Remove marquee elements (scrolling banners) - Line 320-322 + const marquee = document.getElementsByTagName("marquee")[0]; + if (marquee !== undefined) { + marquee.remove(); + } + + // Remove footer - Line 398-402 + const footer = document.getElementsByClassName("footer")[0]; + if (footer !== null) { + footer.remove(); + } + + // Page-specific removals based on current pathname + const pathname = location.pathname; + + // Problem page specific removals - Line 1222-1225 + if (pathname === "/problem.php") { + const langEnHeader = document.querySelector("h2.lang_en"); + if (langEnHeader) { + langEnHeader.remove(); + } + + const centerElements = document.getElementsByTagName("center"); + if (centerElements[1]) { + centerElements[1].remove(); + } + } + + // User info page specific removals - Line 2500-2505 + if (pathname === "/userinfo.php") { + const searchParams = new URLSearchParams(location.search); + if (searchParams.get("ByUserScript") === null) { + const submissionElement = document.getElementById("submission"); + if (submissionElement) { + const childNodes = submissionElement.childNodes; + // Remove all child nodes + for (let i = childNodes.length - 1; i >= 0; i--) { + childNodes[i].remove(); + } + } + } + } + + // Problem solution page specific removals - Line 3209-3211 + if (pathname === "/problem_solution.php") { + const langEnHeader = document.querySelector("h2.lang_en"); + if (langEnHeader) { + langEnHeader.remove(); + } + } + } + + /** + * Feature loader - Initializes all extracted feature modules + * + * This module provides a centralized way to initialize all feature modules. + * Features are loaded conditionally based on UtilityEnabled settings and + * current page URL. + */ + + + /** + * Initialize all feature modules + * Features will self-check if they should be active based on UtilityEnabled + * + * @param {Object} context - Shared context object with dependencies + * @param {string} context.CurrentUsername - Current logged-in username + * @param {URLSearchParams} context.SearchParams - URL search parameters + * @param {boolean} context.IsAdmin - Whether user is admin + * @param {HTMLStyleElement} context.Style - Style element for adding CSS + * @param {string} context.CaptchaSiteKey - Cloudflare Turnstile site key + * @param {Function} context.GetUsernameHTML - Function to render usernames + * @param {Function} context.PurifyHTML - Function to sanitize HTML + * @param {Function} context.RenderMathJax - Function to render MathJax + */ + async function initializeFeatures(context) { + try { + // Initialize features that need to run early (before main page load) + init$4(); + + // Initialize features that clean up/modify the page + init(); + + // Initialize page-specific features + init$2(); + await init$1(); + + // Initialize complex features that need context + if (context) ; + + console.log('[XMOJ-Script] Feature modules initialized'); + } catch (error) { + console.error('[XMOJ-Script] Error initializing features:', error); + } + } + + /** + * Get list of all extracted features + * Useful for debugging and feature management + */ + function getExtractedFeatures() { + return [ + 'AutoLogin', + 'Discussion', + 'CopySamples', + 'CompareSource', + 'RemoveUseless', + ]; + } + /** * Main entry point for XMOJ Script * This file imports all utilities and features, then initializes the application @@ -5119,7 +6417,16 @@ int main() // Initialize theme initTheme(); + // Initialize extracted feature modules + // These run before main() to allow early initialization (like AutoLogin) + initializeFeatures().then(() => { + console.log('[XMOJ-Script] Extracted features loaded:', getExtractedFeatures()); + }); + // Start the main application + // Note: bootstrap.js still contains all original code for compatibility + // Extracted features in src/features/ provide the same functionality + // in a more maintainable way main(); })(); diff --git a/src/features/auto-login.js b/src/features/auto-login.js new file mode 100644 index 00000000..b6db487e --- /dev/null +++ b/src/features/auto-login.js @@ -0,0 +1,44 @@ +/** + * Auto Login Feature + * Automatically redirects to login page when user is not logged in + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize auto login feature + * Checks if user is logged in and redirects to login page if necessary + */ +export function init() { + // Only execute if AutoLogin feature is enabled + if (!UtilityEnabled("AutoLogin")) { + return; + } + + // Check if navbar exists (indicates page is loaded) + if (document.querySelector("#navbar") === null) { + return; + } + + // Check if profile element exists + const profileElement = document.querySelector("#profile"); + if (profileElement === null) { + return; + } + + // Check if user is not logged in (profile shows "登录" = "Login") + const isNotLoggedIn = profileElement.innerHTML === "登录"; + + // Exclude login-related pages from auto-redirect + const excludedPaths = ["/login.php", "/loginpage.php", "/lostpassword.php"]; + const isExcludedPath = excludedPaths.includes(location.pathname); + + // If user is not logged in and not already on a login page, redirect + if (isNotLoggedIn && !isExcludedPath) { + // Save current page to return after login + localStorage.setItem("UserScript-LastPage", location.pathname + location.search); + + // Redirect to login page + location.href = "https://www.xmoj.tech/loginpage.php"; + } +} diff --git a/src/features/compare-source.js b/src/features/compare-source.js new file mode 100644 index 00000000..121b9bc2 --- /dev/null +++ b/src/features/compare-source.js @@ -0,0 +1,171 @@ +/** + * Compare Source Feature + * Allows users to compare two source code submissions side-by-side using CodeMirror MergeView + * + * Extracted from bootstrap.js: + * - Lines 985: Feature definition + * - Lines 1465-1474: Button on problem page + * - Lines 2720-2787: Main comparison interface + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize compare source feature + * - Adds a "Compare Submissions" button on problem pages + * - Creates comparison interface on comparesource.php page + */ +export async function init() { + // Only execute if CompareSource feature is enabled + if (!UtilityEnabled("CompareSource")) { + return; + } + + // Add "Compare Submissions" button on problem pages + addCompareButtonOnProblemPage(); + + // Handle comparison interface on comparesource.php page + if (location.pathname === "/comparesource.php") { + await handleCompareSourcePage(); + } +} + +/** + * Adds a "Compare Submissions" button on problem pages + * Button navigates to comparesource.php when clicked + */ +function addCompareButtonOnProblemPage() { + const inputAppend = document.querySelector("body > div.container > div > div.input-append"); + if (!inputAppend) { + return; + } + + const compareButton = document.createElement("button"); + inputAppend.appendChild(compareButton); + compareButton.className = "btn btn-outline-secondary"; + compareButton.innerText = "比较提交记录"; + compareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php"; + }); + compareButton.style.marginBottom = "7px"; +} + +/** + * Handles the compare source page functionality + * Creates either a form to input submission IDs or displays a side-by-side comparison + */ +async function handleCompareSourcePage() { + const searchParams = new URLSearchParams(location.search); + + // If no search parameters, show the input form + if (location.search === "") { + createComparisonForm(); + } else { + // If search parameters exist, fetch and display the comparison + await createComparisonView(searchParams); + } +} + +/** + * Creates the form to input submission IDs for comparison + */ +function createComparisonForm() { + const container = document.querySelector("body > div.container > div"); + if (!container) { + return; + } + + container.innerHTML = ""; + + // Left code input + const leftCodeText = document.createElement("span"); + container.appendChild(leftCodeText); + leftCodeText.innerText = "左侧代码的运行编号:"; + + const leftCode = document.createElement("input"); + container.appendChild(leftCode); + leftCode.classList.add("form-control"); + leftCode.style.width = "40%"; + leftCode.style.marginBottom = "5px"; + + // Right code input + const rightCodeText = document.createElement("span"); + container.appendChild(rightCodeText); + rightCodeText.innerText = "右侧代码的运行编号:"; + + const rightCode = document.createElement("input"); + container.appendChild(rightCode); + rightCode.classList.add("form-control"); + rightCode.style.width = "40%"; + rightCode.style.marginBottom = "5px"; + + // Compare button + const compareButton = document.createElement("button"); + container.appendChild(compareButton); + compareButton.innerText = "比较"; + compareButton.className = "btn btn-primary"; + compareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php?left=" + + Number(leftCode.value) + "&right=" + Number(rightCode.value); + }); +} + +/** + * Creates the side-by-side comparison view using CodeMirror MergeView + * @param {URLSearchParams} searchParams - URL parameters containing left and right submission IDs + */ +async function createComparisonView(searchParams) { + const mtElement = document.querySelector("body > div > div.mt-3"); + if (!mtElement) { + return; + } + + // Create comparison interface with checkbox and comparison element + mtElement.innerHTML = ` +
    + + +
    +
    `; + + // Fetch left code + let leftCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("left")) + .then((response) => { + return response.text(); + }) + .then((response) => { + leftCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Fetch right code + let rightCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("right")) + .then((response) => { + return response.text(); + }) + .then((response) => { + rightCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Create CodeMirror MergeView for side-by-side comparison + const compareElement = document.getElementById("CompareElement"); + const mergeViewElement = CodeMirror.MergeView(compareElement, { + value: leftCode, + origLeft: null, + orig: rightCode, + lineNumbers: true, + mode: "text/x-c++src", + collapseIdentical: "true", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + revertButtons: false, + ignoreWhitespace: true + }); + + // Add event listener for ignore whitespace checkbox + const ignoreWhitespace = document.getElementById("IgnoreWhitespace"); + ignoreWhitespace.addEventListener("change", () => { + mergeViewElement.ignoreWhitespace = ignoreWhitespace.checked; + }); +} diff --git a/src/features/copy-samples.js b/src/features/copy-samples.js new file mode 100644 index 00000000..8f567d64 --- /dev/null +++ b/src/features/copy-samples.js @@ -0,0 +1,54 @@ +/** + * Copy Samples Feature + * Fixes copy functionality for test samples in problem pages + * Feature ID: CopySamples + * Type: F (Fix) + * Description: 题目界面测试样例有时复制无效 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize copy samples feature + * Adds click handlers to copy buttons that copy sample data to clipboard + * + * This feature fixes issues where copy buttons on the problem page don't work + * properly. It intercepts clicks on .copy-btn elements and copies the associated + * .sampledata content to the clipboard using GM_setClipboard. + * + * Expected DOM structure: + * - Button with class "copy-btn" + * - Parent element containing a .sampledata element with the text to copy + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 + */ +export function init() { + // Only execute if CopySamples feature is enabled + if (!UtilityEnabled("CopySamples")) { + return; + } + + // Attach click handlers to all copy buttons + $(".copy-btn").click((Event) => { + let CurrentButton = $(Event.currentTarget); + let span = CurrentButton.parent().last().find(".sampledata"); + + // Check if sample data element was found + if (!span.length) { + CurrentButton.text("未找到代码块").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + return; + } + + // Copy sample data to clipboard + GM_setClipboard(span.text()); + + // Show success feedback + CurrentButton.text("复制成功").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + }); +} diff --git a/src/features/discussion.js b/src/features/discussion.js new file mode 100644 index 00000000..1e7ad8d0 --- /dev/null +++ b/src/features/discussion.js @@ -0,0 +1,888 @@ +/** + * Discussion Feature + * Provides discussion forum functionality including discussion list, threads, and replies + * Feature ID: Discussion + * Type: A (Add) + * Description: Adds discussion forum with navbar link, problem page integration, and full forum pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 205-210: Navbar link creation + * - Lines 1284-1317: Discussion button on problem pages + * - Lines 3609-4386: Discussion forum pages (list, new post, thread view) + */ + +import { UtilityEnabled } from '../core/config.js'; +import { RequestAPI } from '../utils/api.js'; +import { GetRelativeTime } from '../utils/time.js'; + +/** + * Initialize discussion feature + * @param {Object} context - Context object containing required dependencies + * @param {string} context.CurrentUsername - Current logged in username + * @param {URLSearchParams} context.SearchParams - URL search parameters + * @param {boolean} context.IsAdmin - Whether current user is admin + * @param {HTMLStyleElement} context.Style - Style element for adding CSS + * @param {string} context.CaptchaSiteKey - Cloudflare turnstile site key + * @param {Function} context.GetUsernameHTML - Function to render username with profile link + * @param {Function} context.PurifyHTML - Function to sanitize HTML content + * @param {Function} context.RenderMathJax - Function to render math formulas + */ +export function init(context) { + // Only execute if Discussion feature is enabled + if (!UtilityEnabled("Discussion")) { + return; + } + + const { + CurrentUsername, + SearchParams, + IsAdmin, + Style, + CaptchaSiteKey, + GetUsernameHTML, + PurifyHTML, + RenderMathJax + } = context; + + // Part 1: Create navbar link (lines 205-210) + let Discussion = null; + if (document.querySelector("#navbar > ul:nth-child(1)")) { + Discussion = document.createElement("li"); + document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); + Discussion.innerHTML = "讨论"; + } + + // Part 2: Discussion button on problem pages (lines 1284-1317) + if (location.pathname === "/problem.php") { + const centerElement = document.querySelector("body > div > div.mt-3 > center"); + if (centerElement) { + let PID = null; + if (SearchParams.get("cid") != null) { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + } else { + PID = SearchParams.get("id"); + } + + if (PID) { + let DiscussButton = document.createElement("button"); + DiscussButton.className = "btn btn-outline-secondary position-relative"; + DiscussButton.innerHTML = `讨论`; + DiscussButton.style.marginLeft = "10px"; + DiscussButton.type = "button"; + DiscussButton.addEventListener("click", () => { + if (SearchParams.get("cid") != null) { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); + } else { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); + } + }); + centerElement.appendChild(DiscussButton); + + let UnreadBadge = document.createElement("span"); + UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + UnreadBadge.style.display = "none"; + DiscussButton.appendChild(UnreadBadge); + + let RefreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (Response) => { + if (Response.Success) { + if (Response.Data.DiscussCount != 0) { + UnreadBadge.innerText = Response.Data.DiscussCount; + UnreadBadge.style.display = ""; + } + } + }); + }; + RefreshCount(); + addEventListener("focus", RefreshCount); + } + } + } + + // Part 3: Discussion forum pages (lines 3609-4386) + if (location.pathname.indexOf("/discuss3") != -1) { + if (Discussion) { + Discussion.classList.add("active"); + } + + // Discussion list page + if (location.pathname == "/discuss3/discuss.php") { + document.title = "讨论列表"; + let ProblemID = parseInt(SearchParams.get("pid")); + let BoardID = parseInt(SearchParams.get("bid")); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    讨论列表${(isNaN(ProblemID) ? "" : ` - 题目` + ProblemID)}

    + + +
    + + + + + + + + + + + + + + + +
    编号标题作者题目编号发布时间回复数最后回复
    `; + NewPost.addEventListener("click", () => { + if (!isNaN(ProblemID)) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?pid=" + ProblemID; + } else if (SearchParams.get("bid") != null) { + location.href = "https://www.xmoj.tech/discuss3/newpost.php?bid=" + SearchParams.get("bid"); + } else { + location.href = "https://www.xmoj.tech/discuss3/newpost.php"; + } + }); + const RefreshPostList = (Silent = true) => { + if (!Silent) { + PostList.children[1].innerHTML = ""; + for (let i = 0; i < 10; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + for (let j = 0; j < 7; j++) { + let Cell = document.createElement("td"); + Row.appendChild(Cell); + Cell.innerHTML = ``; + } + } + } + RequestAPI("GetPosts", { + "ProblemID": Number(ProblemID || 0), + "Page": Number(Page), + "BoardID": Number(SearchParams.get("bid") || -1) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + ErrorElement.style.display = "none"; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + } + let Posts = ResponseData.Data.Posts; + PostList.children[1].innerHTML = ""; + if (Posts.length == 0) { + PostList.children[1].innerHTML = `暂无数据`; + } + for (let i = 0; i < Posts.length; i++) { + let Row = document.createElement("tr"); + PostList.children[1].appendChild(Row); + let IDCell = document.createElement("td"); + Row.appendChild(IDCell); + IDCell.innerText = Posts[i].PostID + " " + Posts[i].BoardName; + let TitleCell = document.createElement("td"); + Row.appendChild(TitleCell); + let TitleLink = document.createElement("a"); + TitleCell.appendChild(TitleLink); + TitleLink.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + Posts[i].PostID; + if (Posts[i].Lock.Locked) { + TitleLink.classList.add("link-secondary"); + TitleLink.innerHTML = "🔒 "; + } + TitleLink.innerHTML += Posts[i].Title; + let AuthorCell = document.createElement("td"); + Row.appendChild(AuthorCell); + GetUsernameHTML(AuthorCell, Posts[i].UserID); + let ProblemIDCell = document.createElement("td"); + Row.appendChild(ProblemIDCell); + if (Posts[i].ProblemID != 0) { + let ProblemIDLink = document.createElement("a"); + ProblemIDCell.appendChild(ProblemIDLink); + ProblemIDLink.href = "https://www.xmoj.tech/problem.php?id=" + Posts[i].ProblemID; + ProblemIDLink.innerText = Posts[i].ProblemID; + } + let PostTimeCell = document.createElement("td"); + Row.appendChild(PostTimeCell); + PostTimeCell.innerHTML = GetRelativeTime(Posts[i].PostTime); + let ReplyCountCell = document.createElement("td"); + Row.appendChild(ReplyCountCell); + ReplyCountCell.innerText = Posts[i].ReplyCount; + let LastReplyTimeCell = document.createElement("td"); + Row.appendChild(LastReplyTimeCell); + LastReplyTimeCell.innerHTML = GetRelativeTime(Posts[i].LastReplyTime); + } + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }; + RefreshPostList(false); + addEventListener("focus", RefreshPostList); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php"; + LinkElement.classList.add("me-2"); + LinkElement.innerText = "全部"; + GotoBoard.appendChild(LinkElement); + for (let i = 0; i < ResponseData.Data.Boards.length; i++) { + let LinkElement = document.createElement("a"); + LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php?bid=" + ResponseData.Data.Boards[i].BoardID; + LinkElement.classList.add("me-2"); + LinkElement.innerText = ResponseData.Data.Boards[i].BoardName; + GotoBoard.appendChild(LinkElement); + } + } + }); + } else if (location.pathname == "/discuss3/newpost.php") { + // New post page implementation + let ProblemID = parseInt(SearchParams.get("pid")); + document.querySelector("body > div > div").innerHTML = `

    发布新讨论` + (!isNaN(ProblemID) ? ` - 题目` + ProblemID : ``) + `

    +
    + +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + ContentElement.classList.remove("is-invalid"); + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + TitleElement.addEventListener("input", () => { + TitleElement.classList.remove("is-invalid"); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + let Title = TitleElement.value; + let Content = ContentElement.value; + let ProblemID = parseInt(SearchParams.get("pid")); + if (Title === "") { + TitleElement.classList.add("is-invalid"); + return; + } + if (Content === "") { + ContentElement.classList.add("is-invalid"); + return; + } + if (document.querySelector("#Board input:checked") === null) { + ErrorElement.innerText = "请选择要发布的板块"; + ErrorElement.style.display = "block"; + return; + } + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewPost", { + "Title": String(Title), + "Content": String(Content), + "ProblemID": Number(isNaN(ProblemID) ? 0 : ProblemID), + "CaptchaSecretKey": String(CaptchaSecretKey), + "BoardID": Number(document.querySelector("#Board input:checked").value) + }, (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ResponseData.Data.PostID; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RequestAPI("GetBoards", {}, (ResponseData) => { + if (ResponseData.Success === true) { + let Data = ResponseData.Data.Boards; + for (let i = 0; i < Data.length; i++) { + let RadioElement = document.createElement("div"); + RadioElement.className = "col-auto form-check form-check-inline"; + let RadioInput = document.createElement("input"); + RadioInput.className = "form-check-input"; + RadioInput.type = "radio"; + RadioInput.name = "Board"; + RadioInput.id = "Board" + Data[i].BoardID; + RadioInput.value = Data[i].BoardID; + RadioElement.appendChild(RadioInput); + if (SearchParams.get("bid") !== null && SearchParams.get("bid") == Data[i].BoardID) { + RadioInput.checked = true; + } + if (!isNaN(ProblemID)) { + RadioInput.disabled = true; + } + if (Data[i].BoardID == 4) { + if (!isNaN(ProblemID)) RadioInput.checked = true; + RadioInput.disabled = true; + } + let RadioLabel = document.createElement("label"); + RadioLabel.className = "form-check-label"; + RadioLabel.htmlFor = "Board" + Data[i].BoardID; + RadioLabel.innerText = Data[i].BoardName; + RadioElement.appendChild(RadioLabel); + Board.appendChild(RadioElement); + } + } + }); + } else if (location.pathname == "/discuss3/thread.php") { + // Thread view page implementation + if (SearchParams.get("tid") == null) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + let ThreadID = SearchParams.get("tid"); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

    +
    + 作者:
    + 发布时间: + 板块: + + + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + let RefreshReply = (Silent = true) => { + if (!Silent) { + PostTitle.innerHTML = ``; + PostAuthor.innerHTML = ``; + PostTime.innerHTML = ``; + PostBoard.innerHTML = ``; + PostReplies.innerHTML = ""; + for (let i = 0; i < 10; i++) { + PostReplies.innerHTML += `
    +
    +
    + + +
    +
    + + + +
    +
    `; + } + } + RequestAPI("GetPost", { + "PostID": Number(ThreadID), "Page": Number(Page) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + let OldScrollTop = document.documentElement.scrollTop; + let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { + Delete.style.display = ""; + } + } + PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); + document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; + PostAuthor.innerHTML = ""; + GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); + PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); + PostBoard.innerHTML = ResponseData.Data.BoardName; + let Replies = ResponseData.Data.Reply; + PostReplies.innerHTML = ""; + for (let i = 0; i < Replies.length; i++) { + let CardElement = document.createElement("div"); + PostReplies.appendChild(CardElement); + CardElement.className = "card mb-3"; + let CardBodyElement = document.createElement("div"); + CardElement.appendChild(CardBodyElement); + CardBodyElement.className = "card-body row"; + let CardBodyRowElement = document.createElement("div"); + CardBodyElement.appendChild(CardBodyRowElement); + CardBodyRowElement.className = "row mb-3"; + let AuthorElement = document.createElement("span"); + CardBodyRowElement.appendChild(AuthorElement); + AuthorElement.className = "col-4 text-muted"; + let AuthorSpanElement = document.createElement("span"); + AuthorElement.appendChild(AuthorSpanElement); + AuthorSpanElement.innerText = "作者:"; + let AuthorUsernameElement = document.createElement("span"); + AuthorElement.appendChild(AuthorUsernameElement); + GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); + let SendTimeElement = document.createElement("span"); + CardBodyRowElement.appendChild(SendTimeElement); + SendTimeElement.className = "col-4 text-muted"; + SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); + + let OKButton; + if (!LockButtons) { + let ButtonsElement = document.createElement("span"); + CardBodyRowElement.appendChild(ButtonsElement); + ButtonsElement.className = "col-4"; + let ReplyButton = document.createElement("button"); + ButtonsElement.appendChild(ReplyButton); + ReplyButton.type = "button"; + ReplyButton.className = "btn btn-sm btn-info"; + ReplyButton.innerText = "回复"; + ReplyButton.addEventListener("click", () => { + let Content = Replies[i].Content; + Content = Content.split("\n").map((Line) => { + // Count the number of '>' characters at the beginning of the line + let nestingLevel = 0; + while (Line.startsWith(">")) { + nestingLevel++; + Line = Line.substring(1).trim(); + } + // If the line is nested more than 2 levels deep, skip it + if (nestingLevel > 2) { + return null; + } + // Reconstruct the line with the appropriate number of '>' characters + return "> ".repeat(nestingLevel + 1) + Line; + }).filter(Line => Line !== null) // Remove null entries + .join("\n"); + ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; + ContentElement.focus(); + }); + let DeleteButton = document.createElement("button"); + ButtonsElement.appendChild(DeleteButton); + DeleteButton.type = "button"; + DeleteButton.className = "btn btn-sm btn-danger ms-1"; + DeleteButton.innerText = "删除"; + DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + DeleteButton.addEventListener("click", () => { + DeleteButton.disabled = true; + DeleteButton.lastChild.style.display = ""; + RequestAPI("DeleteReply", { + "ReplyID": Number(Replies[i].ReplyID) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + DeleteButton.disabled = false; + DeleteButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let DeleteSpin = document.createElement("div"); + DeleteButton.appendChild(DeleteSpin); + DeleteSpin.className = "spinner-border spinner-border-sm"; + DeleteSpin.role = "status"; + DeleteSpin.style.display = "none"; + OKButton = document.createElement("button"); + ButtonsElement.appendChild(OKButton); + OKButton.type = "button"; + OKButton.style.display = "none"; + OKButton.className = "btn btn-sm btn-success ms-1"; + OKButton.innerText = "确认"; + let OKSpin = document.createElement("div"); + OKButton.appendChild(OKSpin); + OKSpin.className = "spinner-border spinner-border-sm"; + OKSpin.role = "status"; + OKSpin.style.display = "none"; + OKButton.addEventListener("click", () => { + OKButton.disabled = true; + OKButton.lastChild.style.display = ""; + RequestAPI("EditReply", { + ReplyID: Number(Replies[i].ReplyID), + Content: String(ContentEditor.value) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + OKButton.disabled = false; + OKButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let CancelButton = document.createElement("button"); + ButtonsElement.appendChild(CancelButton); + CancelButton.type = "button"; + CancelButton.style.display = "none"; + CancelButton.className = "btn btn-sm btn-secondary ms-1"; + CancelButton.innerText = "取消"; + CancelButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = ""; + CardBodyElement.children[3].style.display = "none"; + EditButton.style.display = ""; + OKButton.style.display = "none"; + CancelButton.style.display = "none"; + }); + let EditButton = document.createElement("button"); + ButtonsElement.appendChild(EditButton); + EditButton.type = "button"; + EditButton.className = "btn btn-sm btn-warning ms-1"; + EditButton.innerText = "编辑"; + EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + EditButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = "none"; + CardBodyElement.children[3].style.display = ""; + EditButton.style.display = "none"; + OKButton.style.display = ""; + CancelButton.style.display = ""; + }); + } + + let CardBodyHRElement = document.createElement("hr"); + CardBodyElement.appendChild(CardBodyHRElement); + + let ReplyContentElement = document.createElement("div"); + CardBodyElement.appendChild(ReplyContentElement); + ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + if (Replies[i].EditTime != null) { + if (Replies[i].EditPerson == Replies[i].UserID) { + ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } else { + ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } + } + let ContentEditElement = document.createElement("div"); + CardBodyElement.appendChild(ContentEditElement); + ContentEditElement.classList.add("input-group"); + ContentEditElement.style.display = "none"; + let ContentEditor = document.createElement("textarea"); + ContentEditElement.appendChild(ContentEditor); + ContentEditor.className = "form-control col-6"; + ContentEditor.rows = 3; + ContentEditor.value = Replies[i].Content; + if (ContentEditor.value.indexOf("
    ") != -1) { + ContentEditor.value = ContentEditor.value.substring(0, ContentEditor.value.indexOf("
    ")); + } + ContentEditor.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + OKButton.click(); + } + }); + let PreviewTab = document.createElement("div"); + ContentEditElement.appendChild(PreviewTab); + PreviewTab.className = "form-control col-6"; + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + ContentEditor.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + RenderMathJax(); + }); + ContentEditor.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentEditor.value.substring(0, ContentEditor.selectionStart); + let After = ContentEditor.value.substring(ContentEditor.selectionEnd, ContentEditor.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentEditor.value = Before + UploadMessage + After; + ContentEditor.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentEditor.dispatchEvent(new Event("input")); + } else { + ContentEditor.value = Before + `![上传失败!]()` + After; + ContentEditor.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + } + + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + + let CodeElements = document.querySelectorAll("#PostReplies > div > div > div:nth-child(3) > pre > code"); + for (let i = 0; i < CodeElements.length; i++) { + let ModeName = "text/x-c++src"; + if (CodeElements[i].className == "language-c") { + ModeName = "text/x-csrc"; + } else if (CodeElements[i].className == "language-cpp") { + ModeName = "text/x-c++src"; + } + CodeMirror(CodeElements[i].parentElement, { + value: CodeElements[i].innerText, + mode: ModeName, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + lineNumbers: true, + readOnly: true + }).setSize("100%", "auto"); + CodeElements[i].remove(); + } + + if (LockButtons) { + let LockElement = ContentElement.parentElement.parentElement; + LockElement.innerHTML = "讨论已于 " + await GetRelativeTime(ResponseData.Data.Lock.LockTime) + " 被 "; + let LockUsernameSpan = document.createElement("span"); + LockElement.appendChild(LockUsernameSpan); + GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); + LockElement.innerHTML += " 锁定"; + LockElement.classList.add("mb-5"); + } + + if (IsAdmin) { + ToggleLock.style.display = "inline-block"; + ToggleLockButton.checked = ResponseData.Data.Lock.Locked; + ToggleLockButton.onclick = () => { + ToggleLockButton.disabled = true; + ErrorElement.style.display = "none"; + RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { + "PostID": Number(ThreadID) + }, (LockResponseData) => { + ToggleLockButton.disabled = false; + if (LockResponseData.Success) { + RefreshReply(); + } else { + ErrorElement.style.display = ""; + ErrorElement.innerText = "错误:" + LockResponseData.Message; + ToggleLockButton.checked = !ToggleLockButton.checked; + } + }); + }; + } + + Style.innerHTML += "img {"; + Style.innerHTML += " width: 50%;"; + Style.innerHTML += "}"; + + RenderMathJax(); + + if (Silent) { + scrollTo({ + top: OldScrollTop, behavior: "instant" + }); + } + } else { + PostTitle.innerText = "错误:" + ResponseData.Message; + } + }); + }; + Delete.addEventListener("click", () => { + Delete.disabled = true; + Delete.children[0].style.display = "inline-block"; + RequestAPI("DeletePost", { + "PostID": Number(SearchParams.get("tid")) + }, (ResponseData) => { + Delete.disabled = false; + Delete.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewReply", { + "PostID": Number(SearchParams.get("tid")), + "Content": String(ContentElement.value), + "CaptchaSecretKey": String(CaptchaSecretKey) + }, async (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + RefreshReply(); + ContentElement.value = ""; + PreviewTab.innerHTML = ""; + while (PostReplies.innerHTML.indexOf("placeholder") != -1) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + ContentElement.focus(); + ContentElement.scrollIntoView(); + turnstile.reset(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RefreshReply(false); + addEventListener("focus", RefreshReply); + } + } + } +} diff --git a/src/features/index.js b/src/features/index.js new file mode 100644 index 00000000..ed7c2aa6 --- /dev/null +++ b/src/features/index.js @@ -0,0 +1,64 @@ +/** + * Feature loader - Initializes all extracted feature modules + * + * This module provides a centralized way to initialize all feature modules. + * Features are loaded conditionally based on UtilityEnabled settings and + * current page URL. + */ + +import { init as initAutoLogin } from './auto-login.js'; +import { init as initDiscussion } from './discussion.js'; +import { init as initCopySamples } from './copy-samples.js'; +import { init as initCompareSource } from './compare-source.js'; +import { init as initRemoveUseless } from './remove-useless.js'; + +/** + * Initialize all feature modules + * Features will self-check if they should be active based on UtilityEnabled + * + * @param {Object} context - Shared context object with dependencies + * @param {string} context.CurrentUsername - Current logged-in username + * @param {URLSearchParams} context.SearchParams - URL search parameters + * @param {boolean} context.IsAdmin - Whether user is admin + * @param {HTMLStyleElement} context.Style - Style element for adding CSS + * @param {string} context.CaptchaSiteKey - Cloudflare Turnstile site key + * @param {Function} context.GetUsernameHTML - Function to render usernames + * @param {Function} context.PurifyHTML - Function to sanitize HTML + * @param {Function} context.RenderMathJax - Function to render MathJax + */ +export async function initializeFeatures(context) { + try { + // Initialize features that need to run early (before main page load) + initAutoLogin(); + + // Initialize features that clean up/modify the page + initRemoveUseless(); + + // Initialize page-specific features + initCopySamples(); + await initCompareSource(); + + // Initialize complex features that need context + if (context) { + await initDiscussion(context); + } + + console.log('[XMOJ-Script] Feature modules initialized'); + } catch (error) { + console.error('[XMOJ-Script] Error initializing features:', error); + } +} + +/** + * Get list of all extracted features + * Useful for debugging and feature management + */ +export function getExtractedFeatures() { + return [ + 'AutoLogin', + 'Discussion', + 'CopySamples', + 'CompareSource', + 'RemoveUseless', + ]; +} diff --git a/src/features/remove-useless.js b/src/features/remove-useless.js new file mode 100644 index 00000000..721c17a5 --- /dev/null +++ b/src/features/remove-useless.js @@ -0,0 +1,85 @@ +/** + * Remove Useless Feature + * Removes unwanted elements from various pages + * Feature ID: RemoveUseless + * Type: U (Utility) + * Description: 移除页面中的无用元素 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize remove useless feature + * Removes various unwanted elements from the page based on current location + * + * This feature removes several unnecessary or distracting elements: + * - Marquee elements (scrolling text banners) + * - Footer elements + * - English title headers (h2.lang_en) on problem and solution pages + * - Submission nodes on user info pages + * - Center tags on problem pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Line 320-322: Remove marquee elements + * - Line 398-402: Remove footer + * - Line 1222-1225: Remove h2.lang_en and second center tag on problem pages + * - Line 2500-2505: Remove submission child nodes on userinfo page + * - Line 3209-3211: Remove h2.lang_en on problem_solution page + */ +export function init() { + // Only execute if RemoveUseless feature is enabled + if (!UtilityEnabled("RemoveUseless")) { + return; + } + + // Remove marquee elements (scrolling banners) - Line 320-322 + const marquee = document.getElementsByTagName("marquee")[0]; + if (marquee !== undefined) { + marquee.remove(); + } + + // Remove footer - Line 398-402 + const footer = document.getElementsByClassName("footer")[0]; + if (footer !== null) { + footer.remove(); + } + + // Page-specific removals based on current pathname + const pathname = location.pathname; + + // Problem page specific removals - Line 1222-1225 + if (pathname === "/problem.php") { + const langEnHeader = document.querySelector("h2.lang_en"); + if (langEnHeader) { + langEnHeader.remove(); + } + + const centerElements = document.getElementsByTagName("center"); + if (centerElements[1]) { + centerElements[1].remove(); + } + } + + // User info page specific removals - Line 2500-2505 + if (pathname === "/userinfo.php") { + const searchParams = new URLSearchParams(location.search); + if (searchParams.get("ByUserScript") === null) { + const submissionElement = document.getElementById("submission"); + if (submissionElement) { + const childNodes = submissionElement.childNodes; + // Remove all child nodes + for (let i = childNodes.length - 1; i >= 0; i--) { + childNodes[i].remove(); + } + } + } + } + + // Problem solution page specific removals - Line 3209-3211 + if (pathname === "/problem_solution.php") { + const langEnHeader = document.querySelector("h2.lang_en"); + if (langEnHeader) { + langEnHeader.remove(); + } + } +} diff --git a/src/main.js b/src/main.js index 67170eb5..78010cc8 100644 --- a/src/main.js +++ b/src/main.js @@ -23,6 +23,9 @@ import { GetUserInfo, GetUserBadge, GetUsernameHTML } from './utils/user.js'; import { initTheme, NavbarStyler, replaceMarkdownImages, main } from './core/bootstrap.js'; import { registerMenuCommands } from './core/menu.js'; +// Feature modules imports +import { initializeFeatures, getExtractedFeatures } from './features/index.js'; + // Make utilities globally available (for compatibility with inline code) window.escapeHTML = escapeHTML; window.PurifyHTML = PurifyHTML; @@ -52,5 +55,14 @@ registerMenuCommands(); // Initialize theme initTheme(); +// Initialize extracted feature modules +// These run before main() to allow early initialization (like AutoLogin) +initializeFeatures().then(() => { + console.log('[XMOJ-Script] Extracted features loaded:', getExtractedFeatures()); +}); + // Start the main application +// Note: bootstrap.js still contains all original code for compatibility +// Extracted features in src/features/ provide the same functionality +// in a more maintainable way main(); From b432bef9cae629ad8e42621fb6ba4233e6ed25a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Nov 2025 10:46:30 +0000 Subject: [PATCH 06/26] Extract 6 additional features into separate modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit continues the feature extraction process by adding 6 new feature modules, demonstrating the established extraction pattern: New features extracted: - ReplaceXM: Replaces "小明" references with "高老师" - ReplaceYN: Replaces Y/N status indicators with symbols (✓/✗/⏳) - AddAnimation: Adds CSS transitions to status and test-case elements - AddColorText: Adds CSS classes for colored text (red, green, blue) - SavePassword: Automatically saves and fills login credentials - RemoveAlerts: Removes redundant alerts and warnings All features follow the established pattern with: - init() function for initialization - UtilityEnabled() checks for feature flags - Clear documentation of extracted code locations - Self-contained, maintainable modules Updated: - src/features/index.js: Added imports and initialization for new features - README_REFACTORING.md: Updated documentation with new features - dist/XMOJ.user.js: Rebuilt with new features Total extracted features: 11 (was 5) Remaining features: 30 (was 36) --- README_REFACTORING.md | 15 +- dist/XMOJ.user.js | 272 ++++++++++++++++++++++++++++++++- src/features/add-animation.js | 30 ++++ src/features/add-color-text.js | 36 +++++ src/features/index.js | 22 +++ src/features/remove-alerts.js | 45 ++++++ src/features/replace-xm.js | 41 +++++ src/features/replace-yn.js | 53 +++++++ src/features/save-password.js | 86 +++++++++++ 9 files changed, 587 insertions(+), 13 deletions(-) create mode 100644 src/features/add-animation.js create mode 100644 src/features/add-color-text.js create mode 100644 src/features/remove-alerts.js create mode 100644 src/features/replace-xm.js create mode 100644 src/features/replace-yn.js create mode 100644 src/features/save-password.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md index 093dca11..0bb5485c 100644 --- a/README_REFACTORING.md +++ b/README_REFACTORING.md @@ -103,6 +103,12 @@ The following features have been extracted into separate modules under `src/feat - **CopySamples** (`copy-samples.js`) - Fixes copy functionality for test samples - **CompareSource** (`compare-source.js`) - Side-by-side code comparison with diff highlighting - **RemoveUseless** (`remove-useless.js`) - Removes unwanted page elements (marquees, footers, etc.) +- **ReplaceXM** (`replace-xm.js`) - Replaces "小明" references with "高老师" +- **ReplaceYN** (`replace-yn.js`) - Replaces Y/N status indicators with symbols (✓/✗/⏳) +- **AddAnimation** (`add-animation.js`) - Adds CSS transitions to status and test-case elements +- **AddColorText** (`add-color-text.js`) - Adds CSS classes for colored text (red, green, blue) +- **SavePassword** (`save-password.js`) - Automatically saves and fills login credentials +- **RemoveAlerts** (`remove-alerts.js`) - Removes redundant alerts and warnings ### Feature Extraction Pattern @@ -139,17 +145,16 @@ To extract additional features from `bootstrap.js`: The following features remain in `bootstrap.js` and can be extracted following the pattern above: -- AddAnimation, AddColorText, AddUnits +- AddUnits - ApplyData, AutoCheat, AutoCountdown, AutoO2, AutoRefresh - BBSPopup, CompileError, CopyMD - DarkMode (theme already extracted, check for additional code) - DebugMode, DownloadPlayback, ExportACCode - IOFile, ImproveACRate, LoginFailed, MessagePopup - MoreSTD, NewBootstrap, NewDownload, NewTopBar -- OpenAllProblem, ProblemSwitcher -- RefreshSolution, RemoveAlerts -- ReplaceLinks, ReplaceXM, ReplaceYN, ResetType -- SavePassword, Translate, UploadStd +- OpenAllProblem, ProblemSwitcher, Rating +- RefreshSolution, ReplaceLinks, ResetType +- Translate, UploadStd ## Development diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js index 26b3637f..4b65fb32 100644 --- a/dist/XMOJ.user.js +++ b/dist/XMOJ.user.js @@ -5094,7 +5094,7 @@ int main() * Initialize auto login feature * Checks if user is logged in and redirects to login page if necessary */ - function init$4() { + function init$a() { // Only execute if AutoLogin feature is enabled if (!UtilityEnabled("AutoLogin")) { return; @@ -5154,7 +5154,7 @@ int main() * @param {Function} context.PurifyHTML - Function to sanitize HTML content * @param {Function} context.RenderMathJax - Function to render math formulas */ - function init$3(context) { + function init$9(context) { // Only execute if Discussion feature is enabled if (!UtilityEnabled("Discussion")) { return; @@ -6037,7 +6037,7 @@ int main() * * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 */ - function init$2() { + function init$8() { // Only execute if CopySamples feature is enabled if (!UtilityEnabled("CopySamples")) { return; @@ -6084,7 +6084,7 @@ int main() * - Adds a "Compare Submissions" button on problem pages * - Creates comparison interface on comparesource.php page */ - async function init$1() { + async function init$7() { // Only execute if CompareSource feature is enabled if (!UtilityEnabled("CompareSource")) { return; @@ -6266,7 +6266,7 @@ int main() * - Line 2500-2505: Remove submission child nodes on userinfo page * - Line 3209-3211: Remove h2.lang_en on problem_solution page */ - function init() { + function init$6() { // Only execute if RemoveUseless feature is enabled if (!UtilityEnabled("RemoveUseless")) { return; @@ -6324,6 +6324,246 @@ int main() } } + /** + * Replace XM Feature + * Replaces "小明" references with "高老师" + * Feature ID: ReplaceXM + * Type: C (Cosmetic/Fun) + * Description: 将"小明"替换为"高老师" + */ + + + /** + * Initialize ReplaceXM feature + * Replaces text content throughout the page: + * - "我" -> "高老师" + * - "小明" -> "高老师" + * - "下海" -> "上海" + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 219-222: Text replacement + * - Line 304: Navbar brand text + */ + function init$5() { + // Only execute if ReplaceXM feature is enabled + if (!UtilityEnabled("ReplaceXM")) { + return; + } + + // Replace text content throughout the page + document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); + } + + /** + * Replace YN Feature + * Replaces Y/N status indicators with symbols + * Feature ID: ReplaceYN + * Type: U (Utility) + * Description: 将Y/N状态替换为符号 + */ + + + /** + * Initialize ReplaceYN feature + * Replaces status text with symbols: + * - "Y" (AC/Accepted) -> "✓" + * - "N" (WA/Wrong Answer) -> "✗" + * - "W" (Waiting) -> "⏳" + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 404-417: Status text replacement + */ + function init$4() { + // Only execute if ReplaceYN feature is enabled + if (!UtilityEnabled("ReplaceYN")) { + return; + } + + // Replace AC (Accepted) status + let elements = document.getElementsByClassName("status_y"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "✓"; + } + + // Replace WA (Wrong Answer) status + elements = document.getElementsByClassName("status_n"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "✗"; + } + + // Replace Waiting status + elements = document.getElementsByClassName("status_w"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "⏳"; + } + } + + /** + * Add Animation Feature + * Adds CSS transitions to status and test-case elements + * Feature ID: AddAnimation + * Type: C (Cosmetic) + * Description: 为状态和测试用例元素添加动画 + */ + + + /** + * Initialize AddAnimation feature + * Adds smooth transitions to status and test-case elements + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 381-384: Animation CSS + */ + function init$3() { + // Only execute if AddAnimation feature is enabled + if (!UtilityEnabled("AddAnimation")) { + return; + } + + // Add CSS for animations + const style = document.createElement('style'); + style.innerHTML = `.status, .test-case { + transition: 0.5s !important; + }`; + document.head.appendChild(style); + } + + /** + * Add Color Text Feature + * Adds CSS classes for colored text (red, green, blue) + * Feature ID: AddColorText + * Type: U (Utility) + * Description: 添加彩色文本CSS类 + */ + + + /** + * Initialize AddColorText feature + * Adds CSS classes for red, green, and blue text + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 386-395: Color text CSS + */ + function init$2() { + // Only execute if AddColorText feature is enabled + if (!UtilityEnabled("AddColorText")) { + return; + } + + // Add CSS for colored text classes + const style = document.createElement('style'); + style.innerHTML = `.red { + color: red !important; + } + .green { + color: green !important; + } + .blue { + color: blue !important; + }`; + document.head.appendChild(style); + } + + /** + * Save Password Feature + * Automatically saves and fills login credentials + * Feature ID: SavePassword + * Type: U (Utility) + * Description: 自动保存和填充登录凭据 + */ + + + /** + * Initialize SavePassword feature + * Sets up auto-fill on login page when credentials are available + * + * Note: This feature also integrates with the login handler to: + * - Save credentials after successful login + * - Clear credentials after failed login + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2841-2843: Save credentials on success + * - Lines 2850-2852: Clear credentials on failure + * - Lines 2867-2876: Auto-fill and auto-submit login form + */ + function init$1() { + // Only execute on login page + if (location.pathname !== "/loginpage.php") { + return; + } + + // Only execute if SavePassword feature is enabled + if (!UtilityEnabled("SavePassword")) { + return; + } + + // Auto-fill login form with saved credentials + (async () => { + // Wait a bit for the page to be ready + await new Promise(resolve => setTimeout(resolve, 100)); + + const credential = await getCredential$1(); + if (credential) { + const usernameInput = document.querySelector("#login > div:nth-child(1) > div > input"); + const passwordInput = document.querySelector("#login > div:nth-child(2) > div > input"); + const loginButton = document.getElementsByName("submit")[0]; + + if (usernameInput && passwordInput && loginButton) { + usernameInput.value = credential.id; + passwordInput.value = credential.password; + loginButton.click(); + } + } + })(); + } + + /** + * Remove Alerts Feature + * Removes redundant alerts and warnings + * Feature ID: RemoveAlerts + * Type: D (Debug/Development) + * Description: 去除多余反复的提示 + */ + + + /** + * Initialize RemoveAlerts feature + * Modifies contest links to bypass "contest not started" alerts + * + * On contest pages, when the contest hasn't started yet, this feature + * changes the link to point directly to start_contest.php, bypassing + * the alert that would normally prevent access. + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1666-1667: Modify contest start link + */ + function init() { + // Only execute if RemoveAlerts feature is enabled + if (!UtilityEnabled("RemoveAlerts")) { + return; + } + + // Only execute on contest pages + if (location.pathname !== "/contest.php") { + return; + } + + // Check if contest hasn't started yet + const centerElement = document.querySelector("body > div > div.mt-3 > center"); + if (centerElement && centerElement.innerHTML.indexOf("尚未开始比赛") !== -1) { + const contestLink = document.querySelector("body > div > div.mt-3 > center > a"); + const searchParams = new URLSearchParams(location.search); + const cid = searchParams.get("cid"); + + if (contestLink && cid) { + // Modify link to bypass alert + contestLink.setAttribute("href", `start_contest.php?cid=${cid}`); + } + } + } + /** * Feature loader - Initializes all extracted feature modules * @@ -6350,14 +6590,24 @@ int main() async function initializeFeatures(context) { try { // Initialize features that need to run early (before main page load) - init$4(); + init$a(); // Initialize features that clean up/modify the page + init$6(); init(); - // Initialize page-specific features + // Initialize cosmetic/styling features + init$3(); init$2(); - await init$1(); + + // Initialize text replacement features + init$5(); + init$4(); + + // Initialize page-specific features + init$8(); + init$1(); + await init$7(); // Initialize complex features that need context if (context) ; @@ -6379,6 +6629,12 @@ int main() 'CopySamples', 'CompareSource', 'RemoveUseless', + 'ReplaceXM', + 'ReplaceYN', + 'AddAnimation', + 'AddColorText', + 'SavePassword', + 'RemoveAlerts', ]; } diff --git a/src/features/add-animation.js b/src/features/add-animation.js new file mode 100644 index 00000000..d7d54ba6 --- /dev/null +++ b/src/features/add-animation.js @@ -0,0 +1,30 @@ +/** + * Add Animation Feature + * Adds CSS transitions to status and test-case elements + * Feature ID: AddAnimation + * Type: C (Cosmetic) + * Description: 为状态和测试用例元素添加动画 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AddAnimation feature + * Adds smooth transitions to status and test-case elements + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 381-384: Animation CSS + */ +export function init() { + // Only execute if AddAnimation feature is enabled + if (!UtilityEnabled("AddAnimation")) { + return; + } + + // Add CSS for animations + const style = document.createElement('style'); + style.innerHTML = `.status, .test-case { + transition: 0.5s !important; + }`; + document.head.appendChild(style); +} diff --git a/src/features/add-color-text.js b/src/features/add-color-text.js new file mode 100644 index 00000000..72f82980 --- /dev/null +++ b/src/features/add-color-text.js @@ -0,0 +1,36 @@ +/** + * Add Color Text Feature + * Adds CSS classes for colored text (red, green, blue) + * Feature ID: AddColorText + * Type: U (Utility) + * Description: 添加彩色文本CSS类 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AddColorText feature + * Adds CSS classes for red, green, and blue text + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 386-395: Color text CSS + */ +export function init() { + // Only execute if AddColorText feature is enabled + if (!UtilityEnabled("AddColorText")) { + return; + } + + // Add CSS for colored text classes + const style = document.createElement('style'); + style.innerHTML = `.red { + color: red !important; + } + .green { + color: green !important; + } + .blue { + color: blue !important; + }`; + document.head.appendChild(style); +} diff --git a/src/features/index.js b/src/features/index.js index ed7c2aa6..c8cb9c24 100644 --- a/src/features/index.js +++ b/src/features/index.js @@ -11,6 +11,12 @@ import { init as initDiscussion } from './discussion.js'; import { init as initCopySamples } from './copy-samples.js'; import { init as initCompareSource } from './compare-source.js'; import { init as initRemoveUseless } from './remove-useless.js'; +import { init as initReplaceXM } from './replace-xm.js'; +import { init as initReplaceYN } from './replace-yn.js'; +import { init as initAddAnimation } from './add-animation.js'; +import { init as initAddColorText } from './add-color-text.js'; +import { init as initSavePassword } from './save-password.js'; +import { init as initRemoveAlerts } from './remove-alerts.js'; /** * Initialize all feature modules @@ -33,9 +39,19 @@ export async function initializeFeatures(context) { // Initialize features that clean up/modify the page initRemoveUseless(); + initRemoveAlerts(); + + // Initialize cosmetic/styling features + initAddAnimation(); + initAddColorText(); + + // Initialize text replacement features + initReplaceXM(); + initReplaceYN(); // Initialize page-specific features initCopySamples(); + initSavePassword(); await initCompareSource(); // Initialize complex features that need context @@ -60,5 +76,11 @@ export function getExtractedFeatures() { 'CopySamples', 'CompareSource', 'RemoveUseless', + 'ReplaceXM', + 'ReplaceYN', + 'AddAnimation', + 'AddColorText', + 'SavePassword', + 'RemoveAlerts', ]; } diff --git a/src/features/remove-alerts.js b/src/features/remove-alerts.js new file mode 100644 index 00000000..d6fd9154 --- /dev/null +++ b/src/features/remove-alerts.js @@ -0,0 +1,45 @@ +/** + * Remove Alerts Feature + * Removes redundant alerts and warnings + * Feature ID: RemoveAlerts + * Type: D (Debug/Development) + * Description: 去除多余反复的提示 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize RemoveAlerts feature + * Modifies contest links to bypass "contest not started" alerts + * + * On contest pages, when the contest hasn't started yet, this feature + * changes the link to point directly to start_contest.php, bypassing + * the alert that would normally prevent access. + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1666-1667: Modify contest start link + */ +export function init() { + // Only execute if RemoveAlerts feature is enabled + if (!UtilityEnabled("RemoveAlerts")) { + return; + } + + // Only execute on contest pages + if (location.pathname !== "/contest.php") { + return; + } + + // Check if contest hasn't started yet + const centerElement = document.querySelector("body > div > div.mt-3 > center"); + if (centerElement && centerElement.innerHTML.indexOf("尚未开始比赛") !== -1) { + const contestLink = document.querySelector("body > div > div.mt-3 > center > a"); + const searchParams = new URLSearchParams(location.search); + const cid = searchParams.get("cid"); + + if (contestLink && cid) { + // Modify link to bypass alert + contestLink.setAttribute("href", `start_contest.php?cid=${cid}`); + } + } +} diff --git a/src/features/replace-xm.js b/src/features/replace-xm.js new file mode 100644 index 00000000..01cce47e --- /dev/null +++ b/src/features/replace-xm.js @@ -0,0 +1,41 @@ +/** + * Replace XM Feature + * Replaces "小明" references with "高老师" + * Feature ID: ReplaceXM + * Type: C (Cosmetic/Fun) + * Description: 将"小明"替换为"高老师" + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ReplaceXM feature + * Replaces text content throughout the page: + * - "我" -> "高老师" + * - "小明" -> "高老师" + * - "下海" -> "上海" + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 219-222: Text replacement + * - Line 304: Navbar brand text + */ +export function init() { + // Only execute if ReplaceXM feature is enabled + if (!UtilityEnabled("ReplaceXM")) { + return; + } + + // Replace text content throughout the page + document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); +} + +/** + * Get the site name based on ReplaceXM setting + * Used by navbar and other UI elements + * @returns {string} Site name + */ +export function getSiteName() { + return UtilityEnabled("ReplaceXM") ? "高老师" : "小明"; +} diff --git a/src/features/replace-yn.js b/src/features/replace-yn.js new file mode 100644 index 00000000..dd2e16a0 --- /dev/null +++ b/src/features/replace-yn.js @@ -0,0 +1,53 @@ +/** + * Replace YN Feature + * Replaces Y/N status indicators with symbols + * Feature ID: ReplaceYN + * Type: U (Utility) + * Description: 将Y/N状态替换为符号 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ReplaceYN feature + * Replaces status text with symbols: + * - "Y" (AC/Accepted) -> "✓" + * - "N" (WA/Wrong Answer) -> "✗" + * - "W" (Waiting) -> "⏳" + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 404-417: Status text replacement + */ +export function init() { + // Only execute if ReplaceYN feature is enabled + if (!UtilityEnabled("ReplaceYN")) { + return; + } + + // Replace AC (Accepted) status + let elements = document.getElementsByClassName("status_y"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "✓"; + } + + // Replace WA (Wrong Answer) status + elements = document.getElementsByClassName("status_n"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "✗"; + } + + // Replace Waiting status + elements = document.getElementsByClassName("status_w"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "⏳"; + } +} + +/** + * Check if ReplaceYN is enabled + * Used by other features that need to apply Y/N replacement + * @returns {boolean} + */ +export function isEnabled() { + return UtilityEnabled("ReplaceYN"); +} diff --git a/src/features/save-password.js b/src/features/save-password.js new file mode 100644 index 00000000..9f5c2797 --- /dev/null +++ b/src/features/save-password.js @@ -0,0 +1,86 @@ +/** + * Save Password Feature + * Automatically saves and fills login credentials + * Feature ID: SavePassword + * Type: U (Utility) + * Description: 自动保存和填充登录凭据 + */ + +import { UtilityEnabled } from '../core/config.js'; +import { storeCredential, getCredential, clearCredential } from '../utils/credentials.js'; + +/** + * Initialize SavePassword feature + * Sets up auto-fill on login page when credentials are available + * + * Note: This feature also integrates with the login handler to: + * - Save credentials after successful login + * - Clear credentials after failed login + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2841-2843: Save credentials on success + * - Lines 2850-2852: Clear credentials on failure + * - Lines 2867-2876: Auto-fill and auto-submit login form + */ +export function init() { + // Only execute on login page + if (location.pathname !== "/loginpage.php") { + return; + } + + // Only execute if SavePassword feature is enabled + if (!UtilityEnabled("SavePassword")) { + return; + } + + // Auto-fill login form with saved credentials + (async () => { + // Wait a bit for the page to be ready + await new Promise(resolve => setTimeout(resolve, 100)); + + const credential = await getCredential(); + if (credential) { + const usernameInput = document.querySelector("#login > div:nth-child(1) > div > input"); + const passwordInput = document.querySelector("#login > div:nth-child(2) > div > input"); + const loginButton = document.getElementsByName("submit")[0]; + + if (usernameInput && passwordInput && loginButton) { + usernameInput.value = credential.id; + passwordInput.value = credential.password; + loginButton.click(); + } + } + })(); +} + +/** + * Save credentials after successful login + * Called by login handler after successful authentication + * @param {string} username - Username to save + * @param {string} password - Password to save + */ +export async function saveOnSuccess(username, password) { + if (!UtilityEnabled("SavePassword")) { + return; + } + await storeCredential(username, password); +} + +/** + * Clear credentials after failed login + * Called by login handler after failed authentication + */ +export async function clearOnFailure() { + if (!UtilityEnabled("SavePassword")) { + return; + } + await clearCredential(); +} + +/** + * Check if SavePassword feature is enabled + * @returns {boolean} + */ +export function isEnabled() { + return UtilityEnabled("SavePassword"); +} From abae8053e859cd1cb8769a945706293a34b7401e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Nov 2025 10:50:00 +0000 Subject: [PATCH 07/26] Extract 3 more features: ReplaceLinks, AutoO2, Translate This commit continues feature extraction by adding 3 new feature modules: New features extracted: - ReplaceLinks: Replaces bracketed links with styled buttons - Transforms [text] into styled button elements - AutoO2: Automatically enables O2 optimization flag for code submissions - Auto-checks the O2 checkbox on problem submission pages - Translate: Translates English text to Chinese throughout the site - Navbar translations - Problem set page form placeholders and headers - Contest page table headers All features follow the established pattern with: - init() function for initialization - UtilityEnabled() checks for feature flags - Page-specific logic where appropriate - Error handling and logging Updated: - src/features/index.js: Added imports and initialization for new features - README_REFACTORING.md: Updated documentation with new features - dist/XMOJ.user.js: Rebuilt with new features (485KB, 6870 lines) Total extracted features: 14 (was 11) Remaining features: 27 (was 30) --- README_REFACTORING.md | 9 +- dist/XMOJ.user.js | 222 +++++++++++++++++++++++++++++++--- src/features/auto-o2.js | 36 ++++++ src/features/index.js | 9 ++ src/features/replace-links.js | 33 +++++ src/features/translate.js | 107 ++++++++++++++++ 6 files changed, 393 insertions(+), 23 deletions(-) create mode 100644 src/features/auto-o2.js create mode 100644 src/features/replace-links.js create mode 100644 src/features/translate.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md index 0bb5485c..b0212fff 100644 --- a/README_REFACTORING.md +++ b/README_REFACTORING.md @@ -109,6 +109,9 @@ The following features have been extracted into separate modules under `src/feat - **AddColorText** (`add-color-text.js`) - Adds CSS classes for colored text (red, green, blue) - **SavePassword** (`save-password.js`) - Automatically saves and fills login credentials - **RemoveAlerts** (`remove-alerts.js`) - Removes redundant alerts and warnings +- **ReplaceLinks** (`replace-links.js`) - Replaces bracketed links with styled buttons +- **AutoO2** (`auto-o2.js`) - Automatically enables O2 optimization flag for code submissions +- **Translate** (`translate.js`) - Translates English text to Chinese throughout the site ### Feature Extraction Pattern @@ -146,15 +149,15 @@ To extract additional features from `bootstrap.js`: The following features remain in `bootstrap.js` and can be extracted following the pattern above: - AddUnits -- ApplyData, AutoCheat, AutoCountdown, AutoO2, AutoRefresh +- ApplyData, AutoCheat, AutoCountdown, AutoRefresh - BBSPopup, CompileError, CopyMD - DarkMode (theme already extracted, check for additional code) - DebugMode, DownloadPlayback, ExportACCode - IOFile, ImproveACRate, LoginFailed, MessagePopup - MoreSTD, NewBootstrap, NewDownload, NewTopBar - OpenAllProblem, ProblemSwitcher, Rating -- RefreshSolution, ReplaceLinks, ResetType -- Translate, UploadStd +- RefreshSolution, ResetType +- UploadStd ## Development diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js index 4b65fb32..c36fe04c 100644 --- a/dist/XMOJ.user.js +++ b/dist/XMOJ.user.js @@ -5094,7 +5094,7 @@ int main() * Initialize auto login feature * Checks if user is logged in and redirects to login page if necessary */ - function init$a() { + function init$d() { // Only execute if AutoLogin feature is enabled if (!UtilityEnabled("AutoLogin")) { return; @@ -5154,7 +5154,7 @@ int main() * @param {Function} context.PurifyHTML - Function to sanitize HTML content * @param {Function} context.RenderMathJax - Function to render math formulas */ - function init$9(context) { + function init$c(context) { // Only execute if Discussion feature is enabled if (!UtilityEnabled("Discussion")) { return; @@ -6037,7 +6037,7 @@ int main() * * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 */ - function init$8() { + function init$b() { // Only execute if CopySamples feature is enabled if (!UtilityEnabled("CopySamples")) { return; @@ -6084,7 +6084,7 @@ int main() * - Adds a "Compare Submissions" button on problem pages * - Creates comparison interface on comparesource.php page */ - async function init$7() { + async function init$a() { // Only execute if CompareSource feature is enabled if (!UtilityEnabled("CompareSource")) { return; @@ -6266,7 +6266,7 @@ int main() * - Line 2500-2505: Remove submission child nodes on userinfo page * - Line 3209-3211: Remove h2.lang_en on problem_solution page */ - function init$6() { + function init$9() { // Only execute if RemoveUseless feature is enabled if (!UtilityEnabled("RemoveUseless")) { return; @@ -6344,7 +6344,7 @@ int main() * - Lines 219-222: Text replacement * - Line 304: Navbar brand text */ - function init$5() { + function init$8() { // Only execute if ReplaceXM feature is enabled if (!UtilityEnabled("ReplaceXM")) { return; @@ -6375,7 +6375,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 404-417: Status text replacement */ - function init$4() { + function init$7() { // Only execute if ReplaceYN feature is enabled if (!UtilityEnabled("ReplaceYN")) { return; @@ -6416,7 +6416,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 381-384: Animation CSS */ - function init$3() { + function init$6() { // Only execute if AddAnimation feature is enabled if (!UtilityEnabled("AddAnimation")) { return; @@ -6446,7 +6446,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 386-395: Color text CSS */ - function init$2() { + function init$5() { // Only execute if AddColorText feature is enabled if (!UtilityEnabled("AddColorText")) { return; @@ -6488,7 +6488,7 @@ int main() * - Lines 2850-2852: Clear credentials on failure * - Lines 2867-2876: Auto-fill and auto-submit login form */ - function init$1() { + function init$4() { // Only execute on login page if (location.pathname !== "/loginpage.php") { return; @@ -6539,7 +6539,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1666-1667: Modify contest start link */ - function init() { + function init$3() { // Only execute if RemoveAlerts feature is enabled if (!UtilityEnabled("RemoveAlerts")) { return; @@ -6564,6 +6564,182 @@ int main() } } + /** + * Replace Links Feature + * Replaces bracketed links with styled buttons + * Feature ID: ReplaceLinks + * Type: F (Format/UI) + * Description: 将网站中所有以方括号包装的链接替换为按钮 + */ + + + /** + * Initialize ReplaceLinks feature + * Replaces all links in format [text] with styled buttons + * + * Example transformation: + * [Problem 1001] + * -> + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 216-218: Link to button replacement + */ + function init$2() { + // Only execute if ReplaceLinks feature is enabled + if (!UtilityEnabled("ReplaceLinks")) { + return; + } + + // Replace all bracketed links with buttons + document.body.innerHTML = String(document.body.innerHTML).replaceAll( + /\[([^<]*)<\/a>\]/g, + '' + ); + } + + /** + * Auto O2 Feature + * Automatically enables O2 optimization flag for code submissions + * Feature ID: AutoO2 + * Type: U (Utility) + * Description: 自动启用O2编译优化标志 + */ + + + /** + * Initialize AutoO2 feature + * Automatically checks the "Enable O2" checkbox on problem submission pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2020-2022: Auto-check O2 flag + */ + function init$1() { + // Only execute if AutoO2 feature is enabled + if (!UtilityEnabled("AutoO2")) { + return; + } + + // Only execute on problem pages + if (location.pathname !== "/problem.php") { + return; + } + + // Wait a bit for the page to be ready + setTimeout(() => { + const o2Checkbox = document.querySelector("#enable_O2"); + if (o2Checkbox) { + o2Checkbox.checked = true; + } + }, 100); + } + + /** + * Translate Feature + * Translates English text to Chinese throughout the site + * Feature ID: Translate + * Type: F (Format/UI) + * Description: 统一使用中文,翻译了部分英文 + */ + + + /** + * Initialize Translate feature + * Translates various English UI elements to Chinese based on current page + * + * Translations include: + * - Navbar: "Problems" -> "题库" + * - Problem set page: Form placeholders and table headers + * - Contest page: Table headers + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 211-213: Navbar translation + * - Lines 1073-1078: Problemset page translations + * - Lines 1611-1617: Contest page translations + */ + function init() { + // Only execute if Translate feature is enabled + if (!UtilityEnabled("Translate")) { + return; + } + + const pathname = location.pathname; + + // Translate navbar (on all pages) + translateNavbar(); + + // Page-specific translations + if (pathname === "/problemset.php") { + translateProblemsetPage(); + } else if (pathname === "/contest.php") { + translateContestPage(); + } + } + + /** + * Translate navbar elements + */ + function translateNavbar() { + try { + const problemsLink = document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a"); + if (problemsLink) { + problemsLink.innerText = "题库"; + } + } catch (e) { + console.error('[Translate] Error translating navbar:', e); + } + } + + /** + * Translate problemset page elements + */ + function translateProblemsetPage() { + try { + // Translate search form placeholders and buttons + const problemIdInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input"); + if (problemIdInput) { + problemIdInput.placeholder = "题目编号"; + } + + const confirmButton = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button"); + if (confirmButton) { + confirmButton.innerText = "确认"; + } + + const searchInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input"); + if (searchInput) { + searchInput.placeholder = "标题或内容"; + } + + // Translate table header + const statusHeader = document.querySelector("#problemset > thead > tr > th:nth-child(1)"); + if (statusHeader) { + statusHeader.innerText = "状态"; + } + } catch (e) { + console.error('[Translate] Error translating problemset page:', e); + } + } + + /** + * Translate contest page table headers + */ + function translateContestPage() { + try { + const tableHeader = document.querySelector("body > div > div.mt-3 > center > table > thead > tr"); + if (tableHeader && tableHeader.childNodes.length >= 4) { + tableHeader.childNodes[0].innerText = "编号"; + tableHeader.childNodes[1].innerText = "标题"; + tableHeader.childNodes[2].innerText = "状态"; + tableHeader.childNodes[3].remove(); + if (tableHeader.childNodes[3]) { + tableHeader.childNodes[3].innerText = "创建者"; + } + } + } catch (e) { + console.error('[Translate] Error translating contest page:', e); + } + } + /** * Feature loader - Initializes all extracted feature modules * @@ -6590,24 +6766,27 @@ int main() async function initializeFeatures(context) { try { // Initialize features that need to run early (before main page load) - init$a(); + init$d(); // Initialize features that clean up/modify the page - init$6(); - init(); + init$9(); + init$3(); // Initialize cosmetic/styling features - init$3(); - init$2(); + init$6(); + init$5(); // Initialize text replacement features - init$5(); - init$4(); + init$8(); + init$7(); + init$2(); + init(); // Initialize page-specific features - init$8(); + init$b(); + init$4(); init$1(); - await init$7(); + await init$a(); // Initialize complex features that need context if (context) ; @@ -6635,6 +6814,9 @@ int main() 'AddColorText', 'SavePassword', 'RemoveAlerts', + 'ReplaceLinks', + 'AutoO2', + 'Translate', ]; } diff --git a/src/features/auto-o2.js b/src/features/auto-o2.js new file mode 100644 index 00000000..77025231 --- /dev/null +++ b/src/features/auto-o2.js @@ -0,0 +1,36 @@ +/** + * Auto O2 Feature + * Automatically enables O2 optimization flag for code submissions + * Feature ID: AutoO2 + * Type: U (Utility) + * Description: 自动启用O2编译优化标志 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AutoO2 feature + * Automatically checks the "Enable O2" checkbox on problem submission pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2020-2022: Auto-check O2 flag + */ +export function init() { + // Only execute if AutoO2 feature is enabled + if (!UtilityEnabled("AutoO2")) { + return; + } + + // Only execute on problem pages + if (location.pathname !== "/problem.php") { + return; + } + + // Wait a bit for the page to be ready + setTimeout(() => { + const o2Checkbox = document.querySelector("#enable_O2"); + if (o2Checkbox) { + o2Checkbox.checked = true; + } + }, 100); +} diff --git a/src/features/index.js b/src/features/index.js index c8cb9c24..5179cf03 100644 --- a/src/features/index.js +++ b/src/features/index.js @@ -17,6 +17,9 @@ import { init as initAddAnimation } from './add-animation.js'; import { init as initAddColorText } from './add-color-text.js'; import { init as initSavePassword } from './save-password.js'; import { init as initRemoveAlerts } from './remove-alerts.js'; +import { init as initReplaceLinks } from './replace-links.js'; +import { init as initAutoO2 } from './auto-o2.js'; +import { init as initTranslate } from './translate.js'; /** * Initialize all feature modules @@ -48,10 +51,13 @@ export async function initializeFeatures(context) { // Initialize text replacement features initReplaceXM(); initReplaceYN(); + initReplaceLinks(); + initTranslate(); // Initialize page-specific features initCopySamples(); initSavePassword(); + initAutoO2(); await initCompareSource(); // Initialize complex features that need context @@ -82,5 +88,8 @@ export function getExtractedFeatures() { 'AddColorText', 'SavePassword', 'RemoveAlerts', + 'ReplaceLinks', + 'AutoO2', + 'Translate', ]; } diff --git a/src/features/replace-links.js b/src/features/replace-links.js new file mode 100644 index 00000000..f6074638 --- /dev/null +++ b/src/features/replace-links.js @@ -0,0 +1,33 @@ +/** + * Replace Links Feature + * Replaces bracketed links with styled buttons + * Feature ID: ReplaceLinks + * Type: F (Format/UI) + * Description: 将网站中所有以方括号包装的链接替换为按钮 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ReplaceLinks feature + * Replaces all links in format [text] with styled buttons + * + * Example transformation: + * [Problem 1001] + * -> + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 216-218: Link to button replacement + */ +export function init() { + // Only execute if ReplaceLinks feature is enabled + if (!UtilityEnabled("ReplaceLinks")) { + return; + } + + // Replace all bracketed links with buttons + document.body.innerHTML = String(document.body.innerHTML).replaceAll( + /\[([^<]*)<\/a>\]/g, + '' + ); +} diff --git a/src/features/translate.js b/src/features/translate.js new file mode 100644 index 00000000..d82b404b --- /dev/null +++ b/src/features/translate.js @@ -0,0 +1,107 @@ +/** + * Translate Feature + * Translates English text to Chinese throughout the site + * Feature ID: Translate + * Type: F (Format/UI) + * Description: 统一使用中文,翻译了部分英文 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize Translate feature + * Translates various English UI elements to Chinese based on current page + * + * Translations include: + * - Navbar: "Problems" -> "题库" + * - Problem set page: Form placeholders and table headers + * - Contest page: Table headers + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 211-213: Navbar translation + * - Lines 1073-1078: Problemset page translations + * - Lines 1611-1617: Contest page translations + */ +export function init() { + // Only execute if Translate feature is enabled + if (!UtilityEnabled("Translate")) { + return; + } + + const pathname = location.pathname; + + // Translate navbar (on all pages) + translateNavbar(); + + // Page-specific translations + if (pathname === "/problemset.php") { + translateProblemsetPage(); + } else if (pathname === "/contest.php") { + translateContestPage(); + } +} + +/** + * Translate navbar elements + */ +function translateNavbar() { + try { + const problemsLink = document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a"); + if (problemsLink) { + problemsLink.innerText = "题库"; + } + } catch (e) { + console.error('[Translate] Error translating navbar:', e); + } +} + +/** + * Translate problemset page elements + */ +function translateProblemsetPage() { + try { + // Translate search form placeholders and buttons + const problemIdInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input"); + if (problemIdInput) { + problemIdInput.placeholder = "题目编号"; + } + + const confirmButton = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button"); + if (confirmButton) { + confirmButton.innerText = "确认"; + } + + const searchInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input"); + if (searchInput) { + searchInput.placeholder = "标题或内容"; + } + + // Translate table header + const statusHeader = document.querySelector("#problemset > thead > tr > th:nth-child(1)"); + if (statusHeader) { + statusHeader.innerText = "状态"; + } + } catch (e) { + console.error('[Translate] Error translating problemset page:', e); + } +} + +/** + * Translate contest page table headers + */ +function translateContestPage() { + try { + const tableHeader = document.querySelector("body > div > div.mt-3 > center > table > thead > tr"); + if (tableHeader && tableHeader.childNodes.length >= 4) { + tableHeader.childNodes[0].innerText = "编号"; + tableHeader.childNodes[1].innerText = "标题"; + tableHeader.childNodes[2].innerText = "状态"; + tableHeader.childNodes[3].remove(); + if (tableHeader.childNodes[3]) { + tableHeader.childNodes[3].innerText = "创建者"; + } + } + } catch (e) { + console.error('[Translate] Error translating contest page:', e); + } +} From 72d5ae75b31cb7864fbfecd9b49625ee6c469bf0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Nov 2025 11:09:40 +0000 Subject: [PATCH 08/26] Extract 4 utility features: AutoCountdown, MoreSTD, ExportACCode, OpenAllProblem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit continues feature extraction by adding 4 utility feature modules: New features extracted: - AutoCountdown: Automatically updates countdown timers on the page - Updates timers with class "UpdateByJS" every second - Reloads page when countdown expires - Disables default clock on contest pages - MoreSTD: Adds standard solution links to contest problem tables - Reorganizes contest problem table - Adds "标程" column with links to standard solutions - ExportACCode: Exports all accepted code solutions as a ZIP file - Adds export button to user info page - Fetches AC code from export_ac_code.php - Uses JSZip library to create downloadable ZIP - OpenAllProblem: Adds buttons to open all/unsolved problems - "打开全部题目" - Opens all contest problems in new tabs - "打开未解决题目" - Opens only unsolved problems All features follow the established pattern with: - init() function for initialization - UtilityEnabled() checks for feature flags - Page-specific logic and error handling Updated: - src/features/index.js: Added imports and initialization for new features - README_REFACTORING.md: Updated documentation with new features - dist/XMOJ.user.js: Rebuilt with new features (498KB, 7216 lines) Total extracted features: 18 (was 14) Remaining features: 23 (was 27) --- README_REFACTORING.md | 12 +- dist/XMOJ.user.js | 394 +++++++++++++++++++++++++++++-- src/features/auto-countdown.js | 71 ++++++ src/features/export-ac-code.js | 106 +++++++++ src/features/index.js | 14 ++ src/features/more-std.js | 81 +++++++ src/features/open-all-problem.js | 78 ++++++ 7 files changed, 728 insertions(+), 28 deletions(-) create mode 100644 src/features/auto-countdown.js create mode 100644 src/features/export-ac-code.js create mode 100644 src/features/more-std.js create mode 100644 src/features/open-all-problem.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md index b0212fff..e411cb1a 100644 --- a/README_REFACTORING.md +++ b/README_REFACTORING.md @@ -112,6 +112,10 @@ The following features have been extracted into separate modules under `src/feat - **ReplaceLinks** (`replace-links.js`) - Replaces bracketed links with styled buttons - **AutoO2** (`auto-o2.js`) - Automatically enables O2 optimization flag for code submissions - **Translate** (`translate.js`) - Translates English text to Chinese throughout the site +- **AutoCountdown** (`auto-countdown.js`) - Automatically updates countdown timers on the page +- **MoreSTD** (`more-std.js`) - Adds standard solution links to contest problem tables +- **ExportACCode** (`export-ac-code.js`) - Exports all accepted code solutions as a ZIP file +- **OpenAllProblem** (`open-all-problem.js`) - Adds buttons to open all problems or only unsolved ones ### Feature Extraction Pattern @@ -149,13 +153,13 @@ To extract additional features from `bootstrap.js`: The following features remain in `bootstrap.js` and can be extracted following the pattern above: - AddUnits -- ApplyData, AutoCheat, AutoCountdown, AutoRefresh +- ApplyData, AutoCheat, AutoRefresh - BBSPopup, CompileError, CopyMD - DarkMode (theme already extracted, check for additional code) -- DebugMode, DownloadPlayback, ExportACCode +- DebugMode, DownloadPlayback - IOFile, ImproveACRate, LoginFailed, MessagePopup -- MoreSTD, NewBootstrap, NewDownload, NewTopBar -- OpenAllProblem, ProblemSwitcher, Rating +- NewBootstrap, NewDownload, NewTopBar +- ProblemSwitcher, Rating - RefreshSolution, ResetType - UploadStd diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js index c36fe04c..44940ac7 100644 --- a/dist/XMOJ.user.js +++ b/dist/XMOJ.user.js @@ -5094,7 +5094,7 @@ int main() * Initialize auto login feature * Checks if user is logged in and redirects to login page if necessary */ - function init$d() { + function init$h() { // Only execute if AutoLogin feature is enabled if (!UtilityEnabled("AutoLogin")) { return; @@ -5154,7 +5154,7 @@ int main() * @param {Function} context.PurifyHTML - Function to sanitize HTML content * @param {Function} context.RenderMathJax - Function to render math formulas */ - function init$c(context) { + function init$g(context) { // Only execute if Discussion feature is enabled if (!UtilityEnabled("Discussion")) { return; @@ -6037,7 +6037,7 @@ int main() * * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 */ - function init$b() { + function init$f() { // Only execute if CopySamples feature is enabled if (!UtilityEnabled("CopySamples")) { return; @@ -6084,7 +6084,7 @@ int main() * - Adds a "Compare Submissions" button on problem pages * - Creates comparison interface on comparesource.php page */ - async function init$a() { + async function init$e() { // Only execute if CompareSource feature is enabled if (!UtilityEnabled("CompareSource")) { return; @@ -6266,7 +6266,7 @@ int main() * - Line 2500-2505: Remove submission child nodes on userinfo page * - Line 3209-3211: Remove h2.lang_en on problem_solution page */ - function init$9() { + function init$d() { // Only execute if RemoveUseless feature is enabled if (!UtilityEnabled("RemoveUseless")) { return; @@ -6344,7 +6344,7 @@ int main() * - Lines 219-222: Text replacement * - Line 304: Navbar brand text */ - function init$8() { + function init$c() { // Only execute if ReplaceXM feature is enabled if (!UtilityEnabled("ReplaceXM")) { return; @@ -6375,7 +6375,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 404-417: Status text replacement */ - function init$7() { + function init$b() { // Only execute if ReplaceYN feature is enabled if (!UtilityEnabled("ReplaceYN")) { return; @@ -6416,7 +6416,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 381-384: Animation CSS */ - function init$6() { + function init$a() { // Only execute if AddAnimation feature is enabled if (!UtilityEnabled("AddAnimation")) { return; @@ -6446,7 +6446,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 386-395: Color text CSS */ - function init$5() { + function init$9() { // Only execute if AddColorText feature is enabled if (!UtilityEnabled("AddColorText")) { return; @@ -6488,7 +6488,7 @@ int main() * - Lines 2850-2852: Clear credentials on failure * - Lines 2867-2876: Auto-fill and auto-submit login form */ - function init$4() { + function init$8() { // Only execute on login page if (location.pathname !== "/loginpage.php") { return; @@ -6539,7 +6539,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1666-1667: Modify contest start link */ - function init$3() { + function init$7() { // Only execute if RemoveAlerts feature is enabled if (!UtilityEnabled("RemoveAlerts")) { return; @@ -6584,7 +6584,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 216-218: Link to button replacement */ - function init$2() { + function init$6() { // Only execute if ReplaceLinks feature is enabled if (!UtilityEnabled("ReplaceLinks")) { return; @@ -6613,7 +6613,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 2020-2022: Auto-check O2 flag */ - function init$1() { + function init$5() { // Only execute if AutoO2 feature is enabled if (!UtilityEnabled("AutoO2")) { return; @@ -6656,7 +6656,7 @@ int main() * - Lines 1073-1078: Problemset page translations * - Lines 1611-1617: Contest page translations */ - function init() { + function init$4() { // Only execute if Translate feature is enabled if (!UtilityEnabled("Translate")) { return; @@ -6740,6 +6740,342 @@ int main() } } + /** + * Auto Countdown Feature + * Automatically updates countdown timers on the page + * Feature ID: AutoCountdown + * Type: U (Utility) + * Description: 自动更新页面上的倒计时器 + */ + + + /** + * Initialize AutoCountdown feature + * Updates countdown timers with class "UpdateByJS" and reloads page when time expires + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 547-566: Countdown timer update logic + * - Lines 1592-1594: Disables default clock on contest page + */ + function init$3() { + // Only execute if AutoCountdown feature is enabled + if (!UtilityEnabled("AutoCountdown")) { + return; + } + + // Disable default clock on contest page + if (location.pathname === "/contest.php") { + window.clock = () => {}; + } + + // Update countdown timers + const updateCountdowns = () => { + const elements = document.getElementsByClassName("UpdateByJS"); + + for (let i = 0; i < elements.length; i++) { + const endTime = elements[i].getAttribute("EndTime"); + + if (endTime === null) { + elements[i].classList.remove("UpdateByJS"); + continue; + } + + const timeStamp = parseInt(endTime) - new Date().getTime(); + + // Reload page when countdown expires + if (timeStamp < 3000) { + elements[i].classList.remove("UpdateByJS"); + location.reload(); + } + + // Calculate remaining time + const currentDate = new Date(timeStamp); + const day = parseInt((timeStamp / 1000 / 60 / 60 / 24).toFixed(0)); + const hour = currentDate.getUTCHours(); + const minute = currentDate.getUTCMinutes(); + const second = currentDate.getUTCSeconds(); + + // Format and display countdown + elements[i].innerHTML = + (day !== 0 ? day + "天" : "") + + (hour !== 0 ? (hour < 10 ? "0" : "") + hour + "小时" : "") + + (minute !== 0 ? (minute < 10 ? "0" : "") + minute + "分" : "") + + (second !== 0 ? (second < 10 ? "0" : "") + second + "秒" : ""); + } + }; + + // Initial update + updateCountdowns(); + + // Update every second + setInterval(updateCountdowns, 1000); + } + + /** + * More STD Feature + * Adds standard solution links to contest problem tables + * Feature ID: MoreSTD + * Type: U (Utility) + * Description: 在比赛题目表格中添加标程链接 + */ + + + /** + * Initialize MoreSTD feature + * Reorganizes contest problem table to add standard solution links + * + * This feature: + * 1. Removes any existing "标程" column + * 2. Adds a new "标程" column header + * 3. Adds links to standard solutions for each problem + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1699-1717: Standard solution column management + */ + function init$2() { + // Only execute if MoreSTD feature is enabled + if (!UtilityEnabled("MoreSTD")) { + return; + } + + // Only execute on contest pages + if (location.pathname !== "/contest.php") { + return; + } + + // Check if we're in a contest with problem list + const searchParams = new URLSearchParams(location.search); + if (!searchParams.get("cid")) { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + const tableHeader = document.querySelector("#problemset > thead > tr"); + + // Only proceed if table exists and has "标程" column + if (!tableHeader || tableHeader.innerHTML.indexOf("标程") === -1) { + return; + } + + // Remove existing "标程" column + let headerCells = tableHeader.children; + for (let i = 0; i < headerCells.length; i++) { + if (headerCells[i].innerText === "标程") { + headerCells[i].remove(); + + // Remove corresponding cells from each row + const bodyRows = document.querySelector("#problemset > tbody").children; + for (let j = 0; j < bodyRows.length; j++) { + if (bodyRows[j].children[i] !== undefined) { + bodyRows[j].children[i].remove(); + } + } + } + } + + // Add new "标程" column header + tableHeader.innerHTML += '标程'; + + // Add standard solution links for each problem + const bodyRows = document.querySelector("#problemset > tbody").children; + const cid = Number(searchParams.get("cid")); + + for (let i = 0; i < bodyRows.length; i++) { + bodyRows[i].innerHTML += + `打开`; + } + } catch (error) { + console.error('[MoreSTD] Error adding standard solution links:', error); + } + }, 100); + } + + /** + * Export AC Code Feature + * Exports all accepted code solutions as a ZIP file + * Feature ID: ExportACCode + * Type: U (Utility) + * Description: 导出所有AC代码为ZIP文件 + */ + + + /** + * Initialize ExportACCode feature + * Adds a button to export all accepted code solutions + * + * This feature: + * 1. Adds an "导出AC代码" button to the user page + * 2. Fetches all AC code from export_ac_code.php + * 3. Creates a ZIP file with all accepted solutions + * 4. Uses JSZip library for compression + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2445-2495: Export AC code button and logic + */ + function init$1() { + // Only execute if ExportACCode feature is enabled + if (!UtilityEnabled("ExportACCode")) { + return; + } + + // Only execute on user information page + if (location.pathname !== "/userinfo.php") { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + const container = document.querySelector("body > div.container > div"); + if (!container) return; + + // Create export button + const exportButton = document.createElement("button"); + container.appendChild(exportButton); + exportButton.innerText = "导出AC代码"; + exportButton.className = "btn btn-outline-secondary"; + + // Add click handler + exportButton.addEventListener("click", () => { + exportButton.disabled = true; + exportButton.innerText = "正在导出..."; + + const request = new XMLHttpRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState === 4) { + if (request.status === 200) { + const response = request.responseText; + const acCode = response.split("------------------------------------------------------\r\n"); + + // Load JSZip library + const scriptElement = document.createElement("script"); + scriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; + document.head.appendChild(scriptElement); + + scriptElement.onload = () => { + const zip = new JSZip(); + + // Add each AC code file to ZIP + for (let i = 0; i < acCode.length; i++) { + let currentCode = acCode[i]; + if (currentCode !== "") { + const currentQuestionID = currentCode.substring(7, 11); + currentCode = currentCode.substring(14); + currentCode = currentCode.replaceAll("\r", ""); + zip.file(currentQuestionID + ".cpp", currentCode); + } + } + + exportButton.innerText = "正在生成压缩包……"; + zip.generateAsync({ type: "blob" }) + .then((content) => { + saveAs(content, "ACCodes.zip"); + exportButton.innerText = "AC代码导出成功"; + exportButton.disabled = false; + setTimeout(() => { + exportButton.innerText = "导出AC代码"; + }, 1000); + }); + }; + } else { + exportButton.disabled = false; + exportButton.innerText = "AC代码导出失败"; + setTimeout(() => { + exportButton.innerText = "导出AC代码"; + }, 1000); + } + } + }); + + request.open("GET", "https://www.xmoj.tech/export_ac_code.php", true); + request.send(); + }); + } catch (error) { + console.error('[ExportACCode] Error initializing export button:', error); + } + }, 100); + } + + /** + * Open All Problem Feature + * Adds buttons to open all problems or only unsolved problems in new tabs + * Feature ID: OpenAllProblem + * Type: U (Utility) + * Description: 添加按钮以在新标签页中打开所有题目或仅未解决的题目 + */ + + + /** + * Initialize OpenAllProblem feature + * Adds two buttons to contest pages: + * 1. "打开全部题目" - Opens all contest problems in new tabs + * 2. "打开未解决题目" - Opens only unsolved problems in new tabs + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1817-1841: Open all problem buttons + */ + function init() { + // Only execute if OpenAllProblem feature is enabled + if (!UtilityEnabled("OpenAllProblem")) { + return; + } + + // Only execute on contest pages with problem list + if (location.pathname !== "/contest.php") { + return; + } + + const searchParams = new URLSearchParams(location.search); + if (!searchParams.get("cid")) { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + // Find or create container for buttons + let cheatDiv = document.querySelector("#CheatDiv"); + if (!cheatDiv) { + return; + } + + // Create "Open All Problems" button + const openAllButton = document.createElement("button"); + openAllButton.className = "btn btn-outline-secondary"; + openAllButton.innerText = "打开全部题目"; + openAllButton.style.marginRight = "5px"; + cheatDiv.appendChild(openAllButton); + + openAllButton.addEventListener("click", () => { + const rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < rows.length; i++) { + open(rows[i].children[2].children[0].href, "_blank"); + } + }); + + // Create "Open Unsolved Problems" button + const openUnsolvedButton = document.createElement("button"); + openUnsolvedButton.className = "btn btn-outline-secondary"; + openUnsolvedButton.innerText = "打开未解决题目"; + cheatDiv.appendChild(openUnsolvedButton); + + openUnsolvedButton.addEventListener("click", () => { + const rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < rows.length; i++) { + // Only open problems that are not marked as solved (status_y) + if (!rows[i].children[0].children[0].classList.contains("status_y")) { + open(rows[i].children[2].children[0].href, "_blank"); + } + } + }); + } catch (error) { + console.error('[OpenAllProblem] Error initializing buttons:', error); + } + }, 100); + } + /** * Feature loader - Initializes all extracted feature modules * @@ -6766,27 +7102,33 @@ int main() async function initializeFeatures(context) { try { // Initialize features that need to run early (before main page load) - init$d(); + init$h(); // Initialize features that clean up/modify the page - init$9(); - init$3(); + init$d(); + init$7(); // Initialize cosmetic/styling features - init$6(); - init$5(); + init$a(); + init$9(); // Initialize text replacement features - init$8(); - init$7(); + init$c(); + init$b(); + init$6(); + init$4(); + + // Initialize utility features + init$3(); init$2(); init(); // Initialize page-specific features - init$b(); - init$4(); + init$f(); + init$8(); + init$5(); init$1(); - await init$a(); + await init$e(); // Initialize complex features that need context if (context) ; @@ -6817,6 +7159,10 @@ int main() 'ReplaceLinks', 'AutoO2', 'Translate', + 'AutoCountdown', + 'MoreSTD', + 'ExportACCode', + 'OpenAllProblem', ]; } diff --git a/src/features/auto-countdown.js b/src/features/auto-countdown.js new file mode 100644 index 00000000..9319131e --- /dev/null +++ b/src/features/auto-countdown.js @@ -0,0 +1,71 @@ +/** + * Auto Countdown Feature + * Automatically updates countdown timers on the page + * Feature ID: AutoCountdown + * Type: U (Utility) + * Description: 自动更新页面上的倒计时器 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AutoCountdown feature + * Updates countdown timers with class "UpdateByJS" and reloads page when time expires + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 547-566: Countdown timer update logic + * - Lines 1592-1594: Disables default clock on contest page + */ +export function init() { + // Only execute if AutoCountdown feature is enabled + if (!UtilityEnabled("AutoCountdown")) { + return; + } + + // Disable default clock on contest page + if (location.pathname === "/contest.php") { + window.clock = () => {}; + } + + // Update countdown timers + const updateCountdowns = () => { + const elements = document.getElementsByClassName("UpdateByJS"); + + for (let i = 0; i < elements.length; i++) { + const endTime = elements[i].getAttribute("EndTime"); + + if (endTime === null) { + elements[i].classList.remove("UpdateByJS"); + continue; + } + + const timeStamp = parseInt(endTime) - new Date().getTime(); + + // Reload page when countdown expires + if (timeStamp < 3000) { + elements[i].classList.remove("UpdateByJS"); + location.reload(); + } + + // Calculate remaining time + const currentDate = new Date(timeStamp); + const day = parseInt((timeStamp / 1000 / 60 / 60 / 24).toFixed(0)); + const hour = currentDate.getUTCHours(); + const minute = currentDate.getUTCMinutes(); + const second = currentDate.getUTCSeconds(); + + // Format and display countdown + elements[i].innerHTML = + (day !== 0 ? day + "天" : "") + + (hour !== 0 ? (hour < 10 ? "0" : "") + hour + "小时" : "") + + (minute !== 0 ? (minute < 10 ? "0" : "") + minute + "分" : "") + + (second !== 0 ? (second < 10 ? "0" : "") + second + "秒" : ""); + } + }; + + // Initial update + updateCountdowns(); + + // Update every second + setInterval(updateCountdowns, 1000); +} diff --git a/src/features/export-ac-code.js b/src/features/export-ac-code.js new file mode 100644 index 00000000..5523cdcd --- /dev/null +++ b/src/features/export-ac-code.js @@ -0,0 +1,106 @@ +/** + * Export AC Code Feature + * Exports all accepted code solutions as a ZIP file + * Feature ID: ExportACCode + * Type: U (Utility) + * Description: 导出所有AC代码为ZIP文件 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ExportACCode feature + * Adds a button to export all accepted code solutions + * + * This feature: + * 1. Adds an "导出AC代码" button to the user page + * 2. Fetches all AC code from export_ac_code.php + * 3. Creates a ZIP file with all accepted solutions + * 4. Uses JSZip library for compression + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2445-2495: Export AC code button and logic + */ +export function init() { + // Only execute if ExportACCode feature is enabled + if (!UtilityEnabled("ExportACCode")) { + return; + } + + // Only execute on user information page + if (location.pathname !== "/userinfo.php") { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + const container = document.querySelector("body > div.container > div"); + if (!container) return; + + // Create export button + const exportButton = document.createElement("button"); + container.appendChild(exportButton); + exportButton.innerText = "导出AC代码"; + exportButton.className = "btn btn-outline-secondary"; + + // Add click handler + exportButton.addEventListener("click", () => { + exportButton.disabled = true; + exportButton.innerText = "正在导出..."; + + const request = new XMLHttpRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState === 4) { + if (request.status === 200) { + const response = request.responseText; + const acCode = response.split("------------------------------------------------------\r\n"); + + // Load JSZip library + const scriptElement = document.createElement("script"); + scriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; + document.head.appendChild(scriptElement); + + scriptElement.onload = () => { + const zip = new JSZip(); + + // Add each AC code file to ZIP + for (let i = 0; i < acCode.length; i++) { + let currentCode = acCode[i]; + if (currentCode !== "") { + const currentQuestionID = currentCode.substring(7, 11); + currentCode = currentCode.substring(14); + currentCode = currentCode.replaceAll("\r", ""); + zip.file(currentQuestionID + ".cpp", currentCode); + } + } + + exportButton.innerText = "正在生成压缩包……"; + zip.generateAsync({ type: "blob" }) + .then((content) => { + saveAs(content, "ACCodes.zip"); + exportButton.innerText = "AC代码导出成功"; + exportButton.disabled = false; + setTimeout(() => { + exportButton.innerText = "导出AC代码"; + }, 1000); + }); + }; + } else { + exportButton.disabled = false; + exportButton.innerText = "AC代码导出失败"; + setTimeout(() => { + exportButton.innerText = "导出AC代码"; + }, 1000); + } + } + }); + + request.open("GET", "https://www.xmoj.tech/export_ac_code.php", true); + request.send(); + }); + } catch (error) { + console.error('[ExportACCode] Error initializing export button:', error); + } + }, 100); +} diff --git a/src/features/index.js b/src/features/index.js index 5179cf03..5d21e96b 100644 --- a/src/features/index.js +++ b/src/features/index.js @@ -20,6 +20,10 @@ import { init as initRemoveAlerts } from './remove-alerts.js'; import { init as initReplaceLinks } from './replace-links.js'; import { init as initAutoO2 } from './auto-o2.js'; import { init as initTranslate } from './translate.js'; +import { init as initAutoCountdown } from './auto-countdown.js'; +import { init as initMoreSTD } from './more-std.js'; +import { init as initExportACCode } from './export-ac-code.js'; +import { init as initOpenAllProblem } from './open-all-problem.js'; /** * Initialize all feature modules @@ -54,10 +58,16 @@ export async function initializeFeatures(context) { initReplaceLinks(); initTranslate(); + // Initialize utility features + initAutoCountdown(); + initMoreSTD(); + initOpenAllProblem(); + // Initialize page-specific features initCopySamples(); initSavePassword(); initAutoO2(); + initExportACCode(); await initCompareSource(); // Initialize complex features that need context @@ -91,5 +101,9 @@ export function getExtractedFeatures() { 'ReplaceLinks', 'AutoO2', 'Translate', + 'AutoCountdown', + 'MoreSTD', + 'ExportACCode', + 'OpenAllProblem', ]; } diff --git a/src/features/more-std.js b/src/features/more-std.js new file mode 100644 index 00000000..bc02f28d --- /dev/null +++ b/src/features/more-std.js @@ -0,0 +1,81 @@ +/** + * More STD Feature + * Adds standard solution links to contest problem tables + * Feature ID: MoreSTD + * Type: U (Utility) + * Description: 在比赛题目表格中添加标程链接 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize MoreSTD feature + * Reorganizes contest problem table to add standard solution links + * + * This feature: + * 1. Removes any existing "标程" column + * 2. Adds a new "标程" column header + * 3. Adds links to standard solutions for each problem + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1699-1717: Standard solution column management + */ +export function init() { + // Only execute if MoreSTD feature is enabled + if (!UtilityEnabled("MoreSTD")) { + return; + } + + // Only execute on contest pages + if (location.pathname !== "/contest.php") { + return; + } + + // Check if we're in a contest with problem list + const searchParams = new URLSearchParams(location.search); + if (!searchParams.get("cid")) { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + const tableHeader = document.querySelector("#problemset > thead > tr"); + + // Only proceed if table exists and has "标程" column + if (!tableHeader || tableHeader.innerHTML.indexOf("标程") === -1) { + return; + } + + // Remove existing "标程" column + let headerCells = tableHeader.children; + for (let i = 0; i < headerCells.length; i++) { + if (headerCells[i].innerText === "标程") { + headerCells[i].remove(); + + // Remove corresponding cells from each row + const bodyRows = document.querySelector("#problemset > tbody").children; + for (let j = 0; j < bodyRows.length; j++) { + if (bodyRows[j].children[i] !== undefined) { + bodyRows[j].children[i].remove(); + } + } + } + } + + // Add new "标程" column header + tableHeader.innerHTML += '标程'; + + // Add standard solution links for each problem + const bodyRows = document.querySelector("#problemset > tbody").children; + const cid = Number(searchParams.get("cid")); + + for (let i = 0; i < bodyRows.length; i++) { + bodyRows[i].innerHTML += + `打开`; + } + } catch (error) { + console.error('[MoreSTD] Error adding standard solution links:', error); + } + }, 100); +} diff --git a/src/features/open-all-problem.js b/src/features/open-all-problem.js new file mode 100644 index 00000000..e2b131fa --- /dev/null +++ b/src/features/open-all-problem.js @@ -0,0 +1,78 @@ +/** + * Open All Problem Feature + * Adds buttons to open all problems or only unsolved problems in new tabs + * Feature ID: OpenAllProblem + * Type: U (Utility) + * Description: 添加按钮以在新标签页中打开所有题目或仅未解决的题目 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize OpenAllProblem feature + * Adds two buttons to contest pages: + * 1. "打开全部题目" - Opens all contest problems in new tabs + * 2. "打开未解决题目" - Opens only unsolved problems in new tabs + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1817-1841: Open all problem buttons + */ +export function init() { + // Only execute if OpenAllProblem feature is enabled + if (!UtilityEnabled("OpenAllProblem")) { + return; + } + + // Only execute on contest pages with problem list + if (location.pathname !== "/contest.php") { + return; + } + + const searchParams = new URLSearchParams(location.search); + if (!searchParams.get("cid")) { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + // Find or create container for buttons + let cheatDiv = document.querySelector("#CheatDiv"); + if (!cheatDiv) { + return; + } + + // Create "Open All Problems" button + const openAllButton = document.createElement("button"); + openAllButton.className = "btn btn-outline-secondary"; + openAllButton.innerText = "打开全部题目"; + openAllButton.style.marginRight = "5px"; + cheatDiv.appendChild(openAllButton); + + openAllButton.addEventListener("click", () => { + const rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < rows.length; i++) { + open(rows[i].children[2].children[0].href, "_blank"); + } + }); + + // Create "Open Unsolved Problems" button + const openUnsolvedButton = document.createElement("button"); + openUnsolvedButton.className = "btn btn-outline-secondary"; + openUnsolvedButton.innerText = "打开未解决题目"; + cheatDiv.appendChild(openUnsolvedButton); + + openUnsolvedButton.addEventListener("click", () => { + const rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < rows.length; i++) { + // Only open problems that are not marked as solved (status_y) + if (!rows[i].children[0].children[0].classList.contains("status_y")) { + open(rows[i].children[2].children[0].href, "_blank"); + } + } + }); + } catch (error) { + console.error('[OpenAllProblem] Error initializing buttons:', error); + } + }, 100); +} From 6726f35b9598cc98ecdbcad8528b7c799d453048 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Nov 2025 11:12:23 +0000 Subject: [PATCH 09/26] Extract 2 more features: DarkMode, ImproveACRate This commit adds 2 additional feature modules: New features extracted: - DarkMode: Enables dark theme for the website - Sets Bootstrap theme attribute to "dark" or "light" - Used by 17 different locations for conditional styling - Exports isDarkMode() helper function for other features - ImproveACRate: Adds a button to resubmit already-AC'd problems - Fetches user's AC problems from userinfo page - Displays current AC rate percentage - Resubmits 3 random AC'd problems to improve statistics - Uses existing AC code from status page All features follow the established pattern with: - init() function for initialization - UtilityEnabled() checks for feature flags - Page-specific logic and error handling Updated: - src/features/index.js: Added imports and initialization for new features - DarkMode initialized early (before other styling features) - README_REFACTORING.md: Updated documentation with new features - dist/XMOJ.user.js: Rebuilt with new features (506KB, 7388 lines) Total extracted features: 20 (was 18) Remaining features: 21 (was 23) --- README_REFACTORING.md | 5 +- dist/XMOJ.user.js | 236 +++++++++++++++++++++++++++----- src/features/dark-mode.js | 40 ++++++ src/features/improve-ac-rate.js | 135 ++++++++++++++++++ src/features/index.js | 8 ++ 5 files changed, 390 insertions(+), 34 deletions(-) create mode 100644 src/features/dark-mode.js create mode 100644 src/features/improve-ac-rate.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md index e411cb1a..b9a38009 100644 --- a/README_REFACTORING.md +++ b/README_REFACTORING.md @@ -116,6 +116,8 @@ The following features have been extracted into separate modules under `src/feat - **MoreSTD** (`more-std.js`) - Adds standard solution links to contest problem tables - **ExportACCode** (`export-ac-code.js`) - Exports all accepted code solutions as a ZIP file - **OpenAllProblem** (`open-all-problem.js`) - Adds buttons to open all problems or only unsolved ones +- **DarkMode** (`dark-mode.js`) - Enables dark theme for the website +- **ImproveACRate** (`improve-ac-rate.js`) - Adds a button to resubmit already-AC'd problems ### Feature Extraction Pattern @@ -155,9 +157,8 @@ The following features remain in `bootstrap.js` and can be extracted following t - AddUnits - ApplyData, AutoCheat, AutoRefresh - BBSPopup, CompileError, CopyMD -- DarkMode (theme already extracted, check for additional code) - DebugMode, DownloadPlayback -- IOFile, ImproveACRate, LoginFailed, MessagePopup +- IOFile, LoginFailed, MessagePopup - NewBootstrap, NewDownload, NewTopBar - ProblemSwitcher, Rating - RefreshSolution, ResetType diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js index 44940ac7..24b0be54 100644 --- a/dist/XMOJ.user.js +++ b/dist/XMOJ.user.js @@ -5094,7 +5094,7 @@ int main() * Initialize auto login feature * Checks if user is logged in and redirects to login page if necessary */ - function init$h() { + function init$j() { // Only execute if AutoLogin feature is enabled if (!UtilityEnabled("AutoLogin")) { return; @@ -5154,7 +5154,7 @@ int main() * @param {Function} context.PurifyHTML - Function to sanitize HTML content * @param {Function} context.RenderMathJax - Function to render math formulas */ - function init$g(context) { + function init$i(context) { // Only execute if Discussion feature is enabled if (!UtilityEnabled("Discussion")) { return; @@ -6037,7 +6037,7 @@ int main() * * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 */ - function init$f() { + function init$h() { // Only execute if CopySamples feature is enabled if (!UtilityEnabled("CopySamples")) { return; @@ -6084,7 +6084,7 @@ int main() * - Adds a "Compare Submissions" button on problem pages * - Creates comparison interface on comparesource.php page */ - async function init$e() { + async function init$g() { // Only execute if CompareSource feature is enabled if (!UtilityEnabled("CompareSource")) { return; @@ -6266,7 +6266,7 @@ int main() * - Line 2500-2505: Remove submission child nodes on userinfo page * - Line 3209-3211: Remove h2.lang_en on problem_solution page */ - function init$d() { + function init$f() { // Only execute if RemoveUseless feature is enabled if (!UtilityEnabled("RemoveUseless")) { return; @@ -6344,7 +6344,7 @@ int main() * - Lines 219-222: Text replacement * - Line 304: Navbar brand text */ - function init$c() { + function init$e() { // Only execute if ReplaceXM feature is enabled if (!UtilityEnabled("ReplaceXM")) { return; @@ -6375,7 +6375,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 404-417: Status text replacement */ - function init$b() { + function init$d() { // Only execute if ReplaceYN feature is enabled if (!UtilityEnabled("ReplaceYN")) { return; @@ -6416,7 +6416,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 381-384: Animation CSS */ - function init$a() { + function init$c() { // Only execute if AddAnimation feature is enabled if (!UtilityEnabled("AddAnimation")) { return; @@ -6446,7 +6446,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 386-395: Color text CSS */ - function init$9() { + function init$b() { // Only execute if AddColorText feature is enabled if (!UtilityEnabled("AddColorText")) { return; @@ -6488,7 +6488,7 @@ int main() * - Lines 2850-2852: Clear credentials on failure * - Lines 2867-2876: Auto-fill and auto-submit login form */ - function init$8() { + function init$a() { // Only execute on login page if (location.pathname !== "/loginpage.php") { return; @@ -6539,7 +6539,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1666-1667: Modify contest start link */ - function init$7() { + function init$9() { // Only execute if RemoveAlerts feature is enabled if (!UtilityEnabled("RemoveAlerts")) { return; @@ -6584,7 +6584,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 216-218: Link to button replacement */ - function init$6() { + function init$8() { // Only execute if ReplaceLinks feature is enabled if (!UtilityEnabled("ReplaceLinks")) { return; @@ -6613,7 +6613,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 2020-2022: Auto-check O2 flag */ - function init$5() { + function init$7() { // Only execute if AutoO2 feature is enabled if (!UtilityEnabled("AutoO2")) { return; @@ -6656,7 +6656,7 @@ int main() * - Lines 1073-1078: Problemset page translations * - Lines 1611-1617: Contest page translations */ - function init$4() { + function init$6() { // Only execute if Translate feature is enabled if (!UtilityEnabled("Translate")) { return; @@ -6757,7 +6757,7 @@ int main() * - Lines 547-566: Countdown timer update logic * - Lines 1592-1594: Disables default clock on contest page */ - function init$3() { + function init$5() { // Only execute if AutoCountdown feature is enabled if (!UtilityEnabled("AutoCountdown")) { return; @@ -6832,7 +6832,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1699-1717: Standard solution column management */ - function init$2() { + function init$4() { // Only execute if MoreSTD feature is enabled if (!UtilityEnabled("MoreSTD")) { return; @@ -6914,7 +6914,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 2445-2495: Export AC code button and logic */ - function init$1() { + function init$3() { // Only execute if ExportACCode feature is enabled if (!UtilityEnabled("ExportACCode")) { return; @@ -7016,7 +7016,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1817-1841: Open all problem buttons */ - function init() { + function init$2() { // Only execute if OpenAllProblem feature is enabled if (!UtilityEnabled("OpenAllProblem")) { return; @@ -7076,6 +7076,172 @@ int main() }, 100); } + /** + * Dark Mode Feature + * Enables dark theme for the website + * Feature ID: DarkMode + * Type: A (Appearance) + * Description: 启用网站深色主题 + */ + + + /** + * Initialize DarkMode feature + * Sets the Bootstrap theme to dark or light based on user preference + * + * Note: This feature also affects other parts of the application: + * - CodeMirror theme selection + * - Contest rank table text colors + * - Problem switcher background + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 247-251: Theme attribute setting + * - Used throughout the codebase for conditional styling (17 occurrences) + */ + function init$1() { + // Set theme based on DarkMode setting + if (UtilityEnabled("DarkMode")) { + document.querySelector("html").setAttribute("data-bs-theme", "dark"); + } else { + document.querySelector("html").setAttribute("data-bs-theme", "light"); + } + } + + /** + * Improve AC Rate Feature + * Adds a button to resubmit already-AC'd problems to improve submission statistics + * Feature ID: ImproveACRate + * Type: U (Utility) + * Description: 添加按钮来重新提交已AC的题目以提高正确率 + */ + + + /** + * Initialize ImproveACRate feature + * Adds a "提高正确率" button that resubmits already-AC'd problems + * + * This feature: + * 1. Fetches user's AC problems from userinfo page + * 2. Displays current AC rate percentage + * 3. On click, randomly selects 3 AC'd problems and resubmits them + * 4. Uses existing AC code from status page + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1405-1463: Improve AC rate button and logic + */ + function init() { + // Only execute if ImproveACRate feature is enabled + if (!UtilityEnabled("ImproveACRate")) { + return; + } + + // Only execute on status page + if (location.pathname !== "/status.php") { + return; + } + + // Need current username + const currentUsername = document.querySelector("#profile")?.innerText; + if (!currentUsername || currentUsername === "登录") { + return; + } + + // Wait for page to be ready + setTimeout(async () => { + try { + const container = document.querySelector("body > div.container > div > div.input-append"); + if (!container) return; + + // Create improve AC rate button + const improveACRateButton = document.createElement("button"); + container.appendChild(improveACRateButton); + improveACRateButton.className = "btn btn-outline-secondary"; + improveACRateButton.innerText = "提高正确率"; + improveACRateButton.disabled = true; + + // Fetch user's AC problems + let acProblems = []; + await fetch(`https://www.xmoj.tech/userinfo.php?user=${currentUsername}`) + .then((response) => response.text()) + .then((response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + + // Calculate and display AC rate + const acCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText); + const submitCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText); + const acRate = (acCount / submitCount * 100).toFixed(2); + improveACRateButton.innerText += ` (${acRate}%)`; + + // Extract AC problem IDs + const scriptContent = parsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < scriptContent.length; i++) { + const problemId = Number(scriptContent[i].substring(2, scriptContent[i].indexOf(","))); + if (!isNaN(problemId)) { + acProblems.push(problemId); + } + } + + improveACRateButton.disabled = false; + }); + + // Add click handler + improveACRateButton.addEventListener("click", async () => { + improveACRateButton.disabled = true; + const submitTimes = 3; + let count = 0; + + const submitInterval = setInterval(async () => { + if (count >= submitTimes) { + clearInterval(submitInterval); + location.reload(); + return; + } + + improveACRateButton.innerText = `正在提交 (${count + 1}/${submitTimes})`; + + // Randomly select an AC'd problem + const pid = acProblems[Math.floor(Math.random() * acProblems.length)]; + + // Get a solution ID for this problem + let sid = 0; + await fetch(`https://www.xmoj.tech/status.php?problem_id=${pid}&jresult=4`) + .then((result) => result.text()) + .then((result) => { + const parsedDocument = new DOMParser().parseFromString(result, "text/html"); + sid = parsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + + // Get the source code + let code = ""; + await fetch(`https://www.xmoj.tech/getsource.php?id=${sid}`) + .then((response) => response.text()) + .then((response) => { + code = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Resubmit the code + await fetch("https://www.xmoj.tech/submit.php", { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + referrer: `https://www.xmoj.tech/submitpage.php?id=${pid}`, + method: "POST", + body: `id=${pid}&language=1&source=${encodeURIComponent(code)}&enable_O2=on` + }); + + count++; + }, 1000); + }); + + // Style the button + improveACRateButton.style.marginBottom = "7px"; + improveACRateButton.style.marginRight = "7px"; + } catch (error) { + console.error('[ImproveACRate] Error initializing button:', error); + } + }, 100); + } + /** * Feature loader - Initializes all extracted feature modules * @@ -7102,33 +7268,37 @@ int main() async function initializeFeatures(context) { try { // Initialize features that need to run early (before main page load) - init$h(); + init$j(); - // Initialize features that clean up/modify the page - init$d(); - init$7(); + // Initialize theme (must run early) + init$1(); - // Initialize cosmetic/styling features - init$a(); + // Initialize features that clean up/modify the page + init$f(); init$9(); - // Initialize text replacement features + // Initialize cosmetic/styling features init$c(); init$b(); + + // Initialize text replacement features + init$e(); + init$d(); + init$8(); init$6(); - init$4(); // Initialize utility features - init$3(); + init$5(); + init$4(); init$2(); init(); // Initialize page-specific features - init$f(); - init$8(); - init$5(); - init$1(); - await init$e(); + init$h(); + init$a(); + init$7(); + init$3(); + await init$g(); // Initialize complex features that need context if (context) ; @@ -7163,6 +7333,8 @@ int main() 'MoreSTD', 'ExportACCode', 'OpenAllProblem', + 'DarkMode', + 'ImproveACRate', ]; } diff --git a/src/features/dark-mode.js b/src/features/dark-mode.js new file mode 100644 index 00000000..3bf9e7a8 --- /dev/null +++ b/src/features/dark-mode.js @@ -0,0 +1,40 @@ +/** + * Dark Mode Feature + * Enables dark theme for the website + * Feature ID: DarkMode + * Type: A (Appearance) + * Description: 启用网站深色主题 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize DarkMode feature + * Sets the Bootstrap theme to dark or light based on user preference + * + * Note: This feature also affects other parts of the application: + * - CodeMirror theme selection + * - Contest rank table text colors + * - Problem switcher background + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 247-251: Theme attribute setting + * - Used throughout the codebase for conditional styling (17 occurrences) + */ +export function init() { + // Set theme based on DarkMode setting + if (UtilityEnabled("DarkMode")) { + document.querySelector("html").setAttribute("data-bs-theme", "dark"); + } else { + document.querySelector("html").setAttribute("data-bs-theme", "light"); + } +} + +/** + * Check if dark mode is currently enabled + * Used by other features for conditional styling + * @returns {boolean} + */ +export function isDarkMode() { + return UtilityEnabled("DarkMode"); +} diff --git a/src/features/improve-ac-rate.js b/src/features/improve-ac-rate.js new file mode 100644 index 00000000..a3f02ffc --- /dev/null +++ b/src/features/improve-ac-rate.js @@ -0,0 +1,135 @@ +/** + * Improve AC Rate Feature + * Adds a button to resubmit already-AC'd problems to improve submission statistics + * Feature ID: ImproveACRate + * Type: U (Utility) + * Description: 添加按钮来重新提交已AC的题目以提高正确率 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ImproveACRate feature + * Adds a "提高正确率" button that resubmits already-AC'd problems + * + * This feature: + * 1. Fetches user's AC problems from userinfo page + * 2. Displays current AC rate percentage + * 3. On click, randomly selects 3 AC'd problems and resubmits them + * 4. Uses existing AC code from status page + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1405-1463: Improve AC rate button and logic + */ +export function init() { + // Only execute if ImproveACRate feature is enabled + if (!UtilityEnabled("ImproveACRate")) { + return; + } + + // Only execute on status page + if (location.pathname !== "/status.php") { + return; + } + + // Need current username + const currentUsername = document.querySelector("#profile")?.innerText; + if (!currentUsername || currentUsername === "登录") { + return; + } + + // Wait for page to be ready + setTimeout(async () => { + try { + const container = document.querySelector("body > div.container > div > div.input-append"); + if (!container) return; + + // Create improve AC rate button + const improveACRateButton = document.createElement("button"); + container.appendChild(improveACRateButton); + improveACRateButton.className = "btn btn-outline-secondary"; + improveACRateButton.innerText = "提高正确率"; + improveACRateButton.disabled = true; + + // Fetch user's AC problems + let acProblems = []; + await fetch(`https://www.xmoj.tech/userinfo.php?user=${currentUsername}`) + .then((response) => response.text()) + .then((response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + + // Calculate and display AC rate + const acCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText); + const submitCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText); + const acRate = (acCount / submitCount * 100).toFixed(2); + improveACRateButton.innerText += ` (${acRate}%)`; + + // Extract AC problem IDs + const scriptContent = parsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < scriptContent.length; i++) { + const problemId = Number(scriptContent[i].substring(2, scriptContent[i].indexOf(","))); + if (!isNaN(problemId)) { + acProblems.push(problemId); + } + } + + improveACRateButton.disabled = false; + }); + + // Add click handler + improveACRateButton.addEventListener("click", async () => { + improveACRateButton.disabled = true; + const submitTimes = 3; + let count = 0; + + const submitInterval = setInterval(async () => { + if (count >= submitTimes) { + clearInterval(submitInterval); + location.reload(); + return; + } + + improveACRateButton.innerText = `正在提交 (${count + 1}/${submitTimes})`; + + // Randomly select an AC'd problem + const pid = acProblems[Math.floor(Math.random() * acProblems.length)]; + + // Get a solution ID for this problem + let sid = 0; + await fetch(`https://www.xmoj.tech/status.php?problem_id=${pid}&jresult=4`) + .then((result) => result.text()) + .then((result) => { + const parsedDocument = new DOMParser().parseFromString(result, "text/html"); + sid = parsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + + // Get the source code + let code = ""; + await fetch(`https://www.xmoj.tech/getsource.php?id=${sid}`) + .then((response) => response.text()) + .then((response) => { + code = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Resubmit the code + await fetch("https://www.xmoj.tech/submit.php", { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + referrer: `https://www.xmoj.tech/submitpage.php?id=${pid}`, + method: "POST", + body: `id=${pid}&language=1&source=${encodeURIComponent(code)}&enable_O2=on` + }); + + count++; + }, 1000); + }); + + // Style the button + improveACRateButton.style.marginBottom = "7px"; + improveACRateButton.style.marginRight = "7px"; + } catch (error) { + console.error('[ImproveACRate] Error initializing button:', error); + } + }, 100); +} diff --git a/src/features/index.js b/src/features/index.js index 5d21e96b..5d8e6224 100644 --- a/src/features/index.js +++ b/src/features/index.js @@ -24,6 +24,8 @@ import { init as initAutoCountdown } from './auto-countdown.js'; import { init as initMoreSTD } from './more-std.js'; import { init as initExportACCode } from './export-ac-code.js'; import { init as initOpenAllProblem } from './open-all-problem.js'; +import { init as initDarkMode } from './dark-mode.js'; +import { init as initImproveACRate } from './improve-ac-rate.js'; /** * Initialize all feature modules @@ -44,6 +46,9 @@ export async function initializeFeatures(context) { // Initialize features that need to run early (before main page load) initAutoLogin(); + // Initialize theme (must run early) + initDarkMode(); + // Initialize features that clean up/modify the page initRemoveUseless(); initRemoveAlerts(); @@ -62,6 +67,7 @@ export async function initializeFeatures(context) { initAutoCountdown(); initMoreSTD(); initOpenAllProblem(); + initImproveACRate(); // Initialize page-specific features initCopySamples(); @@ -105,5 +111,7 @@ export function getExtractedFeatures() { 'MoreSTD', 'ExportACCode', 'OpenAllProblem', + 'DarkMode', + 'ImproveACRate', ]; } From f2622b98e7921255d7adf483deeb4ece87ca3c33 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Nov 2025 07:07:19 +0000 Subject: [PATCH 10/26] Add page-specific module system to organize styling by page This commit introduces a new page module system to better organize page-specific styling and functionality that was scattered throughout bootstrap.js. This significantly improves code maintainability by separating concerns. New page modules created: - problem.js: Handles /problem.php page - Submit button fixes (dynamic selector handling) - Sample data card styling - IO file information extraction - Discussion button with unread badge - Custom page styling for code blocks - contest.js: Handles /contest.php page - Contest list view with countdown timers - Running/upcoming/finished contest states - Contest view with problem list formatting - Time display formatting - status.js: Handles /status.php page - Page title and cleanup - Foundation for future status page styling - submit.js: Handles /submitpage.php page - Page title based on problem/contest - Foundation for future submit page styling Page loader system: - src/pages/index.js: Centralized page loader - Routes pages based on location.pathname - Passes context object with utilities to each page - Integrates with main.js via window.load event - Runs after bootstrap.js setup but provides cleaner organization Benefits: - Separates page-specific code from features - Makes it easier to find and modify page styling - Reduces bootstrap.js size over time - Provides clear structure for adding more pages - Better code organization and maintainability Updated: - src/main.js: Added page loader integration - README_REFACTORING.md: Documented page module system - dist/XMOJ.user.js: Rebuilt with page modules (524KB, 7901 lines) Next steps: - Continue extracting more page modules (login, userinfo, etc.) - Move more styling from bootstrap.js to page modules - Further reduce bootstrap.js size --- README_REFACTORING.md | 11 + dist/XMOJ.user.js | 587 +++++++++++++++++++++++++++++++++++++++--- src/main.js | 21 ++ src/pages/contest.js | 189 ++++++++++++++ src/pages/index.js | 48 ++++ src/pages/problem.js | 205 +++++++++++++++ src/pages/status.js | 29 +++ src/pages/submit.js | 26 ++ 8 files changed, 1079 insertions(+), 37 deletions(-) create mode 100644 src/pages/contest.js create mode 100644 src/pages/index.js create mode 100644 src/pages/problem.js create mode 100644 src/pages/status.js create mode 100644 src/pages/submit.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md index b9a38009..bc499a63 100644 --- a/README_REFACTORING.md +++ b/README_REFACTORING.md @@ -93,6 +93,17 @@ The userscript includes the following features (controlled via `UtilityEnabled`) - Translate - Translation features - UploadStd - Upload standard solutions +## Page Modules + +Page-specific styling and functionality has been extracted into separate modules under `src/pages/`: + +- **problem.js** - `/problem.php` - Problem view page with submit button fixes, sample data styling, discussion button +- **contest.js** - `/contest.php` - Contest list and contest view with countdown timers, problem list formatting +- **status.js** - `/status.php` - Submission status page +- **submit.js** - `/submitpage.php` - Code submission page + +Page modules are loaded automatically based on the current pathname and handle page-specific DOM manipulations that don't belong to any particular feature. + ## Extracted Features The following features have been extracted into separate modules under `src/features/`: diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js index 24b0be54..5e1bde0f 100644 --- a/dist/XMOJ.user.js +++ b/dist/XMOJ.user.js @@ -5094,7 +5094,7 @@ int main() * Initialize auto login feature * Checks if user is logged in and redirects to login page if necessary */ - function init$j() { + function init$n() { // Only execute if AutoLogin feature is enabled if (!UtilityEnabled("AutoLogin")) { return; @@ -5154,7 +5154,7 @@ int main() * @param {Function} context.PurifyHTML - Function to sanitize HTML content * @param {Function} context.RenderMathJax - Function to render math formulas */ - function init$i(context) { + function init$m(context) { // Only execute if Discussion feature is enabled if (!UtilityEnabled("Discussion")) { return; @@ -6037,7 +6037,7 @@ int main() * * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 */ - function init$h() { + function init$l() { // Only execute if CopySamples feature is enabled if (!UtilityEnabled("CopySamples")) { return; @@ -6084,7 +6084,7 @@ int main() * - Adds a "Compare Submissions" button on problem pages * - Creates comparison interface on comparesource.php page */ - async function init$g() { + async function init$k() { // Only execute if CompareSource feature is enabled if (!UtilityEnabled("CompareSource")) { return; @@ -6266,7 +6266,7 @@ int main() * - Line 2500-2505: Remove submission child nodes on userinfo page * - Line 3209-3211: Remove h2.lang_en on problem_solution page */ - function init$f() { + function init$j() { // Only execute if RemoveUseless feature is enabled if (!UtilityEnabled("RemoveUseless")) { return; @@ -6344,7 +6344,7 @@ int main() * - Lines 219-222: Text replacement * - Line 304: Navbar brand text */ - function init$e() { + function init$i() { // Only execute if ReplaceXM feature is enabled if (!UtilityEnabled("ReplaceXM")) { return; @@ -6375,7 +6375,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 404-417: Status text replacement */ - function init$d() { + function init$h() { // Only execute if ReplaceYN feature is enabled if (!UtilityEnabled("ReplaceYN")) { return; @@ -6416,7 +6416,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 381-384: Animation CSS */ - function init$c() { + function init$g() { // Only execute if AddAnimation feature is enabled if (!UtilityEnabled("AddAnimation")) { return; @@ -6446,7 +6446,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 386-395: Color text CSS */ - function init$b() { + function init$f() { // Only execute if AddColorText feature is enabled if (!UtilityEnabled("AddColorText")) { return; @@ -6488,7 +6488,7 @@ int main() * - Lines 2850-2852: Clear credentials on failure * - Lines 2867-2876: Auto-fill and auto-submit login form */ - function init$a() { + function init$e() { // Only execute on login page if (location.pathname !== "/loginpage.php") { return; @@ -6539,7 +6539,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1666-1667: Modify contest start link */ - function init$9() { + function init$d() { // Only execute if RemoveAlerts feature is enabled if (!UtilityEnabled("RemoveAlerts")) { return; @@ -6584,7 +6584,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 216-218: Link to button replacement */ - function init$8() { + function init$c() { // Only execute if ReplaceLinks feature is enabled if (!UtilityEnabled("ReplaceLinks")) { return; @@ -6613,7 +6613,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 2020-2022: Auto-check O2 flag */ - function init$7() { + function init$b() { // Only execute if AutoO2 feature is enabled if (!UtilityEnabled("AutoO2")) { return; @@ -6656,7 +6656,7 @@ int main() * - Lines 1073-1078: Problemset page translations * - Lines 1611-1617: Contest page translations */ - function init$6() { + function init$a() { // Only execute if Translate feature is enabled if (!UtilityEnabled("Translate")) { return; @@ -6757,7 +6757,7 @@ int main() * - Lines 547-566: Countdown timer update logic * - Lines 1592-1594: Disables default clock on contest page */ - function init$5() { + function init$9() { // Only execute if AutoCountdown feature is enabled if (!UtilityEnabled("AutoCountdown")) { return; @@ -6832,7 +6832,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1699-1717: Standard solution column management */ - function init$4() { + function init$8() { // Only execute if MoreSTD feature is enabled if (!UtilityEnabled("MoreSTD")) { return; @@ -6914,7 +6914,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 2445-2495: Export AC code button and logic */ - function init$3() { + function init$7() { // Only execute if ExportACCode feature is enabled if (!UtilityEnabled("ExportACCode")) { return; @@ -7016,7 +7016,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1817-1841: Open all problem buttons */ - function init$2() { + function init$6() { // Only execute if OpenAllProblem feature is enabled if (!UtilityEnabled("OpenAllProblem")) { return; @@ -7098,7 +7098,7 @@ int main() * - Lines 247-251: Theme attribute setting * - Used throughout the codebase for conditional styling (17 occurrences) */ - function init$1() { + function init$5() { // Set theme based on DarkMode setting if (UtilityEnabled("DarkMode")) { document.querySelector("html").setAttribute("data-bs-theme", "dark"); @@ -7129,7 +7129,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1405-1463: Improve AC rate button and logic */ - function init() { + function init$4() { // Only execute if ImproveACRate feature is enabled if (!UtilityEnabled("ImproveACRate")) { return; @@ -7268,37 +7268,37 @@ int main() async function initializeFeatures(context) { try { // Initialize features that need to run early (before main page load) - init$j(); + init$n(); // Initialize theme (must run early) - init$1(); + init$5(); // Initialize features that clean up/modify the page - init$f(); - init$9(); + init$j(); + init$d(); // Initialize cosmetic/styling features - init$c(); - init$b(); + init$g(); + init$f(); // Initialize text replacement features - init$e(); - init$d(); - init$8(); - init$6(); + init$i(); + init$h(); + init$c(); + init$a(); // Initialize utility features - init$5(); + init$9(); + init$8(); + init$6(); init$4(); - init$2(); - init(); // Initialize page-specific features - init$h(); - init$a(); + init$l(); + init$e(); + init$b(); init$7(); - init$3(); - await init$g(); + await init$k(); // Initialize complex features that need context if (context) ; @@ -7338,6 +7338,501 @@ int main() ]; } + /** + * Problem Page Module + * Handles all styling and functionality for /problem.php + */ + + + /** + * Initialize problem page + * @param {Object} context - Page context with utilities + */ + async function init$3(context) { + const { SearchParams, RenderMathJax, RequestAPI, Style } = context; + + // Render MathJax + await RenderMathJax(); + + // Check if problem doesn't exist + if (document.querySelector("body > div > div.mt-3 > h2") != null) { + document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; + setTimeout(() => { + location.href = "https://www.xmoj.tech/problemset.php"; + }, 1000); + return; + } + + const PID = SearchParams.get("cid") + ? localStorage.getItem(`UserScript-Contest-${SearchParams.get("cid")}-Problem-${SearchParams.get("pid")}-PID`) + : SearchParams.get("id"); + + // Fix spacing + if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { + document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; + } + + // Fix submit button + fixSubmitButton(); + + // Style sample data cards + const sampleDataElements = document.querySelectorAll(".sampledata"); + for (let i = 0; i < sampleDataElements.length; i++) { + sampleDataElements[i].parentElement.className = "card"; + } + + // Handle IO file information + handleIOFile(PID); + + // Add discussion button (if Discussion feature is enabled) + if (UtilityEnabled("Discussion")) { + addDiscussionButton(PID, SearchParams, RequestAPI); + } + + // Tidy tables + const tables = document.getElementsByTagName("table"); + for (let i = 0; i < tables.length; i++) { + TidyTable(tables[i]); + } + + // Add custom styles + addPageStyles(Style); + } + + /** + * Fix submit button styling and behavior + */ + function fixSubmitButton() { + // Try multiple selectors to find the submit link (it keeps moving position) + const selectors = [ + '.mt-3 > center:nth-child(1) > a:nth-child(12)', + '.mt-3 > center:nth-child(1) > a:nth-child(10)', + '.mt-3 > center:nth-child(1) > a:nth-child(11)', + '.mt-3 > center:nth-child(1) > a:nth-child(13)', + '.mt-3 > center:nth-child(1) > a:nth-child(9)', + '.mt-3 > center:nth-child(1) > a:nth-child(7)', + '.mt-3 > center:nth-child(1) > a:nth-child(8)', + ]; + + let submitLink = null; + for (const selector of selectors) { + submitLink = document.querySelector(selector); + if (submitLink) break; + } + + if (!submitLink) return; + + // Create submit button + const submitButton = document.createElement('button'); + submitButton.id = 'SubmitButton'; + submitButton.className = 'btn btn-outline-secondary'; + submitButton.textContent = '提交'; + submitButton.onclick = function () { + window.location.href = submitLink.href; + }; + + // Replace the link with the button + submitLink.parentNode.replaceChild(submitButton, submitLink); + + // Remove the button's outer brackets + const container = document.querySelector('.mt-3 > center:nth-child(1)'); + if (container) { + let str = container.innerHTML; + let target = submitButton.outerHTML; + let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); + container.innerHTML = result; + + // Re-attach click handler after innerHTML replacement + const newButton = document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary'); + if (newButton) { + newButton.onclick = function () { + window.location.href = submitLink.href; + }; + } + } + } + + /** + * Handle IO file information display + * @param {string} PID - Problem ID + */ + function handleIOFile(PID) { + const ioFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (!ioFileElement) return; + + // Move child nodes out of h3 + while (ioFileElement.childNodes.length >= 1) { + ioFileElement.parentNode.insertBefore(ioFileElement.childNodes[0], ioFileElement); + } + ioFileElement.parentNode.insertBefore(document.createElement("br"), ioFileElement); + ioFileElement.remove(); + + // Extract and store IO filename + const centerNode = document.querySelector("body > div > div.mt-3 > center"); + if (centerNode && centerNode.childNodes[2]) { + const temp = centerNode.childNodes[2].data.trim(); + const ioFilename = temp.substring(0, temp.length - 3); + localStorage.setItem(`UserScript-Problem-${PID}-IOFilename`, ioFilename); + } + } + + /** + * Add discussion button with unread badge + * @param {string} PID - Problem ID + * @param {URLSearchParams} SearchParams - URL search parameters + * @param {Function} RequestAPI - API request function + */ + function addDiscussionButton(PID, SearchParams, RequestAPI) { + const discussButton = document.createElement("button"); + discussButton.className = "btn btn-outline-secondary position-relative"; + discussButton.innerHTML = `讨论`; + discussButton.style.marginLeft = "10px"; + discussButton.type = "button"; + + discussButton.addEventListener("click", () => { + const problemId = SearchParams.get("cid") ? PID : SearchParams.get("id"); + open(`https://www.xmoj.tech/discuss3/discuss.php?pid=${problemId}`, "_blank"); + }); + + document.querySelector("body > div > div.mt-3 > center").appendChild(discussButton); + + // Add unread badge + const unreadBadge = document.createElement("span"); + unreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + unreadBadge.style.display = "none"; + discussButton.appendChild(unreadBadge); + + // Refresh unread count + const refreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (response) => { + if (response.Success && response.Data.DiscussCount != 0) { + unreadBadge.innerText = response.Data.DiscussCount; + unreadBadge.style.display = ""; + } + }); + }; + + refreshCount(); + addEventListener("focus", refreshCount); + } + + /** + * Add custom page styles + * @param {HTMLStyleElement} Style - Style element to append to + */ + function addPageStyles(Style) { + Style.innerHTML += ` +code, kbd, pre, samp { + font-family: monospace, Consolas, 'Courier New'; + font-size: 1rem; +} +pre { + padding: 0.3em 0.5em; + margin: 0.5em 0; +} +.in-out { + overflow: hidden; + display: flex; + padding: 0.5em 0; +} +.in-out .in-out-item { + flex: 1; +}`; + } + + /** + * Contest Page Module + * Handles all styling and functionality for /contest.php + */ + + + /** + * Initialize contest page + * @param {Object} context - Page context with utilities + */ + async function init$2(context) { + const { SearchParams } = context; + + // Check if viewing specific contest or contest list + if (location.href.indexOf("?cid=") === -1) { + // Contest list page + initContestList(); + } else { + // Specific contest page + initContestView(SearchParams); + } + } + + /** + * Initialize contest list view + */ + function initContestList() { + // Style contest list table rows + const contestRows = document.querySelector("body > div > div.mt-3 > center > table > tbody")?.childNodes; + if (!contestRows) return; + + for (let i = 1; i < contestRows.length; i++) { + const currentElement = contestRows[i].childNodes[2]?.childNodes; + if (!currentElement) continue; + + // Handle different contest states + if (currentElement[1]?.childNodes[0]?.data?.indexOf("运行中") !== -1) { + handleRunningContest(currentElement); + } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("开始于") !== -1) { + handleUpcomingContest(currentElement); + } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("已结束") !== -1) { + handleFinishedContest(currentElement); + } + + // Hide column and add user link + contestRows[i].childNodes[3].style.display = "none"; + const creator = contestRows[i].childNodes[4].innerHTML; + contestRows[i].childNodes[4].innerHTML = `${creator}`; + + // Store contest name + const contestId = contestRows[i].childNodes[0].innerText; + const contestName = contestRows[i].childNodes[1].innerText; + localStorage.setItem(`UserScript-Contest-${contestId}-Name`, contestName); + } + } + + /** + * Handle running contest countdown + * @param {NodeList} element - Contest row element + */ + function handleRunningContest(element) { + const time = String(element[1].childNodes[1].innerText).substring(4); + + // Parse time components + const day = parseInt(time.substring(0, time.indexOf("天"))) || 0; + const hourStart = time.indexOf("天") === -1 ? 0 : time.indexOf("天") + 1; + const hour = parseInt(time.substring(hourStart, time.indexOf("小时"))) || 0; + const minuteStart = time.indexOf("小时") === -1 ? 0 : time.indexOf("小时") + 2; + const minute = parseInt(time.substring(minuteStart, time.indexOf("分"))) || 0; + const secondStart = time.indexOf("分") === -1 ? 0 : time.indexOf("分") + 1; + const second = parseInt(time.substring(secondStart, time.indexOf("秒"))) || 0; + + // Calculate timestamp + const diff = window.diff || 0; // Global time diff + const timeStamp = new Date().getTime() + diff + ((((day * 24 + hour) * 60 + minute) * 60 + second) * 1000); + + element[1].childNodes[1].setAttribute("EndTime", timeStamp); + element[1].childNodes[1].classList.add("UpdateByJS"); + } + + /** + * Handle upcoming contest + * @param {NodeList} element - Contest row element + */ + function handleUpcomingContest(element) { + const diff = window.diff || 0; + const timeStamp = Date.parse(String(element[1].childNodes[0].data).substring(4)) + diff; + element[1].setAttribute("EndTime", timeStamp); + element[1].classList.add("UpdateByJS"); + } + + /** + * Handle finished contest + * @param {NodeList} element - Contest row element + */ + function handleFinishedContest(element) { + const timeStamp = String(element[1].childNodes[0].data).substring(4); + element[1].childNodes[0].data = " 已结束 "; + element[1].className = "red"; + + const span = document.createElement("span"); + span.className = "green"; + span.innerHTML = timeStamp; + element[1].appendChild(span); + } + + /** + * Initialize specific contest view + * @param {URLSearchParams} SearchParams - URL search parameters + */ + function initContestView(SearchParams) { + // Update title + const title = document.getElementsByTagName("h3")[0]; + if (title) { + title.innerHTML = "比赛" + title.innerHTML.substring(7); + } + + // Handle countdown timer + const timeLeft = document.querySelector("#time_left"); + if (timeLeft) { + const centerNode = document.querySelector("body > div > div.mt-3 > center"); + let endTimeText = centerNode?.childNodes[3]?.data; + + if (endTimeText) { + endTimeText = endTimeText.substring(endTimeText.indexOf("结束时间是:") + 6, endTimeText.lastIndexOf("。")); + const endTime = new Date(endTimeText).getTime(); + + if (new Date().getTime() < endTime) { + timeLeft.classList.add("UpdateByJS"); + timeLeft.setAttribute("EndTime", endTime); + } + } + } + + // Format contest information + const infoDiv = document.querySelector("body > div > div.mt-3 > center > div"); + if (infoDiv) { + let htmlData = infoDiv.innerHTML; + htmlData = htmlData.replaceAll("  \n  ", " "); + htmlData = htmlData.replaceAll("
    开始于: ", "开始时间:"); + htmlData = htmlData.replaceAll("\n结束于: ", "
    结束时间:"); + htmlData = htmlData.replaceAll("\n订正截止日期: ", "
    订正截止日期:"); + htmlData = htmlData.replaceAll("\n现在时间: ", "当前时间:"); + htmlData = htmlData.replaceAll("\n状态:", "
    状态:"); + infoDiv.innerHTML = htmlData; + } + + // Format problem list + formatProblemList(); + + // Store problem count + const problemCount = document.querySelector("#problemset > tbody")?.rows.length; + if (problemCount) { + localStorage.setItem(`UserScript-Contest-${SearchParams.get("cid")}-ProblemCount`, problemCount); + } + } + + /** + * Format problem list in contest + */ + function formatProblemList() { + const tbody = document.querySelector("#problemset > tbody"); + if (!tbody) return; + + // Format problem names + tbody.innerHTML = tbody.innerHTML.replaceAll( + /\t ([0-9]*)      问题  ([^<]*)/g, + "$2. $1" + ); + tbody.innerHTML = tbody.innerHTML.replaceAll( + /\t\*([0-9]*)      问题  ([^<]*)/g, + "拓展$2. $1" + ); + + // Ensure status divs exist + const rows = tbody.rows; + for (let i = 0; i < rows.length; i++) { + if (rows[i].childNodes[0]?.children.length === 0) { + rows[i].childNodes[0].innerHTML = '
    '; + } + + // Make problem title link open in new tab + const titleLink = rows[i].children[2]?.children[0]; + if (titleLink) { + titleLink.target = "_blank"; + } + } + } + + /** + * Status Page Module + * Handles all styling and functionality for /status.php + */ + + /** + * Initialize status page + * @param {Object} context - Page context with utilities + */ + async function init$1(context) { + const { SearchParams } = context; + + // Only proceed if not in special UserScript mode + if (SearchParams.get("ByUserScript") !== null) { + return; + } + + // Set page title + document.title = "提交状态"; + + // Remove old script tags + const oldScript = document.querySelector("body > script:nth-child(5)"); + if (oldScript) { + oldScript.remove(); + } + + // Additional status page initialization can go here + // Most status page features are handled by feature modules + } + + /** + * Submit Page Module + * Handles all styling and functionality for /submitpage.php + */ + + /** + * Initialize submit page + * @param {Object} context - Page context with utilities + */ + async function init(context) { + const { SearchParams } = context; + + // Set page title + const problemId = SearchParams.get("id"); + const contestId = SearchParams.get("cid"); + + if (problemId) { + document.title = `提交代码: 题目${Number(problemId)}`; + } else if (contestId) { + String.fromCharCode(65 + parseInt(SearchParams.get("pid"))); + document.title = `提交代码: 比赛${Number(contestId)}`; + } + + // Additional submit page initialization can go here + // Most submit page features are handled by feature modules and CodeMirror initialization + } + + /** + * Page loader - Initializes page-specific modules based on current URL + * + * This module provides a centralized way to load page-specific styling and functionality. + * Each page module handles its own initialization and styling. + */ + + + /** + * Page route mapping + */ + const PAGE_ROUTES = { + '/problem.php': init$3, + '/contest.php': init$2, + '/status.php': init$1, + '/submitpage.php': init, + }; + + /** + * Initialize page-specific module based on current pathname + * @param {Object} context - Shared context object with dependencies + */ + async function initializePage(context) { + const pathname = location.pathname; + + const pageInit = PAGE_ROUTES[pathname]; + + if (pageInit) { + try { + await pageInit(context); + console.log(`[XMOJ-Script] Initialized page module: ${pathname}`); + } catch (error) { + console.error(`[XMOJ-Script] Error initializing page ${pathname}:`, error); + } + } + } + + /** + * Get list of all implemented page modules + * @returns {string[]} List of page pathnames with modules + */ + function getImplementedPages() { + return Object.keys(PAGE_ROUTES); + } + /** * Main entry point for XMOJ Script * This file imports all utilities and features, then initializes the application @@ -7385,4 +7880,22 @@ int main() // in a more maintainable way main(); + // Initialize page-specific modules after main() runs + // Page modules handle page-specific styling and DOM manipulations + // This needs to run after bootstrap.js sets up the basic structure + window.addEventListener('load', () => { + // Create context object with commonly used utilities + const pageContext = { + SearchParams: new URLSearchParams(location.search), + RenderMathJax: RenderMathJax$1, + RequestAPI, + TidyTable, + Style: document.querySelector('style#UserScript-Style'), // Assuming bootstrap creates this + }; + + initializePage(pageContext).then(() => { + console.log('[XMOJ-Script] Page modules available for:', getImplementedPages()); + }); + }); + })(); diff --git a/src/main.js b/src/main.js index 78010cc8..aa04ffb5 100644 --- a/src/main.js +++ b/src/main.js @@ -26,6 +26,9 @@ import { registerMenuCommands } from './core/menu.js'; // Feature modules imports import { initializeFeatures, getExtractedFeatures } from './features/index.js'; +// Page modules imports +import { initializePage, getImplementedPages } from './pages/index.js'; + // Make utilities globally available (for compatibility with inline code) window.escapeHTML = escapeHTML; window.PurifyHTML = PurifyHTML; @@ -66,3 +69,21 @@ initializeFeatures().then(() => { // Extracted features in src/features/ provide the same functionality // in a more maintainable way main(); + +// Initialize page-specific modules after main() runs +// Page modules handle page-specific styling and DOM manipulations +// This needs to run after bootstrap.js sets up the basic structure +window.addEventListener('load', () => { + // Create context object with commonly used utilities + const pageContext = { + SearchParams: new URLSearchParams(location.search), + RenderMathJax, + RequestAPI, + TidyTable, + Style: document.querySelector('style#UserScript-Style'), // Assuming bootstrap creates this + }; + + initializePage(pageContext).then(() => { + console.log('[XMOJ-Script] Page modules available for:', getImplementedPages()); + }); +}); diff --git a/src/pages/contest.js b/src/pages/contest.js new file mode 100644 index 00000000..8c3ae56b --- /dev/null +++ b/src/pages/contest.js @@ -0,0 +1,189 @@ +/** + * Contest Page Module + * Handles all styling and functionality for /contest.php + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize contest page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Check if viewing specific contest or contest list + if (location.href.indexOf("?cid=") === -1) { + // Contest list page + initContestList(); + } else { + // Specific contest page + initContestView(SearchParams); + } +} + +/** + * Initialize contest list view + */ +function initContestList() { + // Style contest list table rows + const contestRows = document.querySelector("body > div > div.mt-3 > center > table > tbody")?.childNodes; + if (!contestRows) return; + + for (let i = 1; i < contestRows.length; i++) { + const currentElement = contestRows[i].childNodes[2]?.childNodes; + if (!currentElement) continue; + + // Handle different contest states + if (currentElement[1]?.childNodes[0]?.data?.indexOf("运行中") !== -1) { + handleRunningContest(currentElement); + } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("开始于") !== -1) { + handleUpcomingContest(currentElement); + } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("已结束") !== -1) { + handleFinishedContest(currentElement); + } + + // Hide column and add user link + contestRows[i].childNodes[3].style.display = "none"; + const creator = contestRows[i].childNodes[4].innerHTML; + contestRows[i].childNodes[4].innerHTML = `${creator}`; + + // Store contest name + const contestId = contestRows[i].childNodes[0].innerText; + const contestName = contestRows[i].childNodes[1].innerText; + localStorage.setItem(`UserScript-Contest-${contestId}-Name`, contestName); + } +} + +/** + * Handle running contest countdown + * @param {NodeList} element - Contest row element + */ +function handleRunningContest(element) { + const time = String(element[1].childNodes[1].innerText).substring(4); + + // Parse time components + const day = parseInt(time.substring(0, time.indexOf("天"))) || 0; + const hourStart = time.indexOf("天") === -1 ? 0 : time.indexOf("天") + 1; + const hour = parseInt(time.substring(hourStart, time.indexOf("小时"))) || 0; + const minuteStart = time.indexOf("小时") === -1 ? 0 : time.indexOf("小时") + 2; + const minute = parseInt(time.substring(minuteStart, time.indexOf("分"))) || 0; + const secondStart = time.indexOf("分") === -1 ? 0 : time.indexOf("分") + 1; + const second = parseInt(time.substring(secondStart, time.indexOf("秒"))) || 0; + + // Calculate timestamp + const diff = window.diff || 0; // Global time diff + const timeStamp = new Date().getTime() + diff + ((((day * 24 + hour) * 60 + minute) * 60 + second) * 1000); + + element[1].childNodes[1].setAttribute("EndTime", timeStamp); + element[1].childNodes[1].classList.add("UpdateByJS"); +} + +/** + * Handle upcoming contest + * @param {NodeList} element - Contest row element + */ +function handleUpcomingContest(element) { + const diff = window.diff || 0; + const timeStamp = Date.parse(String(element[1].childNodes[0].data).substring(4)) + diff; + element[1].setAttribute("EndTime", timeStamp); + element[1].classList.add("UpdateByJS"); +} + +/** + * Handle finished contest + * @param {NodeList} element - Contest row element + */ +function handleFinishedContest(element) { + const timeStamp = String(element[1].childNodes[0].data).substring(4); + element[1].childNodes[0].data = " 已结束 "; + element[1].className = "red"; + + const span = document.createElement("span"); + span.className = "green"; + span.innerHTML = timeStamp; + element[1].appendChild(span); +} + +/** + * Initialize specific contest view + * @param {URLSearchParams} SearchParams - URL search parameters + */ +function initContestView(SearchParams) { + // Update title + const title = document.getElementsByTagName("h3")[0]; + if (title) { + title.innerHTML = "比赛" + title.innerHTML.substring(7); + } + + // Handle countdown timer + const timeLeft = document.querySelector("#time_left"); + if (timeLeft) { + const centerNode = document.querySelector("body > div > div.mt-3 > center"); + let endTimeText = centerNode?.childNodes[3]?.data; + + if (endTimeText) { + endTimeText = endTimeText.substring(endTimeText.indexOf("结束时间是:") + 6, endTimeText.lastIndexOf("。")); + const endTime = new Date(endTimeText).getTime(); + + if (new Date().getTime() < endTime) { + timeLeft.classList.add("UpdateByJS"); + timeLeft.setAttribute("EndTime", endTime); + } + } + } + + // Format contest information + const infoDiv = document.querySelector("body > div > div.mt-3 > center > div"); + if (infoDiv) { + let htmlData = infoDiv.innerHTML; + htmlData = htmlData.replaceAll("  \n  ", " "); + htmlData = htmlData.replaceAll("
    开始于: ", "开始时间:"); + htmlData = htmlData.replaceAll("\n结束于: ", "
    结束时间:"); + htmlData = htmlData.replaceAll("\n订正截止日期: ", "
    订正截止日期:"); + htmlData = htmlData.replaceAll("\n现在时间: ", "当前时间:"); + htmlData = htmlData.replaceAll("\n状态:", "
    状态:"); + infoDiv.innerHTML = htmlData; + } + + // Format problem list + formatProblemList(); + + // Store problem count + const problemCount = document.querySelector("#problemset > tbody")?.rows.length; + if (problemCount) { + localStorage.setItem(`UserScript-Contest-${SearchParams.get("cid")}-ProblemCount`, problemCount); + } +} + +/** + * Format problem list in contest + */ +function formatProblemList() { + const tbody = document.querySelector("#problemset > tbody"); + if (!tbody) return; + + // Format problem names + tbody.innerHTML = tbody.innerHTML.replaceAll( + /\t ([0-9]*)      问题  ([^<]*)/g, + "$2. $1" + ); + tbody.innerHTML = tbody.innerHTML.replaceAll( + /\t\*([0-9]*)      问题  ([^<]*)/g, + "拓展$2. $1" + ); + + // Ensure status divs exist + const rows = tbody.rows; + for (let i = 0; i < rows.length; i++) { + if (rows[i].childNodes[0]?.children.length === 0) { + rows[i].childNodes[0].innerHTML = '
    '; + } + + // Make problem title link open in new tab + const titleLink = rows[i].children[2]?.children[0]; + if (titleLink) { + titleLink.target = "_blank"; + } + } +} diff --git a/src/pages/index.js b/src/pages/index.js new file mode 100644 index 00000000..2e26382b --- /dev/null +++ b/src/pages/index.js @@ -0,0 +1,48 @@ +/** + * Page loader - Initializes page-specific modules based on current URL + * + * This module provides a centralized way to load page-specific styling and functionality. + * Each page module handles its own initialization and styling. + */ + +import { init as initProblemPage } from './problem.js'; +import { init as initContestPage } from './contest.js'; +import { init as initStatusPage } from './status.js'; +import { init as initSubmitPage } from './submit.js'; + +/** + * Page route mapping + */ +const PAGE_ROUTES = { + '/problem.php': initProblemPage, + '/contest.php': initContestPage, + '/status.php': initStatusPage, + '/submitpage.php': initSubmitPage, +}; + +/** + * Initialize page-specific module based on current pathname + * @param {Object} context - Shared context object with dependencies + */ +export async function initializePage(context) { + const pathname = location.pathname; + + const pageInit = PAGE_ROUTES[pathname]; + + if (pageInit) { + try { + await pageInit(context); + console.log(`[XMOJ-Script] Initialized page module: ${pathname}`); + } catch (error) { + console.error(`[XMOJ-Script] Error initializing page ${pathname}:`, error); + } + } +} + +/** + * Get list of all implemented page modules + * @returns {string[]} List of page pathnames with modules + */ +export function getImplementedPages() { + return Object.keys(PAGE_ROUTES); +} diff --git a/src/pages/problem.js b/src/pages/problem.js new file mode 100644 index 00000000..d20ba15a --- /dev/null +++ b/src/pages/problem.js @@ -0,0 +1,205 @@ +/** + * Problem Page Module + * Handles all styling and functionality for /problem.php + */ + +import { UtilityEnabled } from '../core/config.js'; +import { TidyTable } from '../utils/table.js'; + +/** + * Initialize problem page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams, RenderMathJax, RequestAPI, Style } = context; + + // Render MathJax + await RenderMathJax(); + + // Check if problem doesn't exist + if (document.querySelector("body > div > div.mt-3 > h2") != null) { + document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; + setTimeout(() => { + location.href = "https://www.xmoj.tech/problemset.php"; + }, 1000); + return; + } + + const PID = SearchParams.get("cid") + ? localStorage.getItem(`UserScript-Contest-${SearchParams.get("cid")}-Problem-${SearchParams.get("pid")}-PID`) + : SearchParams.get("id"); + + // Fix spacing + if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { + document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; + } + + // Fix submit button + fixSubmitButton(); + + // Style sample data cards + const sampleDataElements = document.querySelectorAll(".sampledata"); + for (let i = 0; i < sampleDataElements.length; i++) { + sampleDataElements[i].parentElement.className = "card"; + } + + // Handle IO file information + handleIOFile(PID); + + // Add discussion button (if Discussion feature is enabled) + if (UtilityEnabled("Discussion")) { + addDiscussionButton(PID, SearchParams, RequestAPI); + } + + // Tidy tables + const tables = document.getElementsByTagName("table"); + for (let i = 0; i < tables.length; i++) { + TidyTable(tables[i]); + } + + // Add custom styles + addPageStyles(Style); +} + +/** + * Fix submit button styling and behavior + */ +function fixSubmitButton() { + // Try multiple selectors to find the submit link (it keeps moving position) + const selectors = [ + '.mt-3 > center:nth-child(1) > a:nth-child(12)', + '.mt-3 > center:nth-child(1) > a:nth-child(10)', + '.mt-3 > center:nth-child(1) > a:nth-child(11)', + '.mt-3 > center:nth-child(1) > a:nth-child(13)', + '.mt-3 > center:nth-child(1) > a:nth-child(9)', + '.mt-3 > center:nth-child(1) > a:nth-child(7)', + '.mt-3 > center:nth-child(1) > a:nth-child(8)', + ]; + + let submitLink = null; + for (const selector of selectors) { + submitLink = document.querySelector(selector); + if (submitLink) break; + } + + if (!submitLink) return; + + // Create submit button + const submitButton = document.createElement('button'); + submitButton.id = 'SubmitButton'; + submitButton.className = 'btn btn-outline-secondary'; + submitButton.textContent = '提交'; + submitButton.onclick = function () { + window.location.href = submitLink.href; + }; + + // Replace the link with the button + submitLink.parentNode.replaceChild(submitButton, submitLink); + + // Remove the button's outer brackets + const container = document.querySelector('.mt-3 > center:nth-child(1)'); + if (container) { + let str = container.innerHTML; + let target = submitButton.outerHTML; + let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); + container.innerHTML = result; + + // Re-attach click handler after innerHTML replacement + const newButton = document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary'); + if (newButton) { + newButton.onclick = function () { + window.location.href = submitLink.href; + }; + } + } +} + +/** + * Handle IO file information display + * @param {string} PID - Problem ID + */ +function handleIOFile(PID) { + const ioFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (!ioFileElement) return; + + // Move child nodes out of h3 + while (ioFileElement.childNodes.length >= 1) { + ioFileElement.parentNode.insertBefore(ioFileElement.childNodes[0], ioFileElement); + } + ioFileElement.parentNode.insertBefore(document.createElement("br"), ioFileElement); + ioFileElement.remove(); + + // Extract and store IO filename + const centerNode = document.querySelector("body > div > div.mt-3 > center"); + if (centerNode && centerNode.childNodes[2]) { + const temp = centerNode.childNodes[2].data.trim(); + const ioFilename = temp.substring(0, temp.length - 3); + localStorage.setItem(`UserScript-Problem-${PID}-IOFilename`, ioFilename); + } +} + +/** + * Add discussion button with unread badge + * @param {string} PID - Problem ID + * @param {URLSearchParams} SearchParams - URL search parameters + * @param {Function} RequestAPI - API request function + */ +function addDiscussionButton(PID, SearchParams, RequestAPI) { + const discussButton = document.createElement("button"); + discussButton.className = "btn btn-outline-secondary position-relative"; + discussButton.innerHTML = `讨论`; + discussButton.style.marginLeft = "10px"; + discussButton.type = "button"; + + discussButton.addEventListener("click", () => { + const problemId = SearchParams.get("cid") ? PID : SearchParams.get("id"); + open(`https://www.xmoj.tech/discuss3/discuss.php?pid=${problemId}`, "_blank"); + }); + + document.querySelector("body > div > div.mt-3 > center").appendChild(discussButton); + + // Add unread badge + const unreadBadge = document.createElement("span"); + unreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + unreadBadge.style.display = "none"; + discussButton.appendChild(unreadBadge); + + // Refresh unread count + const refreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (response) => { + if (response.Success && response.Data.DiscussCount != 0) { + unreadBadge.innerText = response.Data.DiscussCount; + unreadBadge.style.display = ""; + } + }); + }; + + refreshCount(); + addEventListener("focus", refreshCount); +} + +/** + * Add custom page styles + * @param {HTMLStyleElement} Style - Style element to append to + */ +function addPageStyles(Style) { + Style.innerHTML += ` +code, kbd, pre, samp { + font-family: monospace, Consolas, 'Courier New'; + font-size: 1rem; +} +pre { + padding: 0.3em 0.5em; + margin: 0.5em 0; +} +.in-out { + overflow: hidden; + display: flex; + padding: 0.5em 0; +} +.in-out .in-out-item { + flex: 1; +}`; +} diff --git a/src/pages/status.js b/src/pages/status.js new file mode 100644 index 00000000..5431ad91 --- /dev/null +++ b/src/pages/status.js @@ -0,0 +1,29 @@ +/** + * Status Page Module + * Handles all styling and functionality for /status.php + */ + +/** + * Initialize status page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Only proceed if not in special UserScript mode + if (SearchParams.get("ByUserScript") !== null) { + return; + } + + // Set page title + document.title = "提交状态"; + + // Remove old script tags + const oldScript = document.querySelector("body > script:nth-child(5)"); + if (oldScript) { + oldScript.remove(); + } + + // Additional status page initialization can go here + // Most status page features are handled by feature modules +} diff --git a/src/pages/submit.js b/src/pages/submit.js new file mode 100644 index 00000000..f7c11abd --- /dev/null +++ b/src/pages/submit.js @@ -0,0 +1,26 @@ +/** + * Submit Page Module + * Handles all styling and functionality for /submitpage.php + */ + +/** + * Initialize submit page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Set page title + const problemId = SearchParams.get("id"); + const contestId = SearchParams.get("cid"); + + if (problemId) { + document.title = `提交代码: 题目${Number(problemId)}`; + } else if (contestId) { + const problemLetter = String.fromCharCode(65 + parseInt(SearchParams.get("pid"))); + document.title = `提交代码: 比赛${Number(contestId)}`; + } + + // Additional submit page initialization can go here + // Most submit page features are handled by feature modules and CodeMirror initialization +} From 2eaee856c826200a9f21013c1069d42ac4774749 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Nov 2025 07:16:10 +0000 Subject: [PATCH 11/26] Extract 5 more page modules: problemset, userinfo, login, contestrank This commit continues building out the page module system by extracting 5 additional page modules, improving code organization significantly. New page modules: - problemset.js: /problemset.php - Column width management - Improved search forms layout - Problem name caching in localStorage - userinfo.js: /userinfo.php - User profile layout with avatar (Gravatar/Cravatar) - AC problems display in grid format - Badge management for admins (add/delete) - Last online time (async loading) - Translated table headers - login.js: /loginpage.php - Bootstrap-styled login form - Foundation for login handling (LoginFailed feature) - contestrank.js: /contestrank-oi.php and /contestrank-correct.php - Shared module for both OI and correct rankings - Medal badges for top rankings - Colored cells based on AC/WA status - Error count display - Auto-refresh support for OI rankings - Dark mode support for cell colors Updated: - src/pages/index.js: Added 5 new page routes (7 pathnames total) - src/main.js: Enhanced page context with more utilities - Added GetUserInfo, GetUserBadge, GetUsernameHTML - Added GetRelativeTime, SmartAlert - Added IsAdmin flag - README_REFACTORING.md: Documented all 8 page modules - dist/XMOJ.user.js: Rebuilt (549KB, 8591 lines) Benefits: - Page-specific code is now well-organized and easy to find - Clearer separation between features and page styling - Each page module is self-contained and maintainable - Context object provides necessary utilities to pages - Bootstrap.js continues to shrink as code is extracted Total page modules: 8 (was 4) Total feature modules: 20 --- README_REFACTORING.md | 4 + dist/XMOJ.user.js | 786 ++++++++++++++++++++++++++++++++++++--- src/main.js | 8 +- src/pages/contestrank.js | 223 +++++++++++ src/pages/index.js | 9 + src/pages/login.js | 61 +++ src/pages/problemset.js | 103 +++++ src/pages/userinfo.js | 292 +++++++++++++++ 8 files changed, 1437 insertions(+), 49 deletions(-) create mode 100644 src/pages/contestrank.js create mode 100644 src/pages/login.js create mode 100644 src/pages/problemset.js create mode 100644 src/pages/userinfo.js diff --git a/README_REFACTORING.md b/README_REFACTORING.md index bc499a63..4e7760ec 100644 --- a/README_REFACTORING.md +++ b/README_REFACTORING.md @@ -101,6 +101,10 @@ Page-specific styling and functionality has been extracted into separate modules - **contest.js** - `/contest.php` - Contest list and contest view with countdown timers, problem list formatting - **status.js** - `/status.php` - Submission status page - **submit.js** - `/submitpage.php` - Code submission page +- **problemset.js** - `/problemset.php` - Problem list page with search forms and column widths +- **userinfo.js** - `/userinfo.php` - User profile page with avatar, AC problems, badge management +- **login.js** - `/loginpage.php` - Login page with Bootstrap-styled form +- **contestrank.js** - `/contestrank-oi.php` and `/contestrank-correct.php` - Contest ranking pages with colored cells Page modules are loaded automatically based on the current pathname and handle page-specific DOM manipulations that don't belong to any particular feature. diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js index 5e1bde0f..b0e0e903 100644 --- a/dist/XMOJ.user.js +++ b/dist/XMOJ.user.js @@ -5094,7 +5094,7 @@ int main() * Initialize auto login feature * Checks if user is logged in and redirects to login page if necessary */ - function init$n() { + function init$r() { // Only execute if AutoLogin feature is enabled if (!UtilityEnabled("AutoLogin")) { return; @@ -5154,7 +5154,7 @@ int main() * @param {Function} context.PurifyHTML - Function to sanitize HTML content * @param {Function} context.RenderMathJax - Function to render math formulas */ - function init$m(context) { + function init$q(context) { // Only execute if Discussion feature is enabled if (!UtilityEnabled("Discussion")) { return; @@ -6037,7 +6037,7 @@ int main() * * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 */ - function init$l() { + function init$p() { // Only execute if CopySamples feature is enabled if (!UtilityEnabled("CopySamples")) { return; @@ -6084,7 +6084,7 @@ int main() * - Adds a "Compare Submissions" button on problem pages * - Creates comparison interface on comparesource.php page */ - async function init$k() { + async function init$o() { // Only execute if CompareSource feature is enabled if (!UtilityEnabled("CompareSource")) { return; @@ -6266,7 +6266,7 @@ int main() * - Line 2500-2505: Remove submission child nodes on userinfo page * - Line 3209-3211: Remove h2.lang_en on problem_solution page */ - function init$j() { + function init$n() { // Only execute if RemoveUseless feature is enabled if (!UtilityEnabled("RemoveUseless")) { return; @@ -6344,7 +6344,7 @@ int main() * - Lines 219-222: Text replacement * - Line 304: Navbar brand text */ - function init$i() { + function init$m() { // Only execute if ReplaceXM feature is enabled if (!UtilityEnabled("ReplaceXM")) { return; @@ -6375,7 +6375,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 404-417: Status text replacement */ - function init$h() { + function init$l() { // Only execute if ReplaceYN feature is enabled if (!UtilityEnabled("ReplaceYN")) { return; @@ -6416,7 +6416,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 381-384: Animation CSS */ - function init$g() { + function init$k() { // Only execute if AddAnimation feature is enabled if (!UtilityEnabled("AddAnimation")) { return; @@ -6446,7 +6446,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 386-395: Color text CSS */ - function init$f() { + function init$j() { // Only execute if AddColorText feature is enabled if (!UtilityEnabled("AddColorText")) { return; @@ -6488,7 +6488,7 @@ int main() * - Lines 2850-2852: Clear credentials on failure * - Lines 2867-2876: Auto-fill and auto-submit login form */ - function init$e() { + function init$i() { // Only execute on login page if (location.pathname !== "/loginpage.php") { return; @@ -6539,7 +6539,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1666-1667: Modify contest start link */ - function init$d() { + function init$h() { // Only execute if RemoveAlerts feature is enabled if (!UtilityEnabled("RemoveAlerts")) { return; @@ -6584,7 +6584,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 216-218: Link to button replacement */ - function init$c() { + function init$g() { // Only execute if ReplaceLinks feature is enabled if (!UtilityEnabled("ReplaceLinks")) { return; @@ -6613,7 +6613,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 2020-2022: Auto-check O2 flag */ - function init$b() { + function init$f() { // Only execute if AutoO2 feature is enabled if (!UtilityEnabled("AutoO2")) { return; @@ -6656,7 +6656,7 @@ int main() * - Lines 1073-1078: Problemset page translations * - Lines 1611-1617: Contest page translations */ - function init$a() { + function init$e() { // Only execute if Translate feature is enabled if (!UtilityEnabled("Translate")) { return; @@ -6757,7 +6757,7 @@ int main() * - Lines 547-566: Countdown timer update logic * - Lines 1592-1594: Disables default clock on contest page */ - function init$9() { + function init$d() { // Only execute if AutoCountdown feature is enabled if (!UtilityEnabled("AutoCountdown")) { return; @@ -6832,7 +6832,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1699-1717: Standard solution column management */ - function init$8() { + function init$c() { // Only execute if MoreSTD feature is enabled if (!UtilityEnabled("MoreSTD")) { return; @@ -6914,7 +6914,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 2445-2495: Export AC code button and logic */ - function init$7() { + function init$b() { // Only execute if ExportACCode feature is enabled if (!UtilityEnabled("ExportACCode")) { return; @@ -7016,7 +7016,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1817-1841: Open all problem buttons */ - function init$6() { + function init$a() { // Only execute if OpenAllProblem feature is enabled if (!UtilityEnabled("OpenAllProblem")) { return; @@ -7098,7 +7098,7 @@ int main() * - Lines 247-251: Theme attribute setting * - Used throughout the codebase for conditional styling (17 occurrences) */ - function init$5() { + function init$9() { // Set theme based on DarkMode setting if (UtilityEnabled("DarkMode")) { document.querySelector("html").setAttribute("data-bs-theme", "dark"); @@ -7129,7 +7129,7 @@ int main() * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js * - Lines 1405-1463: Improve AC rate button and logic */ - function init$4() { + function init$8() { // Only execute if ImproveACRate feature is enabled if (!UtilityEnabled("ImproveACRate")) { return; @@ -7268,37 +7268,37 @@ int main() async function initializeFeatures(context) { try { // Initialize features that need to run early (before main page load) - init$n(); + init$r(); // Initialize theme (must run early) - init$5(); + init$9(); // Initialize features that clean up/modify the page - init$j(); - init$d(); + init$n(); + init$h(); // Initialize cosmetic/styling features - init$g(); - init$f(); + init$k(); + init$j(); // Initialize text replacement features - init$i(); - init$h(); - init$c(); - init$a(); + init$m(); + init$l(); + init$g(); + init$e(); // Initialize utility features - init$9(); + init$d(); + init$c(); + init$a(); init$8(); - init$6(); - init$4(); // Initialize page-specific features - init$l(); - init$e(); + init$p(); + init$i(); + init$f(); init$b(); - init$7(); - await init$k(); + await init$o(); // Initialize complex features that need context if (context) ; @@ -7348,7 +7348,7 @@ int main() * Initialize problem page * @param {Object} context - Page context with utilities */ - async function init$3(context) { + async function init$7(context) { const { SearchParams, RenderMathJax, RequestAPI, Style } = context; // Render MathJax @@ -7396,7 +7396,7 @@ int main() } // Add custom styles - addPageStyles(Style); + addPageStyles$1(Style); } /** @@ -7522,7 +7522,7 @@ int main() * Add custom page styles * @param {HTMLStyleElement} Style - Style element to append to */ - function addPageStyles(Style) { + function addPageStyles$1(Style) { Style.innerHTML += ` code, kbd, pre, samp { font-family: monospace, Consolas, 'Courier New'; @@ -7552,7 +7552,7 @@ pre { * Initialize contest page * @param {Object} context - Page context with utilities */ - async function init$2(context) { + async function init$6(context) { const { SearchParams } = context; // Check if viewing specific contest or contest list @@ -7740,7 +7740,7 @@ pre { * Initialize status page * @param {Object} context - Page context with utilities */ - async function init$1(context) { + async function init$5(context) { const { SearchParams } = context; // Only proceed if not in special UserScript mode @@ -7770,7 +7770,7 @@ pre { * Initialize submit page * @param {Object} context - Page context with utilities */ - async function init(context) { + async function init$4(context) { const { SearchParams } = context; // Set page title @@ -7788,6 +7788,685 @@ pre { // Most submit page features are handled by feature modules and CodeMirror initialization } + /** + * Problemset Page Module + * Handles all styling and functionality for /problemset.php + */ + + + /** + * Initialize problemset page + * @param {Object} context - Page context with utilities + */ + async function init$3(context) { + const { SearchParams } = context; + + // Set column widths + if (UtilityEnabled("ResetType")) { + setColumnWidths(); + } + + // Replace search forms with improved layout + replaceSearchForms(SearchParams); + + // Store problem names in localStorage + storeProblemNames(); + } + + /** + * Set column widths for problemset table + */ + function setColumnWidths() { + try { + const headers = document.querySelectorAll("#problemset > thead > tr > th"); + if (headers.length >= 5) { + headers[0].style.width = "5%"; // Status + headers[1].style.width = "10%"; // ID + headers[2].style.width = "75%"; // Title + headers[3].style.width = "5%"; // AC ratio + headers[4].style.width = "5%"; // Difficulty + } + } catch (error) { + console.error('[Problemset] Error setting column widths:', error); + } + } + + /** + * Replace search forms with improved layout + * @param {URLSearchParams} SearchParams - URL search parameters + */ + function replaceSearchForms(SearchParams) { + try { + const oldTable = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)"); + if (!oldTable) return; + + oldTable.outerHTML = ` +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    `; + + // Restore search value if present + const searchParam = SearchParams.get("search"); + if (searchParam) { + const searchInput = document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input"); + if (searchInput) { + searchInput.value = searchParam; + } + } + } catch (error) { + console.error('[Problemset] Error replacing search forms:', error); + } + } + + /** + * Store problem names in localStorage for quick access + */ + function storeProblemNames() { + try { + const rows = document.querySelector("#problemset")?.rows; + if (!rows) return; + + for (let i = 1; i < rows.length; i++) { + const problemId = rows[i].children[1]?.innerText; + const problemName = rows[i].children[2]?.innerText; + + if (problemId && problemName) { + localStorage.setItem(`UserScript-Problem-${problemId}-Name`, problemName); + } + } + } catch (error) { + console.error('[Problemset] Error storing problem names:', error); + } + } + + /** + * User Info Page Module + * Handles all styling and functionality for /userinfo.php + */ + + + /** + * Initialize user info page + * @param {Object} context - Page context with utilities + */ + async function init$2(context) { + const { SearchParams, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin } = context; + + // Check if in ByUserScript mode (upload standard solution) + if (SearchParams.get("ByUserScript") !== null) { + document.title = "上传标程"; + // Upload standard solution UI is handled in bootstrap.js + return; + } + + // Clean up submission section if RemoveUseless is enabled + if (UtilityEnabled("RemoveUseless")) { + cleanupSubmissionSection(); + } + + // Execute embedded script (chart initialization) + executeEmbeddedScript(); + + // Translate table headers + translateTableHeaders(); + + // Extract and display user information + await displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); + } + + /** + * Clean up submission section + */ + function cleanupSubmissionSection() { + try { + const submissionElement = document.getElementById("submission"); + if (submissionElement) { + const childNodes = submissionElement.childNodes; + for (let i = childNodes.length - 1; i >= 0; i--) { + childNodes[i].remove(); + } + } + } catch (error) { + console.error('[UserInfo] Error cleaning up submission section:', error); + } + } + + /** + * Execute embedded chart script + */ + function executeEmbeddedScript() { + try { + const scriptElement = document.querySelector("body > script:nth-child(5)"); + if (scriptElement) { + eval(scriptElement.innerHTML); + } + } catch (error) { + console.error('[UserInfo] Error executing embedded script:', error); + } + } + + /** + * Translate table headers + */ + function translateTableHeaders() { + try { + // Remove first row + const firstRow = document.querySelector("#statics > tbody > tr:nth-child(1)"); + if (firstRow) { + firstRow.remove(); + } + + // Translate remaining headers + const rows = document.querySelector("#statics > tbody")?.children; + if (!rows) return; + + for (let i = 0; i < rows.length; i++) { + if (rows[i].children[0]) { + const headerText = rows[i].children[0].innerText; + if (headerText === "Statistics") { + rows[i].children[0].innerText = "统计"; + } else if (headerText === "Email:") { + rows[i].children[0].innerText = "电子邮箱"; + } + rows[i].children[1].removeAttribute("align"); + } + } + } catch (error) { + console.error('[UserInfo] Error translating table headers:', error); + } + } + + /** + * Display user profile with avatar and solved problems + * @param {Function} GetUserInfo - Function to get user info + * @param {Function} GetUserBadge - Function to get user badge + * @param {Function} GetRelativeTime - Function to format relative time + * @param {Function} RequestAPI - Function to make API requests + * @param {Function} SmartAlert - Function to show alerts + * @param {boolean} IsAdmin - Whether current user is admin + */ + async function displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { + try { + // Extract AC problems + const acCell = document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)"); + const acProblems = []; + + if (acCell) { + const childNodes = acCell.childNodes; + for (let i = 0; i < childNodes.length; i++) { + if (childNodes[i].tagName === "A" && childNodes[i].href.indexOf("problem.php?id=") !== -1) { + acProblems.push(Number(childNodes[i].innerText.trim())); + } + } + acCell.remove(); + } + + // Extract user info from caption + const caption = document.querySelector("#statics > caption"); + if (!caption) return; + + const captionText = caption.childNodes[0].data.trim(); + const [userId, userNick] = captionText.split("--"); + caption.remove(); + + // Set page title + document.title = `用户 ${userId} 的个人中心`; + + // Create new layout + await createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); + } catch (error) { + console.error('[UserInfo] Error displaying user profile:', error); + } + } + + /** + * Create user profile layout with avatar, info, and solved problems + */ + async function createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { + // Create main row + const row = document.createElement("div"); + row.className = "row"; + + // Left column + const leftDiv = document.createElement("div"); + leftDiv.className = "col-md-5"; + row.appendChild(leftDiv); + + // Avatar and user info + const leftTopDiv = document.createElement("div"); + leftTopDiv.className = "row mb-2"; + leftDiv.appendChild(leftTopDiv); + + // Avatar + const avatarContainer = document.createElement("div"); + avatarContainer.classList.add("col-auto"); + const avatarElement = document.createElement("img"); + + const userInfo = await GetUserInfo(userId); + const emailHash = userInfo?.EmailHash; + + if (!emailHash) { + avatarElement.src = `https://cravatar.cn/avatar/00000000000000000000000000000000?d=mp&f=y`; + } else { + avatarElement.src = `https://cravatar.cn/avatar/${emailHash}?d=retro`; + } + + avatarElement.classList.add("rounded", "me-2"); + avatarElement.style.height = "120px"; + avatarContainer.appendChild(avatarElement); + leftTopDiv.appendChild(avatarContainer); + + // User info + const userInfoElement = document.createElement("div"); + userInfoElement.classList.add("col-auto"); + userInfoElement.style.lineHeight = "40px"; + userInfoElement.innerHTML += `用户名:${userId}
    `; + userInfoElement.innerHTML += `昵称:${userNick}
    `; + + if (UtilityEnabled("Rating")) { + userInfoElement.innerHTML += `评分:${userInfo?.Rating || 'N/A'}
    `; + } + + // Last online time (async) + const lastOnlineElement = document.createElement('div'); + lastOnlineElement.innerHTML = "最后在线:加载中...
    "; + userInfoElement.appendChild(lastOnlineElement); + + RequestAPI("LastOnline", { "Username": userId }, (result) => { + if (result.Success) { + lastOnlineElement.innerHTML = `最后在线:${GetRelativeTime(result.Data.logintime)}
    `; + } else { + lastOnlineElement.innerHTML = "最后在线:近三个月内从未
    "; + } + }); + + // Badge management buttons (admin only) + if (IsAdmin) { + await addBadgeManagement(userId, userInfoElement, GetUserBadge, RequestAPI, SmartAlert); + } + + leftTopDiv.appendChild(userInfoElement); + + // Move statistics table to left column + const leftTable = document.querySelector("body > div > div > center > table"); + if (leftTable) { + leftDiv.appendChild(leftTable); + } + + // Right column - AC problems + const rightDiv = document.createElement("div"); + rightDiv.className = "col-md-7"; + row.appendChild(rightDiv); + rightDiv.innerHTML = "
    已解决题目
    "; + + for (const problemId of acProblems) { + rightDiv.innerHTML += `${problemId} `; + } + + // Replace page content + const contentDiv = document.querySelector("body > div > div"); + if (contentDiv) { + contentDiv.innerHTML = ""; + contentDiv.appendChild(row); + } + } + + /** + * Add badge management buttons for admins + */ + async function addBadgeManagement(userId, container, GetUserBadge, RequestAPI, SmartAlert) { + const badgeInfo = await GetUserBadge(userId); + + if (badgeInfo.Content !== "") { + // Delete badge button + const deleteBadgeButton = document.createElement("button"); + deleteBadgeButton.className = "btn btn-outline-danger btn-sm"; + deleteBadgeButton.innerText = "删除标签"; + deleteBadgeButton.addEventListener("click", async () => { + if (confirm("您确定要删除此标签吗?")) { + RequestAPI("DeleteBadge", { "UserID": userId }, (response) => { + if (response.Success) { + clearBadgeCache(userId); + window.location.reload(); + } else { + SmartAlert(response.Message); + } + }); + } + }); + container.appendChild(deleteBadgeButton); + } else { + // Add badge button + const addBadgeButton = document.createElement("button"); + addBadgeButton.className = "btn btn-outline-primary btn-sm"; + addBadgeButton.innerText = "添加标签"; + addBadgeButton.addEventListener("click", async () => { + RequestAPI("NewBadge", { "UserID": userId }, (response) => { + if (response.Success) { + clearBadgeCache(userId); + window.location.reload(); + } else { + SmartAlert(response.Message); + } + }); + }); + container.appendChild(addBadgeButton); + } + } + + /** + * Clear badge cache for a user + * @param {string} userId - User ID + */ + function clearBadgeCache(userId) { + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(`UserScript-User-${userId}-Badge-`)) { + keysToRemove.push(key); + } + } + for (const key of keysToRemove) { + localStorage.removeItem(key); + } + } + + /** + * Login Page Module + * Handles all styling and functionality for /loginpage.php + * + * Note: Login functionality is handled by LoginFailed and SavePassword features + * This module only handles page styling + */ + + + /** + * Initialize login page + * @param {Object} context - Page context with utilities + */ + async function init$1(context) { + // Replace login form with Bootstrap-styled version + if (UtilityEnabled("NewBootstrap")) { + replaceLoginForm(); + } + + // Login handling and SavePassword are handled by feature modules + } + + /** + * Replace login form with modern Bootstrap styling + */ + function replaceLoginForm() { + try { + const loginForm = document.querySelector("#login"); + if (!loginForm) return; + + loginForm.innerHTML = `
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    `; + } catch (error) { + console.error('[Login] Error replacing login form:', error); + } + } + + /** + * Contest Rank Pages Module + * Handles all styling and functionality for /contestrank-oi.php and /contestrank-correct.php + */ + + + /** + * Initialize contest rank page + * @param {Object} context - Page context with utilities + */ + async function init(context) { + const { SearchParams, TidyTable, GetUsernameHTML, Style } = context; + + const pathname = location.pathname; + const isOI = pathname === "/contestrank-oi.php"; + const isCorrect = pathname === "/contestrank-correct.php"; + + // Create rank table if doesn't exist + if (document.querySelector("#rank") === null) { + document.querySelector("body > div > div.mt-3").innerHTML = + '

    比赛排名

    '; + } + + // Check if in UserScript mode + const byUserScript = SearchParams.get("ByUserScript") !== null; + + // Handle title and headers + const titleElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (titleElement.innerText === "比赛排名") { + document.querySelector("#rank").innerText = "比赛暂时还没有排名"; + } else { + if (isOI && !byUserScript) { + await initOIRanking(titleElement, TidyTable, GetUsernameHTML); + } else if (isCorrect) { + initCorrectRanking(titleElement, TidyTable, GetUsernameHTML); + } + } + + // Add page styles + addPageStyles(Style); + + // Hide link and set title + const linkElement = document.querySelector("body > div.container > div > center > a"); + if (linkElement) { + linkElement.style.display = "none"; + } + + const centerDiv = document.querySelector("body > div.container > div > center"); + if (centerDiv) { + centerDiv.style.paddingBottom = "10px"; + } + + document.title = titleElement.innerText; + } + + /** + * Initialize OI ranking table + */ + async function initOIRanking(titleElement, TidyTable, GetUsernameHTML) { + // Update title + const originalTitle = titleElement.innerText; + titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(OI排名)"; + + // Translate headers + translateRankHeaders(); + + // Refresh ranking function + const refreshOIRank = async () => { + await fetch(location.href) + .then((response) => response.text()) + .then(async (response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + TidyTable(parsedDocument.getElementById("rank")); + + const rows = parsedDocument.getElementById("rank").rows; + for (let i = 1; i < rows.length; i++) { + // Add medal badge + const metalCell = rows[i].cells[0]; + const metal = document.createElement("span"); + metal.innerText = metalCell.innerText; + metal.className = "badge text-bg-primary"; + metalCell.innerText = ""; + metalCell.appendChild(metal); + + // Format username + GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); + + // Style problem cells + for (let j = 5; j < rows[i].cells.length; j++) { + styleProblemCell(rows[i].cells[j]); + } + } + + // Update DOM + document.querySelector("#rank > tbody").innerHTML = parsedDocument.querySelector("#rank > tbody").innerHTML; + }); + }; + + // Initial refresh + await refreshOIRank(); + + // Auto-refresh on focus if enabled + if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", refreshOIRank); + } + } + + /** + * Initialize correct ranking table + */ + function initCorrectRanking(titleElement, TidyTable, GetUsernameHTML) { + // Update title + if (UtilityEnabled("ResetType")) { + const originalTitle = titleElement.innerText; + titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(订正排名)"; + + const linkElement = document.querySelector("body > div > div.mt-3 > center > a"); + if (linkElement) { + linkElement.remove(); + } + } + + // Translate headers + translateRankHeaders(); + + // Style rows + TidyTable(document.getElementById("rank")); + const rows = document.getElementById("rank").rows; + + for (let i = 1; i < rows.length; i++) { + // Add medal badge + const metalCell = rows[i].cells[0]; + const metal = document.createElement("span"); + metal.innerText = metalCell.innerText; + metal.className = "badge text-bg-primary"; + metalCell.innerText = ""; + metalCell.appendChild(metal); + + // Format username + GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); + + // Style problem cells + for (let j = 5; j < rows[i].cells.length; j++) { + styleProblemCell(rows[i].cells[j]); + } + } + } + + /** + * Translate rank table headers + */ + function translateRankHeaders() { + try { + const headers = document.querySelectorAll("#rank > thead > tr > th"); + if (headers.length >= 5) { + headers[0].innerText = "排名"; + headers[1].innerText = "用户"; + headers[2].innerText = "昵称"; + headers[3].innerText = "AC数"; + headers[4].innerText = "得分"; + } + } catch (error) { + console.error('[ContestRank] Error translating headers:', error); + } + } + + /** + * Style problem cell based on status + * @param {HTMLTableCellElement} cell - Table cell to style + */ + function styleProblemCell(cell) { + let innerText = cell.innerText; + let backgroundColor = cell.style.backgroundColor; + + // Parse RGB values + const red = parseInt(backgroundColor.substring(4, backgroundColor.indexOf(","))); + const green = parseInt(backgroundColor.substring(backgroundColor.indexOf(",") + 2, backgroundColor.lastIndexOf(","))); + const blue = parseInt(backgroundColor.substring(backgroundColor.lastIndexOf(",") + 2, backgroundColor.lastIndexOf(")"))); + + const noData = (red === 238 && green === 238 && blue === 238); + const firstBlood = (red === 170 && green === 170 && blue === 255); + const solved = (green === 255); + + let errorCount = 0; + if (solved) { + errorCount = (blue === 170 ? 5 : (blue - 51) / 32); + } else { + errorCount = (blue === 22 ? 15 : (170 - blue) / 10); + } + + // Apply styling + if (noData) { + backgroundColor = ""; + } else if (firstBlood) { + backgroundColor = "rgb(127, 127, 255)"; + } else if (solved) { + backgroundColor = `rgb(0, 255, 0, ${Math.max(1 / 10 * (10 - errorCount), 0.2)})`; + if (errorCount !== 0) { + innerText += ` (${errorCount === 5 ? "4+" : errorCount})`; + } + } else { + backgroundColor = `rgba(255, 0, 0, ${Math.min(errorCount / 10 + 0.2, 1)})`; + if (errorCount !== 0) { + innerText += ` (${errorCount === 15 ? "14+" : errorCount})`; + } + } + + cell.innerHTML = innerText; + cell.style.backgroundColor = backgroundColor; + cell.style.color = UtilityEnabled("DarkMode") ? "white" : "black"; + } + + /** + * Add page-specific styles + * @param {HTMLStyleElement} Style - Style element + */ + function addPageStyles(Style) { + Style.innerHTML += ` +td { + white-space: nowrap; +}`; + } + /** * Page loader - Initializes page-specific modules based on current URL * @@ -7800,10 +8479,15 @@ pre { * Page route mapping */ const PAGE_ROUTES = { - '/problem.php': init$3, - '/contest.php': init$2, - '/status.php': init$1, - '/submitpage.php': init, + '/problem.php': init$7, + '/contest.php': init$6, + '/status.php': init$5, + '/submitpage.php': init$4, + '/problemset.php': init$3, + '/userinfo.php': init$2, + '/loginpage.php': init$1, + '/contestrank-oi.php': init, + '/contestrank-correct.php': init, }; /** @@ -7890,7 +8574,13 @@ pre { RenderMathJax: RenderMathJax$1, RequestAPI, TidyTable, - Style: document.querySelector('style#UserScript-Style'), // Assuming bootstrap creates this + GetUserInfo: GetUserInfo$1, + GetUserBadge: GetUserBadge$1, + GetUsernameHTML: GetUsernameHTML$1, + GetRelativeTime, + SmartAlert, + Style: document.querySelector('style#UserScript-Style'), + IsAdmin: window.IsAdmin || false, // Set by bootstrap.js }; initializePage(pageContext).then(() => { diff --git a/src/main.js b/src/main.js index aa04ffb5..be3fc11a 100644 --- a/src/main.js +++ b/src/main.js @@ -80,7 +80,13 @@ window.addEventListener('load', () => { RenderMathJax, RequestAPI, TidyTable, - Style: document.querySelector('style#UserScript-Style'), // Assuming bootstrap creates this + GetUserInfo, + GetUserBadge, + GetUsernameHTML, + GetRelativeTime, + SmartAlert, + Style: document.querySelector('style#UserScript-Style'), + IsAdmin: window.IsAdmin || false, // Set by bootstrap.js }; initializePage(pageContext).then(() => { diff --git a/src/pages/contestrank.js b/src/pages/contestrank.js new file mode 100644 index 00000000..f4d8fe01 --- /dev/null +++ b/src/pages/contestrank.js @@ -0,0 +1,223 @@ +/** + * Contest Rank Pages Module + * Handles all styling and functionality for /contestrank-oi.php and /contestrank-correct.php + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize contest rank page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams, TidyTable, GetUsernameHTML, Style } = context; + + const pathname = location.pathname; + const isOI = pathname === "/contestrank-oi.php"; + const isCorrect = pathname === "/contestrank-correct.php"; + + // Create rank table if doesn't exist + if (document.querySelector("#rank") === null) { + document.querySelector("body > div > div.mt-3").innerHTML = + '

    比赛排名

    '; + } + + // Check if in UserScript mode + const byUserScript = SearchParams.get("ByUserScript") !== null; + + // Handle title and headers + const titleElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (titleElement.innerText === "比赛排名") { + document.querySelector("#rank").innerText = "比赛暂时还没有排名"; + } else { + if (isOI && !byUserScript) { + await initOIRanking(titleElement, TidyTable, GetUsernameHTML); + } else if (isCorrect) { + initCorrectRanking(titleElement, TidyTable, GetUsernameHTML); + } + } + + // Add page styles + addPageStyles(Style); + + // Hide link and set title + const linkElement = document.querySelector("body > div.container > div > center > a"); + if (linkElement) { + linkElement.style.display = "none"; + } + + const centerDiv = document.querySelector("body > div.container > div > center"); + if (centerDiv) { + centerDiv.style.paddingBottom = "10px"; + } + + document.title = titleElement.innerText; +} + +/** + * Initialize OI ranking table + */ +async function initOIRanking(titleElement, TidyTable, GetUsernameHTML) { + // Update title + const originalTitle = titleElement.innerText; + titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(OI排名)"; + + // Translate headers + translateRankHeaders(); + + // Refresh ranking function + const refreshOIRank = async () => { + await fetch(location.href) + .then((response) => response.text()) + .then(async (response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + TidyTable(parsedDocument.getElementById("rank")); + + const rows = parsedDocument.getElementById("rank").rows; + for (let i = 1; i < rows.length; i++) { + // Add medal badge + const metalCell = rows[i].cells[0]; + const metal = document.createElement("span"); + metal.innerText = metalCell.innerText; + metal.className = "badge text-bg-primary"; + metalCell.innerText = ""; + metalCell.appendChild(metal); + + // Format username + GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); + + // Style problem cells + for (let j = 5; j < rows[i].cells.length; j++) { + styleProblemCell(rows[i].cells[j]); + } + } + + // Update DOM + document.querySelector("#rank > tbody").innerHTML = parsedDocument.querySelector("#rank > tbody").innerHTML; + }); + }; + + // Initial refresh + await refreshOIRank(); + + // Auto-refresh on focus if enabled + if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", refreshOIRank); + } +} + +/** + * Initialize correct ranking table + */ +function initCorrectRanking(titleElement, TidyTable, GetUsernameHTML) { + // Update title + if (UtilityEnabled("ResetType")) { + const originalTitle = titleElement.innerText; + titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(订正排名)"; + + const linkElement = document.querySelector("body > div > div.mt-3 > center > a"); + if (linkElement) { + linkElement.remove(); + } + } + + // Translate headers + translateRankHeaders(); + + // Style rows + TidyTable(document.getElementById("rank")); + const rows = document.getElementById("rank").rows; + + for (let i = 1; i < rows.length; i++) { + // Add medal badge + const metalCell = rows[i].cells[0]; + const metal = document.createElement("span"); + metal.innerText = metalCell.innerText; + metal.className = "badge text-bg-primary"; + metalCell.innerText = ""; + metalCell.appendChild(metal); + + // Format username + GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); + + // Style problem cells + for (let j = 5; j < rows[i].cells.length; j++) { + styleProblemCell(rows[i].cells[j]); + } + } +} + +/** + * Translate rank table headers + */ +function translateRankHeaders() { + try { + const headers = document.querySelectorAll("#rank > thead > tr > th"); + if (headers.length >= 5) { + headers[0].innerText = "排名"; + headers[1].innerText = "用户"; + headers[2].innerText = "昵称"; + headers[3].innerText = "AC数"; + headers[4].innerText = "得分"; + } + } catch (error) { + console.error('[ContestRank] Error translating headers:', error); + } +} + +/** + * Style problem cell based on status + * @param {HTMLTableCellElement} cell - Table cell to style + */ +function styleProblemCell(cell) { + let innerText = cell.innerText; + let backgroundColor = cell.style.backgroundColor; + + // Parse RGB values + const red = parseInt(backgroundColor.substring(4, backgroundColor.indexOf(","))); + const green = parseInt(backgroundColor.substring(backgroundColor.indexOf(",") + 2, backgroundColor.lastIndexOf(","))); + const blue = parseInt(backgroundColor.substring(backgroundColor.lastIndexOf(",") + 2, backgroundColor.lastIndexOf(")"))); + + const noData = (red === 238 && green === 238 && blue === 238); + const firstBlood = (red === 170 && green === 170 && blue === 255); + const solved = (green === 255); + + let errorCount = 0; + if (solved) { + errorCount = (blue === 170 ? 5 : (blue - 51) / 32); + } else { + errorCount = (blue === 22 ? 15 : (170 - blue) / 10); + } + + // Apply styling + if (noData) { + backgroundColor = ""; + } else if (firstBlood) { + backgroundColor = "rgb(127, 127, 255)"; + } else if (solved) { + backgroundColor = `rgb(0, 255, 0, ${Math.max(1 / 10 * (10 - errorCount), 0.2)})`; + if (errorCount !== 0) { + innerText += ` (${errorCount === 5 ? "4+" : errorCount})`; + } + } else { + backgroundColor = `rgba(255, 0, 0, ${Math.min(errorCount / 10 + 0.2, 1)})`; + if (errorCount !== 0) { + innerText += ` (${errorCount === 15 ? "14+" : errorCount})`; + } + } + + cell.innerHTML = innerText; + cell.style.backgroundColor = backgroundColor; + cell.style.color = UtilityEnabled("DarkMode") ? "white" : "black"; +} + +/** + * Add page-specific styles + * @param {HTMLStyleElement} Style - Style element + */ +function addPageStyles(Style) { + Style.innerHTML += ` +td { + white-space: nowrap; +}`; +} diff --git a/src/pages/index.js b/src/pages/index.js index 2e26382b..440e3d6a 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -9,6 +9,10 @@ import { init as initProblemPage } from './problem.js'; import { init as initContestPage } from './contest.js'; import { init as initStatusPage } from './status.js'; import { init as initSubmitPage } from './submit.js'; +import { init as initProblemsetPage } from './problemset.js'; +import { init as initUserinfoPage } from './userinfo.js'; +import { init as initLoginPage } from './login.js'; +import { init as initContestRankPage } from './contestrank.js'; /** * Page route mapping @@ -18,6 +22,11 @@ const PAGE_ROUTES = { '/contest.php': initContestPage, '/status.php': initStatusPage, '/submitpage.php': initSubmitPage, + '/problemset.php': initProblemsetPage, + '/userinfo.php': initUserinfoPage, + '/loginpage.php': initLoginPage, + '/contestrank-oi.php': initContestRankPage, + '/contestrank-correct.php': initContestRankPage, }; /** diff --git a/src/pages/login.js b/src/pages/login.js new file mode 100644 index 00000000..952cca77 --- /dev/null +++ b/src/pages/login.js @@ -0,0 +1,61 @@ +/** + * Login Page Module + * Handles all styling and functionality for /loginpage.php + * + * Note: Login functionality is handled by LoginFailed and SavePassword features + * This module only handles page styling + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize login page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + // Replace login form with Bootstrap-styled version + if (UtilityEnabled("NewBootstrap")) { + replaceLoginForm(); + } + + // Login handling and SavePassword are handled by feature modules +} + +/** + * Replace login form with modern Bootstrap styling + */ +function replaceLoginForm() { + try { + const loginForm = document.querySelector("#login"); + if (!loginForm) return; + + loginForm.innerHTML = `
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    `; + } catch (error) { + console.error('[Login] Error replacing login form:', error); + } +} diff --git a/src/pages/problemset.js b/src/pages/problemset.js new file mode 100644 index 00000000..15819c71 --- /dev/null +++ b/src/pages/problemset.js @@ -0,0 +1,103 @@ +/** + * Problemset Page Module + * Handles all styling and functionality for /problemset.php + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize problemset page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Set column widths + if (UtilityEnabled("ResetType")) { + setColumnWidths(); + } + + // Replace search forms with improved layout + replaceSearchForms(SearchParams); + + // Store problem names in localStorage + storeProblemNames(); +} + +/** + * Set column widths for problemset table + */ +function setColumnWidths() { + try { + const headers = document.querySelectorAll("#problemset > thead > tr > th"); + if (headers.length >= 5) { + headers[0].style.width = "5%"; // Status + headers[1].style.width = "10%"; // ID + headers[2].style.width = "75%"; // Title + headers[3].style.width = "5%"; // AC ratio + headers[4].style.width = "5%"; // Difficulty + } + } catch (error) { + console.error('[Problemset] Error setting column widths:', error); + } +} + +/** + * Replace search forms with improved layout + * @param {URLSearchParams} SearchParams - URL search parameters + */ +function replaceSearchForms(SearchParams) { + try { + const oldTable = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)"); + if (!oldTable) return; + + oldTable.outerHTML = ` +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    `; + + // Restore search value if present + const searchParam = SearchParams.get("search"); + if (searchParam) { + const searchInput = document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input"); + if (searchInput) { + searchInput.value = searchParam; + } + } + } catch (error) { + console.error('[Problemset] Error replacing search forms:', error); + } +} + +/** + * Store problem names in localStorage for quick access + */ +function storeProblemNames() { + try { + const rows = document.querySelector("#problemset")?.rows; + if (!rows) return; + + for (let i = 1; i < rows.length; i++) { + const problemId = rows[i].children[1]?.innerText; + const problemName = rows[i].children[2]?.innerText; + + if (problemId && problemName) { + localStorage.setItem(`UserScript-Problem-${problemId}-Name`, problemName); + } + } + } catch (error) { + console.error('[Problemset] Error storing problem names:', error); + } +} diff --git a/src/pages/userinfo.js b/src/pages/userinfo.js new file mode 100644 index 00000000..b6c3edc2 --- /dev/null +++ b/src/pages/userinfo.js @@ -0,0 +1,292 @@ +/** + * User Info Page Module + * Handles all styling and functionality for /userinfo.php + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize user info page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin } = context; + + // Check if in ByUserScript mode (upload standard solution) + if (SearchParams.get("ByUserScript") !== null) { + document.title = "上传标程"; + // Upload standard solution UI is handled in bootstrap.js + return; + } + + // Clean up submission section if RemoveUseless is enabled + if (UtilityEnabled("RemoveUseless")) { + cleanupSubmissionSection(); + } + + // Execute embedded script (chart initialization) + executeEmbeddedScript(); + + // Translate table headers + translateTableHeaders(); + + // Extract and display user information + await displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); +} + +/** + * Clean up submission section + */ +function cleanupSubmissionSection() { + try { + const submissionElement = document.getElementById("submission"); + if (submissionElement) { + const childNodes = submissionElement.childNodes; + for (let i = childNodes.length - 1; i >= 0; i--) { + childNodes[i].remove(); + } + } + } catch (error) { + console.error('[UserInfo] Error cleaning up submission section:', error); + } +} + +/** + * Execute embedded chart script + */ +function executeEmbeddedScript() { + try { + const scriptElement = document.querySelector("body > script:nth-child(5)"); + if (scriptElement) { + eval(scriptElement.innerHTML); + } + } catch (error) { + console.error('[UserInfo] Error executing embedded script:', error); + } +} + +/** + * Translate table headers + */ +function translateTableHeaders() { + try { + // Remove first row + const firstRow = document.querySelector("#statics > tbody > tr:nth-child(1)"); + if (firstRow) { + firstRow.remove(); + } + + // Translate remaining headers + const rows = document.querySelector("#statics > tbody")?.children; + if (!rows) return; + + for (let i = 0; i < rows.length; i++) { + if (rows[i].children[0]) { + const headerText = rows[i].children[0].innerText; + if (headerText === "Statistics") { + rows[i].children[0].innerText = "统计"; + } else if (headerText === "Email:") { + rows[i].children[0].innerText = "电子邮箱"; + } + rows[i].children[1].removeAttribute("align"); + } + } + } catch (error) { + console.error('[UserInfo] Error translating table headers:', error); + } +} + +/** + * Display user profile with avatar and solved problems + * @param {Function} GetUserInfo - Function to get user info + * @param {Function} GetUserBadge - Function to get user badge + * @param {Function} GetRelativeTime - Function to format relative time + * @param {Function} RequestAPI - Function to make API requests + * @param {Function} SmartAlert - Function to show alerts + * @param {boolean} IsAdmin - Whether current user is admin + */ +async function displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { + try { + // Extract AC problems + const acCell = document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)"); + const acProblems = []; + + if (acCell) { + const childNodes = acCell.childNodes; + for (let i = 0; i < childNodes.length; i++) { + if (childNodes[i].tagName === "A" && childNodes[i].href.indexOf("problem.php?id=") !== -1) { + acProblems.push(Number(childNodes[i].innerText.trim())); + } + } + acCell.remove(); + } + + // Extract user info from caption + const caption = document.querySelector("#statics > caption"); + if (!caption) return; + + const captionText = caption.childNodes[0].data.trim(); + const [userId, userNick] = captionText.split("--"); + caption.remove(); + + // Set page title + document.title = `用户 ${userId} 的个人中心`; + + // Create new layout + await createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); + } catch (error) { + console.error('[UserInfo] Error displaying user profile:', error); + } +} + +/** + * Create user profile layout with avatar, info, and solved problems + */ +async function createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { + // Create main row + const row = document.createElement("div"); + row.className = "row"; + + // Left column + const leftDiv = document.createElement("div"); + leftDiv.className = "col-md-5"; + row.appendChild(leftDiv); + + // Avatar and user info + const leftTopDiv = document.createElement("div"); + leftTopDiv.className = "row mb-2"; + leftDiv.appendChild(leftTopDiv); + + // Avatar + const avatarContainer = document.createElement("div"); + avatarContainer.classList.add("col-auto"); + const avatarElement = document.createElement("img"); + + const userInfo = await GetUserInfo(userId); + const emailHash = userInfo?.EmailHash; + + if (!emailHash) { + avatarElement.src = `https://cravatar.cn/avatar/00000000000000000000000000000000?d=mp&f=y`; + } else { + avatarElement.src = `https://cravatar.cn/avatar/${emailHash}?d=retro`; + } + + avatarElement.classList.add("rounded", "me-2"); + avatarElement.style.height = "120px"; + avatarContainer.appendChild(avatarElement); + leftTopDiv.appendChild(avatarContainer); + + // User info + const userInfoElement = document.createElement("div"); + userInfoElement.classList.add("col-auto"); + userInfoElement.style.lineHeight = "40px"; + userInfoElement.innerHTML += `用户名:${userId}
    `; + userInfoElement.innerHTML += `昵称:${userNick}
    `; + + if (UtilityEnabled("Rating")) { + userInfoElement.innerHTML += `评分:${userInfo?.Rating || 'N/A'}
    `; + } + + // Last online time (async) + const lastOnlineElement = document.createElement('div'); + lastOnlineElement.innerHTML = "最后在线:加载中...
    "; + userInfoElement.appendChild(lastOnlineElement); + + RequestAPI("LastOnline", { "Username": userId }, (result) => { + if (result.Success) { + lastOnlineElement.innerHTML = `最后在线:${GetRelativeTime(result.Data.logintime)}
    `; + } else { + lastOnlineElement.innerHTML = "最后在线:近三个月内从未
    "; + } + }); + + // Badge management buttons (admin only) + if (IsAdmin) { + await addBadgeManagement(userId, userInfoElement, GetUserBadge, RequestAPI, SmartAlert); + } + + leftTopDiv.appendChild(userInfoElement); + + // Move statistics table to left column + const leftTable = document.querySelector("body > div > div > center > table"); + if (leftTable) { + leftDiv.appendChild(leftTable); + } + + // Right column - AC problems + const rightDiv = document.createElement("div"); + rightDiv.className = "col-md-7"; + row.appendChild(rightDiv); + rightDiv.innerHTML = "
    已解决题目
    "; + + for (const problemId of acProblems) { + rightDiv.innerHTML += `${problemId} `; + } + + // Replace page content + const contentDiv = document.querySelector("body > div > div"); + if (contentDiv) { + contentDiv.innerHTML = ""; + contentDiv.appendChild(row); + } +} + +/** + * Add badge management buttons for admins + */ +async function addBadgeManagement(userId, container, GetUserBadge, RequestAPI, SmartAlert) { + const badgeInfo = await GetUserBadge(userId); + + if (badgeInfo.Content !== "") { + // Delete badge button + const deleteBadgeButton = document.createElement("button"); + deleteBadgeButton.className = "btn btn-outline-danger btn-sm"; + deleteBadgeButton.innerText = "删除标签"; + deleteBadgeButton.addEventListener("click", async () => { + if (confirm("您确定要删除此标签吗?")) { + RequestAPI("DeleteBadge", { "UserID": userId }, (response) => { + if (response.Success) { + clearBadgeCache(userId); + window.location.reload(); + } else { + SmartAlert(response.Message); + } + }); + } + }); + container.appendChild(deleteBadgeButton); + } else { + // Add badge button + const addBadgeButton = document.createElement("button"); + addBadgeButton.className = "btn btn-outline-primary btn-sm"; + addBadgeButton.innerText = "添加标签"; + addBadgeButton.addEventListener("click", async () => { + RequestAPI("NewBadge", { "UserID": userId }, (response) => { + if (response.Success) { + clearBadgeCache(userId); + window.location.reload(); + } else { + SmartAlert(response.Message); + } + }); + }); + container.appendChild(addBadgeButton); + } +} + +/** + * Clear badge cache for a user + * @param {string} userId - User ID + */ +function clearBadgeCache(userId) { + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(`UserScript-User-${userId}-Badge-`)) { + keysToRemove.push(key); + } + } + for (const key of keysToRemove) { + localStorage.removeItem(key); + } +} From e19c4df6ad5ab8255a67a82ef63eea9f6e0d549d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 05:00:39 +0000 Subject: [PATCH 12/26] Add comprehensive Claude.md documentation for AI assistants --- Claude.md | 464 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 Claude.md diff --git a/Claude.md b/Claude.md new file mode 100644 index 00000000..39ef864c --- /dev/null +++ b/Claude.md @@ -0,0 +1,464 @@ +# XMOJ-Script Project Documentation for Claude + +## Project Overview + +XMOJ-Script is a Greasemonkey/Tampermonkey userscript that enhances the XMOJ (小明 Online Judge) platform. The project has been refactored from a monolithic 5000-line file into a modular, maintainable architecture using Rollup bundler. + +**Original File**: XMOJ.user.js (~5000 lines, unmaintainable) +**Current Structure**: Modular ES6 with features, pages, utilities, and core modules +**Build Output**: dist/XMOJ.user.js (preserves userscript header) + +## Architecture + +### Three-Layer Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ FEATURES LAYER │ +│ (20 modules) - User-facing functionality controlled │ +│ by UtilityEnabled() flags in localStorage │ +│ Examples: AutoLogin, DarkMode, Discussion, etc. │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ PAGES LAYER │ +│ (8 modules) - Page-specific styling and DOM │ +│ manipulation, routed by location.pathname │ +│ Examples: problem.js, contest.js, userinfo.js │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ UTILITIES & CORE LAYER │ +│ Shared utilities (10 modules) + core application logic │ +│ bootstrap.js contains legacy code being refactored │ +└─────────────────────────────────────────────────────────┘ +``` + +### Directory Structure + +``` +src/ +├── core/ # Core application logic +│ ├── bootstrap.js # Main app (LEGACY - being refactored out) +│ ├── config.js # UtilityEnabled() feature flags +│ ├── constants.js # Global constants +│ └── menu.js # Greasemonkey menu commands +│ +├── features/ # Feature modules (by UtilityEnabled name) +│ ├── index.js # Feature loader (routes to features) +│ ├── auto-login.js # AutoLogin feature +│ ├── dark-mode.js # DarkMode feature +│ └── ... (20 total) # Each feature is self-contained +│ +├── pages/ # Page-specific modules (by pathname) +│ ├── index.js # Page loader (routes to pages) +│ ├── problem.js # /problem.php +│ ├── contest.js # /contest.php +│ └── ... (8 total) # Each page handles its own styling +│ +├── utils/ # Shared utilities +│ ├── html.js # HTML escaping, sanitization +│ ├── time.js # Time formatting +│ ├── api.js # API requests +│ └── ... (10 total) # Pure utility functions +│ +└── main.js # Entry point (ties everything together) +``` + +## Key Concepts + +### 1. Features + +Features are user-facing functionality controlled by `UtilityEnabled()` flags stored in localStorage. + +**Pattern:** +```javascript +import { UtilityEnabled } from '../core/config.js'; + +export function init(context) { + if (!UtilityEnabled("FeatureName")) { + return; // Feature disabled, do nothing + } + + // Feature implementation here +} +``` + +**Key Points:** +- Each feature checks `UtilityEnabled()` before executing +- Features are initialized in `src/features/index.js` +- Features run BEFORE page modules (early in page load) +- Most features default to enabled (except DebugMode, SuperDebug, ReplaceXM) + +**Current Features (20):** +AutoLogin, Discussion, CopySamples, CompareSource, RemoveUseless, ReplaceXM, ReplaceYN, AddAnimation, AddColorText, SavePassword, RemoveAlerts, ReplaceLinks, AutoO2, Translate, AutoCountdown, MoreSTD, ExportACCode, OpenAllProblem, DarkMode, ImproveACRate + +### 2. Pages + +Pages are route-specific modules that handle page styling and DOM manipulation. + +**Pattern:** +```javascript +export async function init(context) { + // Access utilities from context + const { SearchParams, RenderMathJax, RequestAPI } = context; + + // Page-specific initialization here +} +``` + +**Key Points:** +- Pages are routed by `location.pathname` in `src/pages/index.js` +- Pages receive a context object with utilities +- Pages run AFTER bootstrap.js setup (on window.load event) +- Pages handle styling that doesn't belong to features + +**Current Pages (8 modules, 9 routes):** +- `/problem.php` - Problem view +- `/contest.php` - Contest list/view +- `/status.php` - Submission status +- `/submitpage.php` - Code submission +- `/problemset.php` - Problem list +- `/userinfo.php` - User profile +- `/loginpage.php` - Login form +- `/contestrank-oi.php` - OI ranking +- `/contestrank-correct.php` - Correct ranking + +### 3. Context Object + +The context object is passed to page modules and provides access to utilities: + +```javascript +const pageContext = { + SearchParams: new URLSearchParams(location.search), + RenderMathJax, // MathJax rendering + RequestAPI, // API requests + TidyTable, // Table styling + GetUserInfo, // User information + GetUserBadge, // User badges + GetUsernameHTML, // Username formatting + GetRelativeTime, // Relative time formatting + SmartAlert, // Alert system + Style, // Global style element + IsAdmin, // Admin flag +}; +``` + +## Development Workflow + +### Building + +```bash +npm install # Install dependencies +npm run build # Build once +npm run watch # Watch mode (auto-rebuild) +``` + +**Output:** `dist/XMOJ.user.js` (includes userscript header) + +### Adding a New Feature + +1. Create `src/features/feature-name.js`: +```javascript +import { UtilityEnabled } from '../core/config.js'; + +export function init(context) { + if (!UtilityEnabled("FeatureName")) return; + // Implementation +} +``` + +2. Add to `src/features/index.js`: +```javascript +import { init as initFeatureName } from './feature-name.js'; + +// In initializeFeatures(): +initFeatureName(); + +// In getExtractedFeatures(): +return [..., 'FeatureName']; +``` + +3. Test build and commit + +### Adding a New Page + +1. Create `src/pages/page-name.js`: +```javascript +export async function init(context) { + const { SearchParams } = context; + // Page-specific code +} +``` + +2. Add to `src/pages/index.js`: +```javascript +import { init as initPageName } from './page-name.js'; + +const PAGE_ROUTES = { + ... + '/page-name.php': initPageName, +}; +``` + +3. Test build and commit + +## Important Patterns & Conventions + +### 1. Feature Extraction Pattern + +When extracting a feature from bootstrap.js: +- Search for `UtilityEnabled("FeatureName")` +- Extract ALL related code (search entire file) +- Create self-contained module +- Document extracted line numbers in comments +- Update feature loader +- Test build + +### 2. Page Extraction Pattern + +When extracting a page from bootstrap.js: +- Search for `location.pathname == "/page.php"` +- Extract page-specific code (styling, DOM manipulation) +- Preserve feature flag checks (features handle their own checks) +- Create module with context parameter +- Update page loader +- Test build + +### 3. Utility Functions + +Utilities are made globally available via `window` for compatibility: +```javascript +// In main.js +window.RequestAPI = RequestAPI; +window.GetUserInfo = GetUserInfo; +// etc. +``` + +This allows bootstrap.js (legacy code) to call utilities. + +### 4. Error Handling + +Always wrap page/feature initialization in try-catch: +```javascript +try { + // Implementation +} catch (error) { + console.error('[ModuleName] Error:', error); +} +``` + +### 5. Async Operations + +Many operations are async (MathJax, API calls, user info): +```javascript +export async function init(context) { + await RenderMathJax(); + const userInfo = await GetUserInfo(userId); + // ... +} +``` + +## Important Technical Details + +### 1. localStorage Keys + +The script uses localStorage extensively: +- `UserScript-Setting-{FeatureName}` - Feature flags (true/false) +- `UserScript-User-{UserID}-*` - User data cache +- `UserScript-Problem-{PID}-*` - Problem data cache +- `UserScript-Contest-{CID}-*` - Contest data cache +- `UserScript-LastPage` - Last visited page (for AutoLogin) + +### 2. Feature Flags Default + +Most features default to enabled. Exceptions: +- `DebugMode` - false by default +- `SuperDebug` - false by default +- `ReplaceXM` - false by default (changes "小明" to "高老师") + +### 3. External Libraries + +The script uses these external libraries (loaded by bootstrap.js): +- **Bootstrap 5** - UI framework +- **jQuery** - DOM manipulation (legacy) +- **CodeMirror** - Code editor (for submit page) +- **MathJax** - Math rendering +- **DOMPurify** - HTML sanitization +- **CryptoJS** - MD5 hashing (for passwords) +- **FileSaver.js** - File downloads +- **marked.js** - Markdown rendering + +### 4. Greasemonkey APIs + +The script uses these GM APIs: +- `GM_registerMenuCommand` - Menu commands +- `GM_xmlhttpRequest` - Cross-origin requests +- `GM_setClipboard` - Clipboard access +- `GM_setValue` / `GM_getValue` - Persistent storage (optional) +- `GM_cookie` - Cookie access + +### 5. API Endpoints + +The script communicates with XMOJ backend via `RequestAPI()`: +- `GetUserInfo` - User information +- `GetUserBadge` - User badges +- `GetPostCount` - Discussion counts +- `GetMailMentionList` - Mail/mention notifications +- `LastOnline` - Last online time +- And many more... + +### 6. Time Synchronization + +The script syncs with server time: +```javascript +window.diff // Global time difference with server +``` + +Used by countdown timers (AutoCountdown feature, contest timers). + +### 7. Dark Mode + +DarkMode feature sets `data-bs-theme` attribute: +```javascript +document.querySelector("html").setAttribute("data-bs-theme", "dark"); +``` + +Many components check this for conditional styling. + +## Remaining Refactoring Work + +### Features Still in bootstrap.js (21 remaining) + +These features have NOT been extracted yet: +- AddUnits +- ApplyData, AutoCheat, AutoRefresh +- BBSPopup, CompileError, CopyMD +- DebugMode, DownloadPlayback +- IOFile, LoginFailed, MessagePopup +- NewBootstrap, NewDownload, NewTopBar +- ProblemSwitcher, Rating +- RefreshSolution, ResetType +- UploadStd + +### Pages That Could Be Extracted + +Additional pages that could benefit from extraction: +- `/index.php` - Home page +- `/modifypage.php` - Settings/changelog +- `/reinfo.php` - Test case info +- `/ceinfo.php` - Compile error info +- `/showsource.php` - Source code view +- `/problem_std.php` - Standard solution +- `/comparesource.php` - Source comparison (partially in features) +- `/downloads.php` - Downloads page +- `/problemstatus.php` - Problem statistics +- `/problem_solution.php` - Problem solution +- `/open_contest.php` - Open contests +- `/mail.php` - Mail system +- `/contest_video.php` - Contest video +- `/problem_video.php` - Problem video + +### Legacy Code in bootstrap.js + +The `bootstrap.js` file still contains: +- ~4400 lines of legacy code +- All remaining features (see above) +- All remaining page handlers +- Some duplicate code (features extracted but not removed from bootstrap) + +**Strategy:** Continue extracting features and pages until bootstrap.js only contains: +- Initial setup (theme, navbar, resources) +- Core application logic that doesn't fit elsewhere +- Minimal glue code + +## Git Workflow + +**Branch:** `claude/refactor-userscript-structure-011CUxWM4EozUNd9os4Da49N` + +**Commit Message Pattern:** +``` +Extract N features/pages: Name1, Name2, Name3 + +Description of what was extracted and why. + +New features/pages: +- Name1: Description +- Name2: Description + +Updated: +- Files that changed +- Build output size + +Total extracted: X +Remaining: Y +``` + +**Push Pattern:** +```bash +git add -A +git commit -m "..." +git push -u origin claude/refactor-userscript-structure-011CUxWM4EozUNd9os4Da49N +``` + +## Testing + +**Manual Testing Required:** +- Build completes without errors +- Output file has userscript header +- File size is reasonable (~500-600KB currently) +- No obvious syntax errors in output + +**No Automated Tests:** This project has no test suite. Changes must be tested manually on the XMOJ website. + +## Common Pitfalls + +1. **Don't remove code from bootstrap.js** until confirming it's fully extracted and working in the new module +2. **Preserve userscript header** - Rollup config handles this, don't modify without checking +3. **Check UtilityEnabled logic** - Features must self-check if enabled +4. **Context object** - Pages need utilities passed via context +5. **Async/await** - Many operations are async, handle properly +6. **localStorage keys** - Use existing patterns, don't create new schemes +7. **Global window vars** - Utilities must be on window for bootstrap.js compatibility +8. **Line number references** - When extracting, note original line numbers in comments + +## Success Metrics + +**Started with:** +- 1 file: XMOJ.user.js (5000 lines) +- No organization +- Impossible to maintain + +**Currently:** +- 20 feature modules (organized by functionality) +- 8 page modules (organized by route) +- 10 utility modules (shared code) +- 4 core modules (application logic) +- Clean architecture +- Easy to find and modify code +- ~549KB build output (reasonable) + +**Goal:** +- Extract all remaining 21 features +- Extract all remaining pages +- Reduce bootstrap.js to <1000 lines of essential code +- Maintain or reduce bundle size +- Keep all functionality working + +## Contact & Resources + +**Documentation:** +- README_REFACTORING.md - Detailed refactoring guide +- This file (Claude.md) - AI assistant documentation + +**Key Files to Understand:** +1. `src/main.js` - Entry point, shows initialization flow +2. `src/features/index.js` - Feature loader, shows all features +3. `src/pages/index.js` - Page loader, shows all pages +4. `src/core/config.js` - UtilityEnabled implementation +5. `rollup.config.js` - Build configuration + +**External Resources:** +- XMOJ Website: https://www.xmoj.tech +- Rollup Docs: https://rollupjs.org +- Greasemonkey API: https://wiki.greasespot.net/Greasemonkey_Manual:API From 164cd258f5fb33eb9d0080e3adfae2dbc7737d36 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 05:01:53 +0000 Subject: [PATCH 13/26] Add dist/ to .gitignore (build artifacts should not be tracked) --- .gitignore | 1 + dist/XMOJ.user.js | 8591 --------------------------------------------- 2 files changed, 1 insertion(+), 8591 deletions(-) delete mode 100644 dist/XMOJ.user.js diff --git a/.gitignore b/.gitignore index 2e71e24c..bd658d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .idea .vscode .DS_Store +dist/ diff --git a/dist/XMOJ.user.js b/dist/XMOJ.user.js deleted file mode 100644 index b0e0e903..00000000 --- a/dist/XMOJ.user.js +++ /dev/null @@ -1,8591 +0,0 @@ -// ==UserScript== -// @name XMOJ -// @version 2.5.0 -// @description XMOJ增强脚本 -// @author @XMOJ-Script-dev, @langningchen and the community -// @namespace https://github/langningchen -// @match *://*.xmoj.tech/* -// @match *://116.62.212.172/* -// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js -// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js -// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/clike/clike.min.js -// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/merge/merge.min.js -// @require https://gitee.com/mirrors_google/diff-match-patch/raw/master/javascript/diff_match_patch_uncompressed.js -// @require https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.2/purify.min.js -// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js -// @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js -// @grant GM_registerMenuCommand -// @grant GM_xmlhttpRequest -// @grant GM_setClipboard -// @grant unsafeWindow -// @grant GM_setValue -// @grant GM_getValue -// @grant GM_cookie -// @homepage https://www.xmoj-bbs.me/ -// @supportURL https://support.xmoj-bbs.me/form/8050213e-c806-4680-b414-0d1c48263677 -// @connect api.xmoj-bbs.tech -// @connect api.xmoj-bbs.me -// @connect challenges.cloudflare.com -// @connect cppinsights.io -// @connect cdnjs.cloudflare.com -// @connect 127.0.0.1 -// @license GPL -// @icon data:image/png;base64,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Uk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAJcEhZcwAACxMAAAsTAQCanBgAAAPSaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA3LjItYzAwMCA3OS4xYjY1YTc5LCAyMDIyLzA2LzEzLTE3OjQ2OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZTIyMGE0MzYtMWFhYi01MjRjLTg1ZjQtNDUyYjdkYTE4ZjdhIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjlEQTA5MUE5OTM0NEYxNEM5Q0RFMEVFREY2MzA4QThEIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjk1RkQ1QzI3QzBFN0I2NDdCMTBGMzU5NjU0RUI1NjQ2IiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCAyMy41IChXaW5kb3dzKSIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZTIyMGE0MzYtMWFhYi01MjRjLTg1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 -// ==/UserScript== - -(function () { - 'use strict'; - - /** - * Feature configuration and utility enabling/disabling - */ - - /** - * Check if a utility/feature is enabled - * @param {string} Name - The name of the utility/feature - * @returns {boolean} True if enabled, false otherwise - */ - let UtilityEnabled = (Name) => { - try { - if (localStorage.getItem("UserScript-Setting-" + Name) == null) { - const defaultOffItems = ["DebugMode", "SuperDebug", "ReplaceXM"]; - localStorage.setItem("UserScript-Setting-" + Name, defaultOffItems.includes(Name) ? "false" : "true"); - } - return localStorage.getItem("UserScript-Setting-" + Name) == "true"; - } catch (e) { - console.error(e); - if (UtilityEnabled("DebugMode")) { - const { SmartAlert } = require('./alerts'); - 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."); - } - } - }; - - /** - * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * You should have received a copy of the GNU General Public License along with this program. If not, see . - */ - - const AdminUserList = ["zhuchenrui2", "shanwenxiao", "chenlangning", "admin"]; - - /** - * Alert utilities - */ - - /** - * Shows an alert only if the message has changed - * @param {string} Message - The message to display - */ - let SmartAlert = (Message) => { - if (localStorage.getItem("UserScript-Alert") !== Message) { - alert(Message); - } - localStorage.setItem("UserScript-Alert", Message); - }; - - /** - * HTML utilities for escaping and purifying HTML content - */ - - - /** - * Escapes HTML special characters - * @param {string} str - The string to escape - * @returns {string} The escaped string - */ - let escapeHTML$1 = (str) => { - return str.replace(/[&<>"']/g, function (match) { - const escape = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - return escape[match]; - }); - }; - - /** - * Purifies HTML content using DOMPurify - * @param {string} Input - The HTML content to purify - * @returns {string} The purified HTML content - */ - let PurifyHTML$1 = (Input) => { - try { - return DOMPurify.sanitize(Input, { - "ALLOWED_TAGS": ["a", "b", "big", "blockquote", "br", "code", "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "hr", "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "ul", "var"], - "ALLOWED_ATTR": ["abbr", "accept", "accept-charset", "accesskey", "action", "align", "alt", "axis", "border", "cellpadding", "cellspacing", "char", "charoff", "charset", "checked", "cite", "clear", "color", "cols", "colspan", "compact", "coords", "datetime", "dir", "disabled", "enctype", "for", "frame", "headers", "height", "href", "hreflang", "hspace", "ismap", "itemprop", "label", "lang", "longdesc", "maxlength", "media", "method", "multiple", "name", "nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev", "rows", "rowspan", "rules", "scope", "selected", "shape", "size", "span", "src", "start", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "vspace", "width"] - }); - } 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."); - } - } - }; - - /** - * Time utilities for formatting and converting time values - */ - - - /** - * Calculates the relative time based on the input date. - * @param {string|Date} Input - The input date. - * @returns {string} The relative time in a formatted string. - */ - let GetRelativeTime = (Input) => { - try { - Input = new Date(parseInt(Input)); - let Now = new Date().getTime(); - let Delta = Now - Input.getTime(); - let RelativeName = ""; - if (Delta < 0) { - RelativeName = "未来"; - } else if (Delta <= 1000 * 60) { - RelativeName = "刚刚"; - } else if (Delta <= 1000 * 60 * 60) { - RelativeName = Math.floor((Now - Input) / 1000 / 60) + "分钟前"; - } else if (Delta <= 1000 * 60 * 60 * 24) { - RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60) + "小时前"; - } else if (Delta <= 1000 * 60 * 60 * 24 * 31) { - RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24) + "天前"; - } else if (Delta <= 1000 * 60 * 60 * 24 * 365) { - RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 31) + "个月前"; - } else { - RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 365) + "年前"; - } - return "" + RelativeName + ""; - } 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."); - } - } - }; - - /** - * Converts the given number of seconds to a formatted string representation of hours, minutes, and seconds. - * @param {number} InputSeconds - The number of seconds to convert. - * @returns {string} The formatted string representation of the input seconds. - */ - let SecondsToString = (InputSeconds) => { - try { - let Hours = Math.floor(InputSeconds / 3600); - let Minutes = Math.floor((InputSeconds % 3600) / 60); - let Seconds = InputSeconds % 60; - return (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; - } 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."); - } - } - }; - - /** - * Converts a string in the format "hh:mm:ss" to the equivalent number of seconds. - * @param {string} InputString - The input string to convert. - * @returns {number} The number of seconds equivalent to the input string. - */ - let StringToSeconds = (InputString) => { - try { - let SplittedString = InputString.split(":"); - return parseInt(SplittedString[0]) * 60 * 60 + parseInt(SplittedString[1]) * 60 + parseInt(SplittedString[2]); - } 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."); - } - } - }; - - /** - * Converts a time value to a string representation. - * @param {number} Time - The time value to convert. - * @returns {string|number} - The converted time value as a string, or the original value if UtilityEnabled("AddUnits") is false. - */ - let TimeToStringTime$1 = (Time) => { - try { - if (UtilityEnabled("AddUnits")) { - if (Time < 1000) { - return Time + "ms"; - } else if (Time < 1000 * 60) { - return (Time / 1000).toFixed(2) + "s"; - } - } else { - return Time; - } - } 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."); - } - } - }; - - /** - * Formatting utilities for sizes and other values - */ - - - /** - * Converts a memory size in bytes to a human-readable string representation. - * @param {number} Memory - The memory size in bytes. - * @returns {string} The human-readable string representation of the memory size. - */ - let SizeToStringSize$1 = (Memory) => { - try { - if (UtilityEnabled("AddUnits")) { - if (Memory < 1024) { - return Memory + "KB"; - } else if (Memory < 1024 * 1024) { - return (Memory / 1024).toFixed(2) + "MB"; - } else if (Memory < 1024 * 1024 * 1024) { - return (Memory / 1024 / 1024).toFixed(2) + "GB"; - } else { - return (Memory / 1024 / 1024 / 1024).toFixed(2) + "TB"; - } - } else { - return Memory; - } - } 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."); - } - } - }; - - /** - * Converts a code size in bytes to a human-readable string representation. - * @param {number} Memory - The code size in bytes. - * @returns {string} The human-readable string representation of the code size. - */ - let CodeSizeToStringSize$1 = (Memory) => { - try { - if (UtilityEnabled("AddUnits")) { - if (Memory < 1024) { - return Memory + "B"; - } else if (Memory < 1024 * 1024) { - return (Memory / 1024).toFixed(2) + "KB"; - } else if (Memory < 1024 * 1024 * 1024) { - return (Memory / 1024 / 1024).toFixed(2) + "MB"; - } else { - return (Memory / 1024 / 1024 / 1024).toFixed(2) + "GB"; - } - } else { - return Memory; - } - } 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."); - } - } - }; - - /** - * Version comparison utilities - */ - - /** - * Compares two version strings - * @param {string} currVer - Current version - * @param {string} remoteVer - Remote version - * @returns {boolean} True if update is needed - */ - function compareVersions(currVer, remoteVer) { - const currParts = currVer.split('.').map(Number); - const remoteParts = remoteVer.split('.').map(Number); - - const maxLen = Math.max(currParts.length, remoteParts.length); - for (let i = 0; i < maxLen; i++) { - const curr = currParts[i] !== undefined ? currParts[i] : 0; - const remote = remoteParts[i] !== undefined ? remoteParts[i] : 0; - if (remote > curr) { - return true; // update needed - } else if (remote < curr) { - return false; // no update needed - } - } - return false; // versions are equal - } - - /** - * API request utilities - */ - - - /** - * Make an API request to the backend - * @param {string} Action - The API action - * @param {Object} Data - The data to send - * @param {Function} CallBack - Callback function to handle response - */ - let RequestAPI = (Action, Data, CallBack) => { - try { - let Session = ""; - let Temp = document.cookie.split(";"); - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].includes("PHPSESSID")) { - Session = Temp[i].split("=")[1]; - } - } - if (Session === "") { //The cookie is httpOnly - GM.cookie.set({ - name: 'PHPSESSID', - value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), - path: "/" - }) - .then(() => { - console.log('Reset PHPSESSID successfully.'); - location.reload(); //Refresh the page to auth with the new PHPSESSID - }) - .catch((error) => { - console.error(error); - }); - } - - // Get current username from profile - let CurrentUsername = ""; - if (document.querySelector("#profile") !== null) { - CurrentUsername = document.querySelector("#profile").innerText; - CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); - } - - let PostData = { - "Authentication": { - "SessionID": Session, "Username": CurrentUsername, - }, "Data": Data, "Version": GM_info.script.version, "DebugMode": UtilityEnabled("DebugMode") - }; - let DataString = JSON.stringify(PostData); - if (UtilityEnabled("DebugMode")) { - console.log("Sent for", Action + ":", DataString); - } - GM_xmlhttpRequest({ - method: "POST", - url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-bbs.me/") + Action, - headers: { - "Content-Type": "application/json", - "Cache-Control": "no-cache", - "XMOJ-UserID": CurrentUsername, - "XMOJ-Script-Version": GM_info.script.version, - "DebugMode": UtilityEnabled("DebugMode") - }, - data: DataString, - onload: (Response) => { - if (UtilityEnabled("DebugMode")) { - console.log("Received for", Action + ":", Response.responseText); - } - try { - CallBack(JSON.parse(Response.responseText)); - } catch (Error) { - console.log(Response.responseText); - } - } - }); - } 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."); - } - } - }; - - /** - * Credential storage utilities using the Credentials API - */ - - /** - * Store user credentials - * @param {string} username - Username - * @param {string} password - Password - */ - let storeCredential$1 = async (username, password) => { - if ('credentials' in navigator && window.PasswordCredential) { - try { - const credential = new PasswordCredential({id: username, password: password}); - await navigator.credentials.store(credential); - } catch (e) { - console.error(e); - } - } - }; - - /** - * Get stored credentials - * @returns {Promise} The stored credentials or null - */ - let getCredential$1 = async () => { - if ('credentials' in navigator && window.PasswordCredential) { - try { - return await navigator.credentials.get({password: true, mediation: 'optional'}); - } catch (e) { - console.error(e); - } - } - return null; - }; - - /** - * Clear stored credentials - */ - let clearCredential = async () => { - if ('credentials' in navigator && window.PasswordCredential) { - try { - await navigator.credentials.preventSilentAccess(); - } catch (e) { - console.error(e); - } - } - }; - - /** - * MathJax rendering utilities - */ - - /** - * Render MathJax on the page - */ - let RenderMathJax$1 = async () => { - try { - if (document.getElementById("MathJax-script") === null) { - var ScriptElement = document.createElement("script"); - ScriptElement.id = "MathJax-script"; - ScriptElement.type = "text/javascript"; - ScriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.0.5/es5/tex-chtml.js"; - document.body.appendChild(ScriptElement); - await new Promise((Resolve) => { - ScriptElement.onload = () => { - Resolve(); - }; - }); - } - if (typeof MathJax !== 'undefined') { //If there is a Math expression - MathJax.startup.input[0].findTeX.options.inlineMath.push(["$", "$"]); - MathJax.startup.input[0].findTeX.getPatterns(); - MathJax.typeset(); - } - } catch (e) { - console.error(e); - } - }; - - /** - * Table utilities for styling and tidying up tables - */ - - - /** - * Tidies up the given table by applying Bootstrap styling and removing unnecessary attributes. - * - * @param {HTMLElement} Table - The table element to be tidied up. - */ - let TidyTable = (Table) => { - try { - if (UtilityEnabled("NewBootstrap") && Table != null) { - Table.className = "table table-hover"; - } - } 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."); - } - } - }; - - /** - * User information utilities - */ - - - /** - * Get user information - * @param {string} Username - The username - * @returns {Promise} User info object with Rating and EmailHash - */ - let GetUserInfo$1 = 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) { - return { - "Rating": localStorage.getItem("UserScript-User-" + Username + "-UserRating"), - "EmailHash": localStorage.getItem("UserScript-User-" + Username + "-EmailHash") - } - } - return await fetch("https://www.xmoj.tech/userinfo.php?user=" + Username).then((Response) => { - return Response.text(); - }).then((Response) => { - if (Response.indexOf("No such User!") !== -1) { - return null; - } - const ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let Rating = (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; - let Temp = ParsedDocument.querySelector("#statics > tbody").children; - let Email = Temp[Temp.length - 1].children[1].innerText.trim(); - let EmailHash = CryptoJS.MD5(Email).toString(); - localStorage.setItem("UserScript-User-" + Username + "-UserRating", Rating); - if (Email == "") { - EmailHash = undefined; - } else { - localStorage.setItem("UserScript-User-" + Username + "-EmailHash", EmailHash); - } - localStorage.setItem("UserScript-User-" + Username + "-LastUpdateTime", new Date().getTime()); - return { - "Rating": Rating, "EmailHash": EmailHash - } - }); - } 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."); - } - } - }; - - /** - * Retrieves the badge information for a given user. - * - * @param {string} Username - The username of the user. - * @returns {Promise} - A promise that resolves to an object containing the badge information. - * @property {string} BackgroundColor - The background color of the badge. - * @property {string} Color - The color of the badge. - * @property {string} Content - The content of the badge. - */ - let GetUserBadge$1 = async (Username) => { - try { - if (localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime") != null && new Date().getTime() - parseInt(localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime")) < 1000 * 60 * 60 * 24) { - return { - "BackgroundColor": localStorage.getItem("UserScript-User-" + Username + "-Badge-BackgroundColor"), - "Color": localStorage.getItem("UserScript-User-" + Username + "-Badge-Color"), - "Content": localStorage.getItem("UserScript-User-" + Username + "-Badge-Content") - } - } else { - let BackgroundColor = ""; - let Color = ""; - let Content = ""; - await new Promise((Resolve) => { - RequestAPI("GetBadge", { - "UserID": String(Username) - }, (Response) => { - if (Response.Success) { - BackgroundColor = Response.Data.BackgroundColor; - Color = Response.Data.Color; - Content = Response.Data.Content; - } - Resolve(); - }); - }); - localStorage.setItem("UserScript-User-" + Username + "-Badge-BackgroundColor", BackgroundColor); - localStorage.setItem("UserScript-User-" + Username + "-Badge-Color", Color); - localStorage.setItem("UserScript-User-" + Username + "-Badge-Content", Content); - localStorage.setItem("UserScript-User-" + Username + "-Badge-LastUpdateTime", String(new Date().getTime())); - return { - "BackgroundColor": BackgroundColor, "Color": Color, "Content": Content - } - } - } 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."); - } - } - }; - - /** - * Sets the HTML content of an element to display a username with optional additional information. - * @param {HTMLElement} Element - The element to set the HTML content. - * @param {string} Username - The username to display. - * @param {boolean} [Simple=false] - Indicates whether to display additional information or not. - * @param {string} [Href="https://www.xmoj.tech/userinfo.php?user="] - The URL to link the username to. - * @returns {Promise} - A promise that resolves when the HTML content is set. - */ - let GetUsernameHTML$1 = async (Element, Username, Simple = false, Href = "https://www.xmoj.tech/userinfo.php?user=") => { - try { - //Username = Username.replaceAll(/[^a-zA-Z0-9]/g, ""); - let ID = "Username-" + Username + "-" + Math.random(); - Element.id = ID; - Element.innerHTML = `
    `; - Element.appendChild(document.createTextNode(Username)); - let UserInfo = await GetUserInfo$1(Username); - if (UserInfo === null) { - document.getElementById(ID).innerHTML = ""; - document.getElementById(ID).appendChild(document.createTextNode(Username)); - return; - } - let HTMLData = ""; - if (!Simple) { - HTMLData += ``; - } - HTMLData += ` 500) { - HTMLData += "link-danger"; - } else if (Rating >= 400) { - HTMLData += "link-warning"; - } else if (Rating >= 300) { - HTMLData += "link-success"; - } else { - HTMLData += "link-info"; - } - } else { - HTMLData += "link-info"; - } - HTMLData += `\";">`; - if (!Simple) { - if (AdminUserList.includes(Username)) { - HTMLData += `脚本管理员`; - } - let BadgeInfo = await GetUserBadge$1(Username); - if (BadgeInfo.Content != "") { - HTMLData += `${BadgeInfo.Content}`; - } - } - if (document.getElementById(ID) !== null) { - document.getElementById(ID).innerHTML = HTMLData; - document.getElementById(ID).getElementsByTagName("a")[0].appendChild(document.createTextNode(Username)); - } - } 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."); - } - } - }; - - /** - * Bootstrap initialization and main application logic - */ - - - // Time difference for server synchronization (initialized to 0) - let diff = 0; - - // Theme management - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); - - const applyTheme = (theme) => { - document.querySelector("html").setAttribute("data-bs-theme", theme); - localStorage.setItem("UserScript-Setting-DarkMode", theme === "dark" ? "true" : "false"); - }; - - const applySystemTheme = (e) => applyTheme(e.matches ? "dark" : "light"); - - let initTheme = () => { - const saved = localStorage.getItem("UserScript-Setting-Theme") || "auto"; - if (saved === "auto") { - applyTheme(prefersDark.matches ? "dark" : "light"); - prefersDark.addEventListener("change", applySystemTheme); - } else { - applyTheme(saved); - prefersDark.removeEventListener("change", applySystemTheme); - } - }; - - // NavbarStyler class for styling the navigation bar - class NavbarStyler { - constructor() { - try { - this.navbar = document.querySelector('.navbar.navbar-expand-lg.bg-body-tertiary'); - if (this.navbar && UtilityEnabled("NewTopBar")) { - this.init(); - } - } 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."); - } - } - } - - init() { - try { - this.applyStyles(); - this.createOverlay(); - this.createSpacer(); - window.addEventListener('resize', () => this.updateBlurOverlay()); - this.updateBlurOverlay(); - } 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."); - } - } - } - - applyStyles() { - try { - let n = this.navbar; - n.classList.add('fixed-top', 'container', 'ml-auto'); - Object.assign(n.style, { - position: 'fixed', - borderRadius: '28px', - boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', - margin: '16px auto', - backgroundColor: 'rgba(255, 255, 255, 0)', - opacity: '0.75', - zIndex: '1000' - }); - } 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."); - } - } - } - - createOverlay() { - try { - if (!document.getElementById('blur-overlay')) { - let overlay = document.createElement('div'); - overlay.id = 'blur-overlay'; - document.body.appendChild(overlay); - - let style = document.createElement('style'); - style.textContent = ` - #blur-overlay { - position: fixed; - backdrop-filter: blur(4px); - z-index: 999; - pointer-events: none; - border-radius: 28px; - } - `; - document.head.appendChild(style); - } - } 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."); - } - } - } - - updateBlurOverlay() { - try { - let overlay = document.getElementById('blur-overlay'); - let n = this.navbar; - Object.assign(overlay.style, { - top: `${n.offsetTop}px`, - left: `${n.offsetLeft}px`, - width: `${n.offsetWidth}px`, - height: `${n.offsetHeight}px` - }); - } 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."); - } - } - } - - createSpacer() { - try { - let spacer = document.getElementById('navbar-spacer'); - let newHeight = this.navbar.offsetHeight + 24; - if (!spacer) { - spacer = document.createElement('div'); - spacer.id = 'navbar-spacer'; - spacer.style.height = `${newHeight}px`; - spacer.style.width = '100%'; - document.body.insertBefore(spacer, document.body.firstChild); - } else { - let currentHeight = parseInt(spacer.style.height, 10); - if (currentHeight !== newHeight) { - document.body.removeChild(spacer); - spacer = document.createElement('div'); - spacer.id = 'navbar-spacer'; - spacer.style.height = `${newHeight}px`; - spacer.style.width = '100%'; - document.body.insertBefore(spacer, document.body.firstChild); - } - } - } 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."); - } - } - } - } - - // Utility function to replace markdown images - function replaceMarkdownImages(text, string) { - return text.replace(/!\[.*?\]\(.*?\)/g, string); - } - - // Main initialization function - async function main() { - try { - if (location.href.startsWith('http://')) { - //use https - location.href = location.href.replace('http://', 'https://'); - } - if (location.host != "www.xmoj.tech") { - location.host = "www.xmoj.tech"; - } else { - if (location.href === 'https://www.xmoj.tech/open_contest_sign_up.php') { - return; - } - - // Get current username - let CurrentUsername = ""; - if (document.querySelector("#profile") !== null) { - CurrentUsername = document.querySelector("#profile").innerText; - CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); - } - - // Determine server URL - let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me"); - - document.body.classList.add("placeholder-glow"); - if (document.querySelector("#navbar") != null) { - if (document.querySelector("body > div > div.jumbotron") != null) { - document.querySelector("body > div > div.jumbotron").className = "mt-3"; - } - - if (UtilityEnabled("AutoLogin") && document.querySelector("#profile") != null && document.querySelector("#profile").innerHTML == "登录" && location.pathname != "/login.php" && location.pathname != "/loginpage.php" && location.pathname != "/lostpassword.php") { - localStorage.setItem("UserScript-LastPage", location.pathname + location.search); - location.href = "https://www.xmoj.tech/loginpage.php"; - } - - let Discussion = null; - if (UtilityEnabled("Discussion")) { - Discussion = document.createElement("li"); - document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); - Discussion.innerHTML = "讨论"; - } - if (UtilityEnabled("Translate")) { - document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a").innerText = "题库"; - } - //send analytics - RequestAPI("SendData", {}); - if (UtilityEnabled("ReplaceLinks")) { - document.body.innerHTML = String(document.body.innerHTML).replaceAll(/\[([^<]*)<\/a>\]/g, ""); - } - if (UtilityEnabled("ReplaceXM")) { - document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("海上", "上海"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("小红", "徐师娘"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("小粉", "彩虹"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("提交上节课的代码", "自动提交当年代码"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("高老师们", "我们"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("自高老师", "自我"); - document.title = String(document.title).replaceAll("小明", "高老师"); - } - - if (UtilityEnabled("NewBootstrap")) { - let Temp = document.querySelectorAll("link"); - for (var i = 0; i < Temp.length; i++) { - if (Temp[i].href.indexOf("bootstrap.min.css") != -1) { - Temp[i].remove(); - } else if (Temp[i].href.indexOf("white.css") != -1) { - Temp[i].remove(); - } else if (Temp[i].href.indexOf("semantic.min.css") != -1) { - Temp[i].remove(); - } else if (Temp[i].href.indexOf("bootstrap-theme.min.css") != -1) { - Temp[i].remove(); - } else if (Temp[i].href.indexOf("problem.css") != -1) { - Temp[i].remove(); - } - } - if (UtilityEnabled("DarkMode")) { - document.querySelector("html").setAttribute("data-bs-theme", "dark"); - } else { - document.querySelector("html").setAttribute("data-bs-theme", "light"); - } - var resources = [{ - type: 'link', - href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css', - rel: 'stylesheet' - }, { - type: 'link', - href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/darcula.min.css', - rel: 'stylesheet' - }, { - type: 'link', - href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/merge/merge.min.css', - rel: 'stylesheet' - }, { - type: 'link', - href: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css', - rel: 'stylesheet' - }, { - type: 'script', - src: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.js', - isModule: true - }]; - let loadResources = async () => { - let promises = resources.map(resource => { - return new Promise((resolve, reject) => { - let element; - if (resource.type === 'script') { - element = document.createElement('script'); - element.src = resource.src; - if (resource.isModule) { - element.type = 'module'; - } - element.onload = resolve; - element.onerror = reject; - } else if (resource.type === 'link') { - element = document.createElement('link'); - element.href = resource.href; - element.rel = resource.rel; - resolve(); // Stylesheets don't have an onload event - } - document.head.appendChild(element); - }); - }); - - await Promise.all(promises); - }; - if (location.pathname == "/submitpage.php") { - await loadResources(); - } else { - loadResources(); - } - document.querySelector("nav").className = "navbar navbar-expand-lg bg-body-tertiary"; - document.querySelector("#navbar > ul:nth-child(1)").classList = "navbar-nav me-auto mb-2 mb-lg-0"; - document.querySelector("body > div > nav > div > div.navbar-header").outerHTML = `${UtilityEnabled("ReplaceXM") ? "高老师" : "小明"}的OJ`; - document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li").classList = "nav-item dropdown"; - document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").className = "nav-link dropdown-toggle"; - document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a > span.caret").remove(); - Temp = document.querySelector("#navbar > ul:nth-child(1)").children; - for (var i = 0; i < Temp.length; i++) { - if (Temp[i].classList.contains("active")) { - Temp[i].classList.remove("active"); - Temp[i].children[0].classList.add("active"); - } - Temp[i].classList.add("nav-item"); - Temp[i].children[0].classList.add("nav-link"); - } - document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").setAttribute("data-bs-toggle", "dropdown"); - document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").removeAttribute("data-toggle"); - } - if (UtilityEnabled("RemoveUseless") && document.getElementsByTagName("marquee")[0] != undefined) { - document.getElementsByTagName("marquee")[0].remove(); - } - let Style = document.createElement("style"); - document.body.appendChild(Style); - Style.innerHTML = ` - nav { - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; - } - blockquote { - border-left: 5px solid var(--bs-secondary-bg); - padding: 0.5em 1em; - } - .status_y:hover { - box-shadow: #52c41a 1px 1px 10px 0px !important; - } - .status_n:hover { - box-shadow: #fe4c61 1px 1px 10px 0px !important; - } - .status_w:hover { - box-shadow: #ffa900 1px 1px 10px 0px !important; - } - .test-case { - border-radius: 5px !important; - } - .test-case:hover { - box-shadow: rgba(0, 0, 0, 0.3) 0px 10px 20px 3px !important; - } - .data[result-item] { - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; - } - .software_list { - width: unset !important; - } - .software_item { - margin: 5px 10px !important; - background-color: var(--bs-secondary-bg) !important; - } - .item-txt { - color: var(--bs-emphasis-color) !important; - } - .cnt-row { - justify-content: inherit; - align-items: stretch; - width: 100% !important; - padding: 1rem 0; - } - .cnt-row-head { - padding: 0.8em 1em; - background-color: var(--bs-secondary-bg); - border-radius: 0.3rem 0.3rem 0 0; - width: 100%; - } - .cnt-row-body { - padding: 1em; - border: 1px solid var(--bs-secondary-bg); - border-top: none; - border-radius: 0 0 0.3rem 0.3rem; - }`; - if (UtilityEnabled("AddAnimation")) { - Style.innerHTML += `.status, .test-case { - transition: 0.5s !important; - }`; - } - if (UtilityEnabled("AddColorText")) { - Style.innerHTML += `.red { - color: red !important; - } - .green { - color: green !important; - } - .blue { - color: blue !important; - }`; - } - - if (UtilityEnabled("RemoveUseless")) { - if (document.getElementsByClassName("footer")[0] != null) { - document.getElementsByClassName("footer")[0].remove(); - } - } - - if (UtilityEnabled("ReplaceYN")) { - let Temp = document.getElementsByClassName("status_y");//AC - for (let i = 0; i < Temp.length; i++) { - Temp[i].innerText = "✓"; - } - Temp = document.getElementsByClassName("status_n");//WA - for (let i = 0; i < Temp.length; i++) { - Temp[i].innerText = "✗"; - } - Temp = document.getElementsByClassName("status_w");//Waiting - for (let i = 0; i < Temp.length; i++) { - Temp[i].innerText = "⏳"; - } - } - - let Temp = document.getElementsByClassName("page-item"); - for (let i = 0; i < Temp.length; i++) { - Temp[i].children[0].className = "page-link"; - } - if (document.getElementsByClassName("pagination")[0] != null) { - document.getElementsByClassName("pagination")[0].classList.add("justify-content-center"); - } - - Temp = document.getElementsByTagName("table"); - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].querySelector("thead") != null) { - TidyTable(Temp[i]); - } - } - - setInterval(() => { - try { - let CurrentDate = new Date(new Date().getTime() + diff); - let Year = CurrentDate.getFullYear(); - if (Year > 3000) { - Year -= 1900; - } - let Month = CurrentDate.getMonth() + 1; - let _Date = CurrentDate.getDate(); - let Hours = CurrentDate.getHours(); - let Minutes = CurrentDate.getMinutes(); - let Seconds = CurrentDate.getSeconds(); - document.getElementById("nowdate").innerHTML = Year + "-" + (Month < 10 ? "0" : "") + Month + "-" + (_Date < 10 ? "0" : "") + _Date + " " + (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; - } catch (Error) { - } - if (UtilityEnabled("NewTopBar")) { - new NavbarStyler(); - } - if (UtilityEnabled("ResetType")) { - if (document.querySelector("#profile") != undefined && document.querySelector("#profile").innerHTML == "登录") { - let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); - PopupUL.innerHTML = ``; - PopupUL.children[0].addEventListener("click", () => { - location.href = "https://www.xmoj.tech/loginpage.php"; - }); - let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); - document.addEventListener("click", (event) => { - if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { - hideDropdownItems(); - } - }); - } else if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul") != undefined && document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul > li:nth-child(2)").innerText != "个人中心") { - let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); - PopupUL.style.cursor = 'pointer'; - PopupUL.innerHTML = ` - - - - - `; - PopupUL.children[0].addEventListener("click", () => { - location.href = "https://www.xmoj.tech/modifypage.php"; - }); - PopupUL.children[1].addEventListener("click", () => { - location.href = "https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername; - }); - PopupUL.children[2].addEventListener("click", () => { - location.href = "https://www.xmoj.tech/mail.php"; - }); - PopupUL.children[3].addEventListener("click", () => { - location.href = "https://www.xmoj.tech/index.php?ByUserScript=1"; - }); - PopupUL.children[4].addEventListener("click", () => { - location.href = "https://www.xmoj.tech/modifypage.php?ByUserScript=1"; - }); - PopupUL.children[5].addEventListener("click", () => { - clearCredential(); - GM.cookie.set({ - name: 'PHPSESSID', - value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), - path: "/" - }) - .then(() => { - console.log('Reset PHPSESSID successfully.'); - }) - .catch((error) => { - console.error(error); - }); //We can no longer rely of the server to set the cookie for us - location.href = "https://www.xmoj.tech/logout.php"; - }); - Array.from(PopupUL.children).forEach(item => { - item.style.opacity = 0; - item.style.transform = 'translateY(-16px)'; - item.style.transition = 'transform 0.3s ease, opacity 0.5s ease'; - }); - let showDropdownItems = () => { - PopupUL.style.display = 'block'; - Array.from(PopupUL.children).forEach((item, index) => { - clearTimeout(item._timeout); - item.style.opacity = 0; - item.style.transform = 'translateY(-4px)'; - item._timeout = setTimeout(() => { - item.style.opacity = 1; - item.style.transform = 'translateY(2px)'; - }, index * 36); - }); - }; - let hideDropdownItems = () => { - Array.from(PopupUL.children).forEach((item) => { - clearTimeout(item._timeout); - item.style.opacity = 0; - item.style.transform = 'translateY(-16px)'; - }); - setTimeout(() => { - PopupUL.style.display = 'none'; - }, 100); - }; - let toggleDropdownItems = () => { - if (PopupUL.style.display === 'block') { - hideDropdownItems(); - } else { - showDropdownItems(); - } - }; - let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); - parentLi.addEventListener("click", toggleDropdownItems); - document.addEventListener("click", (event) => { - if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { - hideDropdownItems(); - } - }); - } - } - if (UtilityEnabled("AutoCountdown")) { - let Temp = document.getElementsByClassName("UpdateByJS"); - for (let i = 0; i < Temp.length; i++) { - let EndTime = Temp[i].getAttribute("EndTime"); - if (EndTime === null) { - Temp[i].classList.remove("UpdateByJS"); - continue; - } - let TimeStamp = parseInt(EndTime) - new Date().getTime(); - if (TimeStamp < 3000) { - Temp[i].classList.remove("UpdateByJS"); - location.reload(); - } - let CurrentDate = new Date(TimeStamp); - let Day = parseInt((TimeStamp / 1000 / 60 / 60 / 24).toFixed(0)); - let Hour = CurrentDate.getUTCHours(); - let Minute = CurrentDate.getUTCMinutes(); - let Second = CurrentDate.getUTCSeconds(); - Temp[i].innerHTML = (Day !== 0 ? Day + "天" : "") + (Hour !== 0 ? (Hour < 10 ? "0" : "") + Hour + "小时" : "") + (Minute !== 0 ? (Minute < 10 ? "0" : "") + Minute + "分" : "") + (Second !== 0 ? (Second < 10 ? "0" : "") + Second + "秒" : ""); - } - } - }, 100); - - // Check for updates - fetch(ServerURL + "/Update.json", {cache: "no-cache"}) - .then((Response) => { - return Response.json(); - }) - .then((Response) => { - let CurrentVersion = GM_info.script.version; - let LatestVersion; - for (let i = Object.keys(Response.UpdateHistory).length - 1; i >= 0; i--) { - let VersionInfo = Object.keys(Response.UpdateHistory)[i]; - if (UtilityEnabled("DebugMode") || Response.UpdateHistory[VersionInfo].Prerelease == false) { - LatestVersion = VersionInfo; - break; - } - } - if (compareVersions(CurrentVersion, LatestVersion)) { - let UpdateDiv = document.createElement("div"); - UpdateDiv.innerHTML = ` - `; - if (UtilityEnabled("NewTopBar")) { - UpdateDiv.style.position = 'fixed'; - UpdateDiv.style.top = '72px'; - UpdateDiv.style.left = '50%'; - UpdateDiv.style.transform = 'translateX(-50%)'; - UpdateDiv.style.zIndex = '1001'; - let spacer = document.createElement("div"); - spacer.style.height = '48px'; - document.body.insertBefore(spacer, document.body.firstChild); - UpdateDiv.querySelector(".btn-close").addEventListener("click", function () { - document.body.removeChild(spacer); - }); - } - document.body.appendChild(UpdateDiv); - document.querySelector("body > div").insertBefore(UpdateDiv, document.querySelector("body > div > div.mt-3")); - } - if (localStorage.getItem("UserScript-Update-LastVersion") != GM_info.script.version) { - localStorage.setItem("UserScript-Update-LastVersion", GM_info.script.version); - let UpdateDiv = document.createElement("div"); - document.querySelector("body").appendChild(UpdateDiv); - UpdateDiv.className = "modal fade"; - UpdateDiv.id = "UpdateModal"; - UpdateDiv.tabIndex = -1; - let UpdateDialog = document.createElement("div"); - UpdateDiv.appendChild(UpdateDialog); - UpdateDialog.className = "modal-dialog"; - let UpdateContent = document.createElement("div"); - UpdateDialog.appendChild(UpdateContent); - UpdateContent.className = "modal-content"; - let UpdateHeader = document.createElement("div"); - UpdateContent.appendChild(UpdateHeader); - UpdateHeader.className = "modal-header"; - let UpdateTitle = document.createElement("h5"); - UpdateHeader.appendChild(UpdateTitle); - UpdateTitle.className = "modal-title"; - UpdateTitle.innerText = "更新日志"; - let UpdateCloseButton = document.createElement("button"); - UpdateHeader.appendChild(UpdateCloseButton); - UpdateCloseButton.type = "button"; - UpdateCloseButton.className = "btn-close"; - UpdateCloseButton.setAttribute("data-bs-dismiss", "modal"); - let UpdateBody = document.createElement("div"); - UpdateContent.appendChild(UpdateBody); - UpdateBody.className = "modal-body"; - let UpdateFooter = document.createElement("div"); - UpdateContent.appendChild(UpdateFooter); - UpdateFooter.className = "modal-footer"; - let UpdateButton = document.createElement("button"); - UpdateFooter.appendChild(UpdateButton); - UpdateButton.type = "button"; - UpdateButton.className = "btn btn-secondary"; - UpdateButton.setAttribute("data-bs-dismiss", "modal"); - UpdateButton.innerText = "关闭"; - let Version = Object.keys(Response.UpdateHistory)[Object.keys(Response.UpdateHistory).length - 1]; - let Data = Response.UpdateHistory[Version]; - let UpdateDataCard = document.createElement("div"); - UpdateBody.appendChild(UpdateDataCard); - UpdateDataCard.className = "card mb-3"; - let UpdateDataCardBody = document.createElement("div"); - UpdateDataCard.appendChild(UpdateDataCardBody); - UpdateDataCardBody.className = "card-body"; - let UpdateDataCardTitle = document.createElement("h5"); - UpdateDataCardBody.appendChild(UpdateDataCardTitle); - UpdateDataCardTitle.className = "card-title"; - UpdateDataCardTitle.innerText = Version; - let UpdateDataCardSubtitle = document.createElement("h6"); - UpdateDataCardBody.appendChild(UpdateDataCardSubtitle); - UpdateDataCardSubtitle.className = "card-subtitle mb-2 text-muted"; - UpdateDataCardSubtitle.innerHTML = GetRelativeTime(Data.UpdateDate); - let UpdateDataCardText = document.createElement("p"); - UpdateDataCardBody.appendChild(UpdateDataCardText); - UpdateDataCardText.className = "card-text"; - //release notes - if (Data.Notes != undefined) { - UpdateDataCardText.innerHTML = Data.Notes; - } - let UpdateDataCardList = document.createElement("ul"); - UpdateDataCardText.appendChild(UpdateDataCardList); - UpdateDataCardList.className = "list-group list-group-flush"; - for (let j = 0; j < Data.UpdateContents.length; j++) { - let UpdateDataCardListItem = document.createElement("li"); - UpdateDataCardList.appendChild(UpdateDataCardListItem); - UpdateDataCardListItem.className = "list-group-item"; - UpdateDataCardListItem.innerHTML = "(" + "#" + Data.UpdateContents[j].PR + ") " + Data.UpdateContents[j].Description; - } - let UpdateDataCardLink = document.createElement("a"); - UpdateDataCardBody.appendChild(UpdateDataCardLink); - UpdateDataCardLink.className = "card-link"; - UpdateDataCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script/releases/tag/" + Version; - UpdateDataCardLink.target = "_blank"; - UpdateDataCardLink.innerText = "查看该版本"; - new bootstrap.Modal(document.getElementById("UpdateModal")).show(); - } - }); - - // Request AddOnScript - RequestAPI("GetAddOnScript", {}, (Response) => { - if (Response.Success) { - eval(Response.Data["Script"]); - } else { - console.warn("Fetch AddOnScript failed: " + Response.Message); - } - }); - - // Toast notifications setup - let ToastContainer = document.createElement("div"); - ToastContainer.classList.add("toast-container", "position-fixed", "bottom-0", "end-0", "p-3"); - document.body.appendChild(ToastContainer); - - addEventListener("focus", () => { - if (UtilityEnabled("BBSPopup")) { - RequestAPI("GetBBSMentionList", {}, (Response) => { - if (Response.Success) { - ToastContainer.innerHTML = ""; - let MentionList = Response.Data.MentionList; - for (let i = 0; i < MentionList.length; i++) { - let Toast = document.createElement("div"); - Toast.classList.add("toast"); - Toast.setAttribute("role", "alert"); - let ToastHeader = document.createElement("div"); - ToastHeader.classList.add("toast-header"); - let ToastTitle = document.createElement("strong"); - ToastTitle.classList.add("me-auto"); - ToastTitle.innerHTML = "提醒:有人@你"; - ToastHeader.appendChild(ToastTitle); - let ToastTime = document.createElement("small"); - ToastTime.classList.add("text-body-secondary"); - ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); - ToastHeader.appendChild(ToastTime); - let ToastCloseButton = document.createElement("button"); - ToastCloseButton.type = "button"; - ToastCloseButton.classList.add("btn-close"); - ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); - ToastHeader.appendChild(ToastCloseButton); - Toast.appendChild(ToastHeader); - let ToastBody = document.createElement("div"); - ToastBody.classList.add("toast-body"); - ToastBody.innerHTML = "讨论" + MentionList[i].PostTitle + "有新回复"; - let ToastFooter = document.createElement("div"); - ToastFooter.classList.add("mt-2", "pt-2", "border-top"); - let ToastDismissButton = document.createElement("button"); - ToastDismissButton.type = "button"; - ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); - ToastDismissButton.innerText = "忽略"; - ToastDismissButton.addEventListener("click", () => { - RequestAPI("ReadBBSMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - Toast.remove(); - }); - ToastFooter.appendChild(ToastDismissButton); - let ToastViewButton = document.createElement("button"); - ToastViewButton.type = "button"; - ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); - ToastViewButton.innerText = "查看"; - ToastViewButton.addEventListener("click", () => { - open("https://www.xmoj.tech/discuss3/thread.php?tid=" + MentionList[i].PostID + '&page=' + MentionList[i].PageNumber, "_blank"); - RequestAPI("ReadBBSMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastViewButton); - ToastBody.appendChild(ToastFooter); - Toast.appendChild(ToastBody); - ToastContainer.appendChild(Toast); - new bootstrap.Toast(Toast).show(); - } - } - }); - } - if (UtilityEnabled("MessagePopup")) { - RequestAPI("GetMailMentionList", {}, async (Response) => { - if (Response.Success) { - ToastContainer.innerHTML = ""; - let MentionList = Response.Data.MentionList; - for (let i = 0; i < MentionList.length; i++) { - let Toast = document.createElement("div"); - Toast.classList.add("toast"); - Toast.setAttribute("role", "alert"); - let ToastHeader = document.createElement("div"); - ToastHeader.classList.add("toast-header"); - let ToastTitle = document.createElement("strong"); - ToastTitle.classList.add("me-auto"); - ToastTitle.innerHTML = "提醒:有新消息"; - ToastHeader.appendChild(ToastTitle); - let ToastTime = document.createElement("small"); - ToastTime.classList.add("text-body-secondary"); - ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); - ToastHeader.appendChild(ToastTime); - let ToastCloseButton = document.createElement("button"); - ToastCloseButton.type = "button"; - ToastCloseButton.classList.add("btn-close"); - ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); - ToastHeader.appendChild(ToastCloseButton); - Toast.appendChild(ToastHeader); - let ToastBody = document.createElement("div"); - ToastBody.classList.add("toast-body"); - ToastBody.innerHTML = "来自用户" + MentionList[i].FromUserID + "的消息"; - let ToastFooter = document.createElement("div"); - ToastFooter.classList.add("mt-2", "pt-2", "border-top"); - let ToastDismissButton = document.createElement("button"); - ToastDismissButton.type = "button"; - ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); - ToastDismissButton.innerText = "忽略"; - ToastDismissButton.addEventListener("click", () => { - RequestAPI("ReadMailMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - Toast.remove(); - }); - ToastFooter.appendChild(ToastDismissButton); - let ToastViewButton = document.createElement("button"); - ToastViewButton.type = "button"; - ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); - ToastViewButton.innerText = "查看"; - ToastViewButton.addEventListener("click", () => { - open("https://www.xmoj.tech/mail.php?to_user=" + MentionList[i].FromUserID, "_blank"); - RequestAPI("ReadMailMention", { - "MentionID": Number(MentionList[i].MentionID) - }, () => { - }); - }); - ToastFooter.appendChild(ToastViewButton); - ToastBody.appendChild(ToastFooter); - Toast.appendChild(ToastBody); - ToastContainer.appendChild(Toast); - new bootstrap.Toast(Toast).show(); - } - } - }); - } - }); - - dispatchEvent(new Event("focus")); - - if (location.pathname == "/index.php" || location.pathname == "/") { - if (new URL(location.href).searchParams.get("ByUserScript") != null) { - document.title = "脚本设置"; - localStorage.setItem("UserScript-Opened", "true"); - let Container = document.getElementsByClassName("mt-3")[0]; - Container.innerHTML = ""; - let Alert = document.createElement("div"); - Alert.classList.add("alert"); - Alert.classList.add("alert-primary"); - Alert.role = "alert"; - Alert.innerHTML = `欢迎您使用XMOJ增强脚本!点击 - 此处 - 查看更新日志。`; - Container.appendChild(Alert); - let UtilitiesCard = document.createElement("div"); - UtilitiesCard.classList.add("card"); - UtilitiesCard.classList.add("mb-3"); - let UtilitiesCardHeader = document.createElement("div"); - UtilitiesCardHeader.classList.add("card-header"); - UtilitiesCardHeader.innerText = "XMOJ增强脚本功能列表"; - UtilitiesCard.appendChild(UtilitiesCardHeader); - let UtilitiesCardBody = document.createElement("div"); - UtilitiesCardBody.classList.add("card-body"); - let CreateList = (Data) => { - let List = document.createElement("ul"); - List.classList.add("list-group"); - for (let i = 0; i < Data.length; i++) { - let Row = document.createElement("li"); - Row.classList.add("list-group-item"); - if (Data[i].Type == "A") { - Row.classList.add("list-group-item-success"); - } else if (Data[i].Type == "F") { - Row.classList.add("list-group-item-warning"); - } else if (Data[i].Type == "D") { - Row.classList.add("list-group-item-danger"); - } - if (Data[i].ID == "Theme") { - let Label = document.createElement("label"); - Label.classList.add("me-2"); - Label.htmlFor = "UserScript-Setting-Theme"; - Label.innerText = Data[i].Name; - Row.appendChild(Label); - let Select = document.createElement("select"); - Select.classList.add("form-select", "form-select-sm", "w-auto", "d-inline"); - Select.id = "UserScript-Setting-Theme"; - [ - ["light", "亮色"], - ["dark", "暗色"], - ["auto", "跟随系统"] - ].forEach(opt => { - let option = document.createElement("option"); - option.value = opt[0]; - option.innerText = opt[1]; - Select.appendChild(option); - }); - Select.value = localStorage.getItem("UserScript-Setting-Theme") || "auto"; - Select.addEventListener("change", () => { - localStorage.setItem("UserScript-Setting-Theme", Select.value); - initTheme(); - }); - Row.appendChild(Select); - } else if (Data[i].Children == undefined) { - let CheckBox = document.createElement("input"); - CheckBox.classList.add("form-check-input"); - CheckBox.classList.add("me-1"); - CheckBox.type = "checkbox"; - CheckBox.id = Data[i].ID; - if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == null) { - localStorage.setItem("UserScript-Setting-" + Data[i].ID, "true"); - } - if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == "false") { - CheckBox.checked = false; - } else { - CheckBox.checked = true; - } - CheckBox.addEventListener("change", () => { - return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); - }); - - Row.appendChild(CheckBox); - let Label = document.createElement("label"); - Label.classList.add("form-check-label"); - Label.htmlFor = Data[i].ID; - Label.innerText = Data[i].Name; - Row.appendChild(Label); - } else { - let Label = document.createElement("label"); - Label.innerText = Data[i].Name; - Row.appendChild(Label); - } - if (Data[i].Children != undefined) { - Row.appendChild(CreateList(Data[i].Children)); - } - List.appendChild(Row); - } - return List; - }; - UtilitiesCardBody.appendChild(CreateList([{ - "ID": "Discussion", - "Type": "F", - "Name": "恢复讨论与短消息功能" - }, { - "ID": "MoreSTD", "Type": "F", "Name": "查看到更多标程" - }, {"ID": "ApplyData", "Type": "A", "Name": "获取数据功能"}, { - "ID": "AutoCheat", "Type": "A", "Name": "自动提交当年代码" - }, {"ID": "Rating", "Type": "A", "Name": "添加用户评分和用户名颜色"}, { - "ID": "AutoRefresh", "Type": "A", "Name": "比赛列表、比赛排名界面自动刷新" - }, { - "ID": "AutoCountdown", "Type": "A", "Name": "比赛列表等界面的时间自动倒计时" - }, {"ID": "DownloadPlayback", "Type": "A", "Name": "回放视频增加下载功能"}, { - "ID": "ImproveACRate", "Type": "A", "Name": "自动提交已AC题目以提高AC率" - }, {"ID": "AutoO2", "Type": "F", "Name": "代码提交界面自动选择O2优化"}, { - "ID": "Beautify", "Type": "F", "Name": "美化界面", "Children": [{ - "ID": "NewTopBar", "Type": "F", "Name": "使用新的顶部导航栏" - }, { - "ID": "NewBootstrap", "Type": "F", "Name": "使用新版的Bootstrap样式库*" - }, {"ID": "ResetType", "Type": "F", "Name": "重新排版*"}, { - "ID": "AddColorText", "Type": "A", "Name": "增加彩色文字" - }, {"ID": "AddUnits", "Type": "A", "Name": "状态界面内存与耗时添加单位"}, { - "ID": "Theme", "Type": "A", "Name": "界面主题" - }, {"ID": "AddAnimation", "Type": "A", "Name": "增加动画"}, { - "ID": "ReplaceYN", "Type": "F", "Name": "题目前状态提示替换为好看的图标" - }, {"ID": "RemoveAlerts", "Type": "D", "Name": "去除多余反复的提示"}, { - "ID": "Translate", "Type": "F", "Name": "统一使用中文,翻译了部分英文*" - }, { - "ID": "ReplaceLinks", "Type": "F", "Name": "将网站中所有以方括号包装的链接替换为按钮" - }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { - "ID": "ReplaceXM", - "Type": "F", - "Name": "将网站中所有“小明”和“我”关键字替换为“高老师”,所有“小红”替换为“徐师娘”,所有“小粉”替换为“彩虹”,所有“下海”、“海上”替换为“上海” (此功能默认关闭)" - }] - }, { - "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" - }, { - "ID": "SavePassword", "Type": "A", "Name": "自动保存用户名与密码,免去每次手动输入密码的繁琐" - }, { - "ID": "CopySamples", "Type": "F", "Name": "题目界面测试样例有时复制无效" - }, { - "ID": "RefreshSolution", "Type": "F", "Name": "状态页面结果自动刷新每次只能刷新一个" - }, {"ID": "CopyMD", "Type": "A", "Name": "复制题目或题解内容"}, { - "ID": "ProblemSwitcher", "Type": "A", "Name": "比赛题目切换器" - }, { - "ID": "OpenAllProblem", "Type": "A", "Name": "比赛题目界面一键打开所有题目" - }, { - "ID": "CheckCode", "Type": "A", "Name": "提交代码前对代码进行检查", "Children": [{ - "ID": "IOFile", "Type": "A", "Name": "是否使用了文件输入输出(如果需要使用)" - }, {"ID": "CompileError", "Type": "A", "Name": "是否有编译错误"}] - }, { - "ID": "ExportACCode", "Type": "F", "Name": "导出AC代码每一道题目一个文件" - }, {"ID": "LoginFailed", "Type": "F", "Name": "修复登录后跳转失败*"}, { - "ID": "NewDownload", "Type": "A", "Name": "下载页面增加下载内容" - }, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, { - "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" - }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { - "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" - }, { - "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" - }])); - let UtilitiesCardFooter = document.createElement("div"); - UtilitiesCardFooter.className = "card-footer text-muted"; - UtilitiesCardFooter.innerText = "* 不建议关闭,可能会导致系统不稳定、界面错乱、功能缺失等问题\n绿色:增加功能 黄色:修改功能 红色:删除功能"; - UtilitiesCardBody.appendChild(UtilitiesCardFooter); - UtilitiesCard.appendChild(UtilitiesCardBody); - Container.appendChild(UtilitiesCard); - let FeedbackCard = document.createElement("div"); - FeedbackCard.className = "card mb-3"; - let FeedbackCardHeader = document.createElement("div"); - FeedbackCardHeader.className = "card-header"; - FeedbackCardHeader.innerText = "反馈、源代码、联系作者"; - FeedbackCard.appendChild(FeedbackCardHeader); - let FeedbackCardBody = document.createElement("div"); - FeedbackCardBody.className = "card-body"; - let FeedbackCardText = document.createElement("p"); - FeedbackCardText.className = "card-text"; - FeedbackCardText.innerText = "如果您有任何建议或者发现了 bug,请前往本项目的 GitHub 页面并提交 issue。提交 issue 前请先搜索是否有相同的 issue,如果有请在该 issue 下留言。请在 issue 中尽可能详细地描述您的问题,并且附上您的浏览器版本、操作系统版本、脚本版本、复现步骤等信息。谢谢您支持本项目。"; - FeedbackCardBody.appendChild(FeedbackCardText); - let FeedbackCardLink = document.createElement("a"); - FeedbackCardLink.className = "card-link"; - FeedbackCardLink.innerText = "GitHub"; - FeedbackCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script"; - FeedbackCardBody.appendChild(FeedbackCardLink); - FeedbackCard.appendChild(FeedbackCardBody); - Container.appendChild(FeedbackCard); - } else { - let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; - let NewsData = []; - for (let i = 0; i < Temp.length; i += 2) { - let Title = Temp[i].children[0].innerText; - let Time = 0; - if (Temp[i].children[1] != null) { - Time = Temp[i].children[1].innerText; - } - let Body = Temp[i + 1].innerHTML; - NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); - } - document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; - for (let i = 0; i < NewsData.length; i++) { - let NewsRow = document.createElement("div"); - NewsRow.className = "cnt-row"; - let NewsRowHead = document.createElement("div"); - NewsRowHead.className = "cnt-row-head title"; - NewsRowHead.innerText = NewsData[i].Title; - if (NewsData[i].Time != 0) { - NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; - } - NewsRow.appendChild(NewsRowHead); - let NewsRowBody = document.createElement("div"); - NewsRowBody.className = "cnt-row-body"; - NewsRowBody.innerHTML = NewsData[i].Body; - NewsRow.appendChild(NewsRowBody); - document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); - } - let CountDownData = document.querySelector("#countdown_list").innerHTML; - document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
    -
    倒计时
    -
    ${CountDownData}
    -
    `; - let Tables = document.getElementsByTagName("table"); - for (let i = 0; i < Tables.length; i++) { - TidyTable(Tables[i]); - } - document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML += `
    -
    公告
    -
    加载中...
    -
    `; - RequestAPI("GetNotice", {}, (Response) => { - if (Response.Success) { - document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = marked.parse(Response.Data["Notice"]).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); - RenderMathJax(); - let UsernameElements = document.getElementsByClassName("Usernames"); - for (let i = 0; i < UsernameElements.length; i++) { - GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); - } - } else { - document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = "加载失败: " + Response.Message; - } - }); - } - } else if (location.pathname == "/problemset.php") { - if (UtilityEnabled("Translate")) { - document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input").placeholder = "题目编号"; - document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button").innerText = "确认"; - document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input").placeholder = "标题或内容"; - document.querySelector("#problemset > thead > tr > th:nth-child(1)").innerText = "状态"; - } - if (UtilityEnabled("ResetType")) { - document.querySelector("#problemset > thead > tr > th:nth-child(1)").style.width = "5%"; - document.querySelector("#problemset > thead > tr > th:nth-child(2)").style.width = "10%"; - document.querySelector("#problemset > thead > tr > th:nth-child(3)").style.width = "75%"; - document.querySelector("#problemset > thead > tr > th:nth-child(4)").style.width = "5%"; - document.querySelector("#problemset > thead > tr > th:nth-child(5)").style.width = "5%"; - } - document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)").outerHTML = ` -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    `; - if (SearchParams.get("search") != null) { - document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input").value = SearchParams.get("search"); - } - - let Temp = document.querySelector("#problemset").rows; - for (let i = 1; i < Temp.length; i++) { - localStorage.setItem("UserScript-Problem-" + Temp[i].children[1].innerText + "-Name", Temp[i].children[2].innerText); - } - } else if (location.pathname == "/problem.php") { - await RenderMathJax(); - if (SearchParams.get("cid") != null && UtilityEnabled("ProblemSwitcher")) { - document.getElementsByTagName("h2")[0].innerHTML += " (" + localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID") + ")"; - 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); - } - } - - let problemSwitcher = document.createElement("div"); - problemSwitcher.style.position = "fixed"; - problemSwitcher.style.top = "50%"; - problemSwitcher.style.left = "0"; - problemSwitcher.style.transform = "translateY(-50%)"; - problemSwitcher.style.maxHeight = "80vh"; - problemSwitcher.style.overflowY = "auto"; - if (document.querySelector("html").getAttribute("data-bs-theme") == "dark") { - problemSwitcher.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; - } else { - problemSwitcher.style.backgroundColor = "rgba(255, 255, 255, 0.8)"; - } - problemSwitcher.style.padding = "10px"; - problemSwitcher.style.borderRadius = "0 10px 10px 0"; - problemSwitcher.style.display = "flex"; - problemSwitcher.style.flexDirection = "column"; - - let problemList = JSON.parse(ContestProblemList); - for (let i = 0; i < problemList.length; i++) { - let buttonText = ""; - if (i < 26) { - buttonText = String.fromCharCode(65 + i); - } else { - buttonText = String.fromCharCode(97 + (i - 26)); - } - let activeClass = ""; - if (problemList[i].url === location.href) { - activeClass = "active"; - } - problemSwitcher.innerHTML += `${buttonText}`; - } - document.body.appendChild(problemSwitcher); - } - if (document.querySelector("body > div > div.mt-3 > h2") != null) { - document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; - setTimeout(() => { - location.href = "https://www.xmoj.tech/problemset.php"; - }, 1000); - } else { - let PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); - if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { - document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; - } - //修复提交按钮 - let SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(12)'); - if (SubmitLink == null) { //a special type of problem - SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(10)'); - } - if (SubmitLink == null) { - SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(11)'); - } - if (SubmitLink == null) { - SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(13)'); - } - if (SubmitLink == null) { - SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(9)'); - } - if (SubmitLink == null) { //为什么这个破东西老是换位置 - SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(7)'); - } - if (SubmitLink == null) { //tmd又换位置 - SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(8)'); - } - let SubmitButton = document.createElement('button'); - SubmitButton.id = 'SubmitButton'; - SubmitButton.className = 'btn btn-outline-secondary'; - SubmitButton.textContent = '提交'; - SubmitButton.href = SubmitLink.href; - SubmitButton.onclick = function () { - window.location.href = SubmitLink.href; - console.log(SubmitLink.href); - }; - - // Replace the element with the button - SubmitLink.parentNode.replaceChild(SubmitButton, SubmitLink); - // Remove the button's outer [] - let str = document.querySelector('.mt-3 > center:nth-child(1)').innerHTML; - let target = SubmitButton.outerHTML; - let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); - document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = result; - document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () { - window.location.href = SubmitLink.href; - console.log(SubmitLink.href); - }; - let Temp = document.querySelectorAll(".sampledata"); - for (var i = 0; i < Temp.length; i++) { - Temp[i].parentElement.className = "card"; - } - if (UtilityEnabled("RemoveUseless")) { - document.querySelector("h2.lang_en").remove(); - document.getElementsByTagName("center")[1].remove(); - } - if (UtilityEnabled("CopySamples")) { - $(".copy-btn").click((Event) => { - let CurrentButton = $(Event.currentTarget); - let span = CurrentButton.parent().last().find(".sampledata"); - if (!span.length) { - CurrentButton.text("未找到代码块").addClass("done"); - setTimeout(() => { - $(".copy-btn").text("复制").removeClass("done"); - }, 1000); - return; - } - GM_setClipboard(span.text()); - CurrentButton.text("复制成功").addClass("done"); - setTimeout(() => { - $(".copy-btn").text("复制").removeClass("done"); - }, 1000); - //document.body.removeChild(textarea[0]); - }); - } - let IOFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); - if (IOFileElement != null) { - while (IOFileElement.childNodes.length >= 1) { - IOFileElement.parentNode.insertBefore(IOFileElement.childNodes[0], IOFileElement); - } - IOFileElement.parentNode.insertBefore(document.createElement("br"), IOFileElement); - IOFileElement.remove(); - let Temp = document.querySelector("body > div > div.mt-3 > center").childNodes[2].data.trim(); - let IOFilename = Temp.substring(0, Temp.length - 3); - localStorage.setItem("UserScript-Problem-" + PID + "-IOFilename", IOFilename); - } - - if (UtilityEnabled("CopyMD")) { - await fetch(location.href).then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let Temp = ParsedDocument.querySelectorAll(".cnt-row-body"); - if (UtilityEnabled("DebugMode")) console.log(Temp); - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].children[0].className === "content lang_cn") { - let CopyMDButton = document.createElement("button"); - CopyMDButton.className = "btn btn-sm btn-outline-secondary copy-btn"; - CopyMDButton.innerText = "复制"; - CopyMDButton.style.marginLeft = "10px"; - CopyMDButton.type = "button"; - document.querySelectorAll(".cnt-row-head.title")[i].appendChild(CopyMDButton); - CopyMDButton.addEventListener("click", () => { - GM_setClipboard(Temp[i].children[0].innerText.trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); - CopyMDButton.innerText = "复制成功"; - setTimeout(() => { - CopyMDButton.innerText = "复制"; - }, 1000); - }); - } - } - }); - } - - if (UtilityEnabled("Discussion")) { - let DiscussButton = document.createElement("button"); - DiscussButton.className = "btn btn-outline-secondary position-relative"; - DiscussButton.innerHTML = `讨论`; - DiscussButton.style.marginLeft = "10px"; - DiscussButton.type = "button"; - DiscussButton.addEventListener("click", () => { - if (SearchParams.get("cid") != null) { - open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); - } else { - open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); - } - }); - document.querySelector("body > div > div.mt-3 > center").appendChild(DiscussButton); - let UnreadBadge = document.createElement("span"); - UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; - UnreadBadge.style.display = "none"; - DiscussButton.appendChild(UnreadBadge); - - let RefreshCount = () => { - RequestAPI("GetPostCount", { - "ProblemID": Number(PID) - }, (Response) => { - if (Response.Success) { - if (Response.Data.DiscussCount != 0) { - UnreadBadge.innerText = Response.Data.DiscussCount; - UnreadBadge.style.display = ""; - } - } - }); - }; - RefreshCount(); - addEventListener("focus", RefreshCount); - } - - let Tables = document.getElementsByTagName("table"); - for (let i = 0; i < Tables.length; i++) { - TidyTable(Tables[i]); - } - } - Style.innerHTML += "code, kbd, pre, samp {"; - Style.innerHTML += " font-family: monospace, Consolas, 'Courier New';"; - Style.innerHTML += " font-size: 1rem;"; - Style.innerHTML += "}"; - Style.innerHTML += "pre {"; - Style.innerHTML += " padding: 0.3em 0.5em;"; - Style.innerHTML += " margin: 0.5em 0;"; - Style.innerHTML += "}"; - Style.innerHTML += ".in-out {"; - Style.innerHTML += " overflow: hidden;"; - Style.innerHTML += " display: flex;"; - Style.innerHTML += " padding: 0.5em 0;"; - Style.innerHTML += "}"; - Style.innerHTML += ".in-out .in-out-item {"; - Style.innerHTML += " flex: 1;"; - Style.innerHTML += " overflow: hidden;"; - Style.innerHTML += "}"; - Style.innerHTML += ".cnt-row .title {"; - Style.innerHTML += " font-weight: bolder;"; - Style.innerHTML += " font-size: 1.1rem;"; - Style.innerHTML += "}"; - Style.innerHTML += ".cnt-row .content {"; - Style.innerHTML += " overflow: hidden;"; - Style.innerHTML += "}"; - Style.innerHTML += "a.copy-btn {"; - Style.innerHTML += " float: right;"; - Style.innerHTML += " padding: 0 0.4em;"; - Style.innerHTML += " border: 1px solid var(--bs-primary);"; - Style.innerHTML += " border-radius: 3px;"; - Style.innerHTML += " color: var(--bs-primary);"; - Style.innerHTML += " cursor: pointer;"; - Style.innerHTML += "}"; - Style.innerHTML += "a.copy-btn:hover {"; - Style.innerHTML += " background-color: var(--bs-secondary-bg);"; - Style.innerHTML += "}"; - Style.innerHTML += "a.done, a.done:hover {"; - Style.innerHTML += " background-color: var(--bs-primary);"; - Style.innerHTML += " color: white;"; - Style.innerHTML += "}"; - } else if (location.pathname == "/status.php") { - if (SearchParams.get("ByUserScript") == null) { - document.title = "提交状态"; - document.querySelector("body > script:nth-child(5)").remove(); - if (UtilityEnabled("NewBootstrap")) { - document.querySelector("#simform").outerHTML = `
    - -
    - - -
    -
    - - -
    - - -
    -
    - -
    `; - } - - if (UtilityEnabled("ImproveACRate")) { - let ImproveACRateButton = document.createElement("button"); - document.querySelector("body > div.container > div > div.input-append").appendChild(ImproveACRateButton); - ImproveACRateButton.className = "btn btn-outline-secondary"; - ImproveACRateButton.innerText = "提高正确率"; - ImproveACRateButton.disabled = true; - let ACProblems = []; - await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) - .then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - ImproveACRateButton.innerText += "(" + (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText) * 100).toFixed(2) + "%)"; - let Temp = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); - for (let i = 0; i < Temp.length; i++) { - ACProblems.push(Number(Temp[i].substring(2, Temp[i].indexOf(",")))); - } - ImproveACRateButton.disabled = false; - }); - ImproveACRateButton.addEventListener("click", async () => { - ImproveACRateButton.disabled = true; - let SubmitTimes = 3; - let Count = 0; - let SubmitInterval = setInterval(async () => { - if (Count >= SubmitTimes) { - clearInterval(SubmitInterval); - location.reload(); - return; - } - ImproveACRateButton.innerText = "正在提交 (" + (Count + 1) + "/" + SubmitTimes + ")"; - let PID = ACProblems[Math.floor(Math.random() * ACProblems.length)]; - let SID = 0; - await fetch("https://www.xmoj.tech/status.php?problem_id=" + PID + "&jresult=4") - .then((Result) => { - return Result.text(); - }).then((Result) => { - let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); - SID = ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; - }); - let Code = ""; - await fetch("https://www.xmoj.tech/getsource.php?id=" + SID) - .then((Response) => { - return Response.text(); - }).then((Response) => { - Code = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); - }); - await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, - "method": "POST", - "body": "id=" + PID + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" - }); - Count++; - }, 1000); - }); - ImproveACRateButton.style.marginBottom = ImproveACRateButton.style.marginRight = "7px"; - ImproveACRateButton.style.marginRight = "7px"; - } - if (UtilityEnabled("CompareSource")) { - let CompareButton = document.createElement("button"); - document.querySelector("body > div.container > div > div.input-append").appendChild(CompareButton); - CompareButton.className = "btn btn-outline-secondary"; - CompareButton.innerText = "比较提交记录"; - CompareButton.addEventListener("click", () => { - location.href = "https://www.xmoj.tech/comparesource.php"; - }); - CompareButton.style.marginBottom = "7px"; - } - if (UtilityEnabled("ResetType")) { - document.querySelector("#result-tab > thead > tr > th:nth-child(1)").remove(); - document.querySelector("#result-tab > thead > tr > th:nth-child(2)").remove(); - document.querySelector("#result-tab > thead > tr > th:nth-child(10)").innerHTML = "开启O2"; - } - let Temp = document.querySelector("#result-tab > tbody").childNodes; - let SolutionIDs = []; - for (let i = 1; i < Temp.length; i += 2) { - let SID = Number(Temp[i].childNodes[1].innerText); - SolutionIDs.push(SID); - if (UtilityEnabled("ResetType")) { - Temp[i].childNodes[0].remove(); - Temp[i].childNodes[0].innerHTML = "
    " + SID + " " + "重交"; - Temp[i].childNodes[1].remove(); - Temp[i].childNodes[1].children[0].removeAttribute("class"); - Temp[i].childNodes[3].childNodes[0].innerText = SizeToStringSize(Temp[i].childNodes[3].childNodes[0].innerText); - Temp[i].childNodes[4].childNodes[0].innerText = TimeToStringTime(Temp[i].childNodes[4].childNodes[0].innerText); - Temp[i].childNodes[5].innerText = Temp[i].childNodes[5].childNodes[0].innerText; - Temp[i].childNodes[6].innerText = CodeSizeToStringSize(Temp[i].childNodes[6].innerText.substring(0, Temp[i].childNodes[6].innerText.length - 1)); - Temp[i].childNodes[9].innerText = (Temp[i].childNodes[9].innerText == "" ? "否" : "是"); - } - if (SearchParams.get("cid") === null) { - localStorage.setItem("UserScript-Solution-" + SID + "-Problem", Temp[i].childNodes[1].innerText); - } else { - localStorage.setItem("UserScript-Solution-" + SID + "-Contest", SearchParams.get("cid")); - localStorage.setItem("UserScript-Solution-" + SID + "-PID-Contest", Temp[i].childNodes[1].innerText.charAt(0)); - } - } - - if (UtilityEnabled("RefreshSolution")) { - let StdList; - await new Promise((Resolve) => { - RequestAPI("GetStdList", {}, async (Result) => { - if (Result.Success) { - StdList = Result.Data.StdList; - Resolve(); - } - }); - }); - - let Rows = document.getElementById("result-tab").rows; - let Points = Array(); - for (let i = 1; i <= SolutionIDs.length; i++) { - Rows[i].cells[2].className = "td_result"; - let SolutionID = SolutionIDs[i - 1]; - if (Rows[i].cells[2].children.length == 2) { - Points[SolutionID] = Rows[i].cells[2].children[1].innerText; - Rows[i].cells[2].children[1].remove(); - } - Rows[i].cells[2].innerHTML += ""; - setTimeout(() => { - RefreshResult(SolutionID); - }, 0); - } - - let RefreshResult = async (SolutionID) => { - let CurrentRow = null; - let Rows = document.getElementById("result-tab").rows; - for (let i = 0; i < SolutionIDs.length; i++) { - if (SolutionIDs[i] == SolutionID) { - CurrentRow = Rows[i + 1]; - break; - } - } - await fetch("status-ajax.php?solution_id=" + SolutionID) - .then((Response) => { - return Response.text(); - }) - .then((Response) => { - let PID = 0; - if (SearchParams.get("cid") === null) { - PID = localStorage.getItem("UserScript-Solution-" + SolutionID + "-Problem"); - } else { - PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + (CurrentRow.cells[1].innerText.charCodeAt(0) - 65) + "-PID"); - } - let ResponseData = Response.split(","); - CurrentRow.cells[3].innerHTML = "
    " + SizeToStringSize(ResponseData[1]) + "
    "; - CurrentRow.cells[4].innerHTML = "
    " + TimeToStringTime(ResponseData[2]) + "
    "; - let TempHTML = ""; - TempHTML += judge_result[ResponseData[0]]; - TempHTML += ""; - if (Points[SolutionID] != undefined) { - TempHTML += "" + Points[SolutionID] + ""; - if (Points[SolutionID].substring(0, Points[SolutionID].length - 1) >= 50) { - TempHTML += `查看标程`; - } - } - if (ResponseData[0] < 4) { - setTimeout(() => { - RefreshResult(SolutionID); - }, 500); - TempHTML += ""; - } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { - if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; - let Std = StdList.find((Element) => { - return Element == Number(PID); - }); - if (Std != undefined) { - TempHTML += "✅"; - } else { - RequestAPI("UploadStd", { - "ProblemID": Number(PID), - }, (Result) => { - if (Result.Success) { - CurrentRow.cells[2].innerHTML += "🆗"; - } else { - CurrentRow.cells[2].innerHTML += "⚠️"; - } - }); - } - } - CurrentRow.cells[2].innerHTML = TempHTML; - }); - }; - } - } - } else if (location.pathname == "/contest.php") { - if (UtilityEnabled("AutoCountdown")) { - clock = () => { - }; - } - if (location.href.indexOf("?cid=") == -1) { - if (UtilityEnabled("ResetType")) { - document.querySelector("body > div > div.mt-3 > center").innerHTML = String(document.querySelector("body > div > div.mt-3 > center").innerHTML).replaceAll("ServerTime:", "服务器时间:"); - document.querySelector("body > div > div.mt-3 > center > table").style.marginTop = "10px"; - - document.querySelector("body > div > div.mt-3 > center > form").outerHTML = `
    -
    -
    -
    - - -
    -
    -
    `; - } - if (UtilityEnabled("Translate")) { - document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[0].innerText = "编号"; - document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[1].innerText = "标题"; - document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[2].innerText = "状态"; - document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].remove(); - document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].innerText = "创建者"; - } - let Temp = document.querySelector("body > div > div.mt-3 > center > table > tbody").childNodes; - for (let i = 1; i < Temp.length; i++) { - let CurrentElement = Temp[i].childNodes[2].childNodes; - if (CurrentElement[1].childNodes[0].data.indexOf("运行中") != -1) { - let Time = String(CurrentElement[1].childNodes[1].innerText).substring(4); - let Day = parseInt(Time.substring(0, Time.indexOf("天"))) || 0; - let Hour = parseInt(Time.substring((Time.indexOf("天") == -1 ? 0 : Time.indexOf("天") + 1), Time.indexOf("小时"))) || 0; - let Minute = parseInt(Time.substring((Time.indexOf("小时") == -1 ? 0 : Time.indexOf("小时") + 2), Time.indexOf("分"))) || 0; - let Second = parseInt(Time.substring((Time.indexOf("分") == -1 ? 0 : Time.indexOf("分") + 1), Time.indexOf("秒"))) || 0; - let TimeStamp = new Date().getTime() + diff + ((((isNaN(Day) ? 0 : Day) * 24 + Hour) * 60 + Minute) * 60 + Second) * 1000; - CurrentElement[1].childNodes[1].setAttribute("EndTime", TimeStamp); - CurrentElement[1].childNodes[1].classList.add("UpdateByJS"); - } else if (CurrentElement[1].childNodes[0].data.indexOf("开始于") != -1) { - let TimeStamp = Date.parse(String(CurrentElement[1].childNodes[0].data).substring(4)) + diff; - CurrentElement[1].setAttribute("EndTime", TimeStamp); - CurrentElement[1].classList.add("UpdateByJS"); - } else if (CurrentElement[1].childNodes[0].data.indexOf("已结束") != -1) { - let TimeStamp = String(CurrentElement[1].childNodes[0].data).substring(4); - CurrentElement[1].childNodes[0].data = " 已结束 "; - CurrentElement[1].className = "red"; - let Temp = document.createElement("span"); - CurrentElement[1].appendChild(Temp); - Temp.className = "green"; - Temp.innerHTML = TimeStamp; - } - Temp[i].childNodes[3].style.display = "none"; - Temp[i].childNodes[4].innerHTML = "" + Temp[i].childNodes[4].innerHTML + ""; - localStorage.setItem("UserScript-Contest-" + Temp[i].childNodes[0].innerText + "-Name", Temp[i].childNodes[1].innerText); - } - } else { - document.getElementsByTagName("h3")[0].innerHTML = "比赛" + document.getElementsByTagName("h3")[0].innerHTML.substring(7); - if (document.querySelector("#time_left") != null) { - let EndTime = document.querySelector("body > div > div.mt-3 > center").childNodes[3].data; - EndTime = EndTime.substring(EndTime.indexOf("结束时间是:") + 6, EndTime.lastIndexOf("。")); - EndTime = new Date(EndTime).getTime(); - if (new Date().getTime() < EndTime) { - document.querySelector("#time_left").classList.add("UpdateByJS"); - document.querySelector("#time_left").setAttribute("EndTime", EndTime); - } - } - let HTMLData = document.querySelector("body > div > div.mt-3 > center > div").innerHTML; - HTMLData = HTMLData.replaceAll("  \n  ", " "); - HTMLData = HTMLData.replaceAll("
    开始于: ", "开始时间:"); - HTMLData = HTMLData.replaceAll("\n结束于: ", "
    结束时间:"); - HTMLData = HTMLData.replaceAll("\n订正截止日期: ", "
    订正截止日期:"); - HTMLData = HTMLData.replaceAll("\n现在时间: ", "当前时间:"); - HTMLData = HTMLData.replaceAll("\n状态:", "
    状态:"); - document.querySelector("body > div > div.mt-3 > center > div").innerHTML = HTMLData; - if (UtilityEnabled("RemoveAlerts") && document.querySelector("body > div > div.mt-3 > center").innerHTML.indexOf("尚未开始比赛") != -1) { - document.querySelector("body > div > div.mt-3 > center > a").setAttribute("href", "start_contest.php?cid=" + SearchParams.get("cid")); - } else if (UtilityEnabled("AutoRefresh")) { - addEventListener("focus", async () => { - await fetch(location.href) - .then((Response) => { - return Response.text(); - }) - .then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let Temp = ParsedDocument.querySelector("#problemset > tbody").children; - if (UtilityEnabled("ReplaceYN")) { - for (let i = 0; i < Temp.length; i++) { - let Status = Temp[i].children[0].innerText; - if (Status.indexOf("Y") != -1) { - document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_y"; - document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✓"; - } else if (Status.indexOf("N") != -1) { - document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_n"; - document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✗"; - } - } - } - }); - }); - document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); - document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); - document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML = String(document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML).replaceAll("
    ", "

    "); - - document.querySelector("#problemset > tbody").innerHTML = String(document.querySelector("#problemset > tbody").innerHTML).replaceAll(/\t ([0-9]*)      问题  ([^<]*)/g, "$2. $1"); - - document.querySelector("#problemset > tbody").innerHTML = String(document.querySelector("#problemset > tbody").innerHTML).replaceAll(/\t\*([0-9]*)      问题  ([^<]*)/g, "拓展$2. $1"); - - if (UtilityEnabled("MoreSTD") && document.querySelector("#problemset > thead > tr").innerHTML.indexOf("标程") != -1) { - let Temp = document.querySelector("#problemset > thead > tr").children; - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].innerText == "标程") { - Temp[i].remove(); - let Temp2 = document.querySelector("#problemset > tbody").children; - for (let j = 0; j < Temp2.length; j++) { - if (Temp2[j].children[i] != undefined) { - Temp2[j].children[i].remove(); - } - } - } - } - document.querySelector("#problemset > thead > tr").innerHTML += "标程"; - Temp = document.querySelector("#problemset > tbody").children; - for (let i = 0; i < Temp.length; i++) { - Temp[i].innerHTML += "打开"; - } - } - - Temp = document.querySelector("#problemset > tbody").rows; - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].childNodes[0].children.length == 0) { - Temp[i].childNodes[0].innerHTML = "
    "; - } - let PID = Temp[i].childNodes[1].innerHTML; - if (PID.substring(0, 2) == "拓展") { - PID = PID.substring(2); - } - Temp[i].children[2].children[0].target = "_blank"; - localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + i + "-PID", PID.substring(3)); - localStorage.setItem("UserScript-Problem-" + PID.substring(3) + "-Name", Temp[i].childNodes[2].innerText); - } - let CheatDiv = document.createElement("div"); - CheatDiv.style.marginTop = "20px"; - CheatDiv.style.textAlign = "left"; - document.querySelector("body > div > div.mt-3 > center").insertBefore(CheatDiv, document.querySelector("#problemset")); - if (UtilityEnabled("AutoCheat")) { - let AutoCheatButton = document.createElement("button"); - CheatDiv.appendChild(AutoCheatButton); - AutoCheatButton.className = "btn btn-outline-secondary"; - AutoCheatButton.innerText = "自动提交当年代码"; - AutoCheatButton.style.marginRight = "5px"; - AutoCheatButton.disabled = true; - let ACProblems = [], ContestProblems = []; - const UrlParams = new URLSearchParams(window.location.search); - const CID = UrlParams.get("cid"); - await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) - .then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let Temp = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); - for (let i = 0; i < Temp.length; i++) { - ACProblems.push(Number(Temp[i].substring(2, Temp[i].indexOf(",")))); - } - AutoCheatButton.disabled = false; - }); - let Rows = document.querySelector("#problemset > tbody").rows; - for (let i = 0; i < Rows.length; i++) { - ContestProblems.push(Rows[i].children[1].innerText.substring(Rows[i].children[1].innerText.indexOf('.') + 2)).toFixed; - } - AutoCheatButton.addEventListener("click", async () => { - AutoCheatButton.disabled = true; - let Submitted = false; - for (let i = 0; i < ContestProblems.length; i++) { - let PID = ContestProblems[i]; - if (ACProblems.indexOf(Number(PID)) == -1) { - console.log("Ignoring problem " + PID + " as it has not been solved yet."); - continue; - } - if (Rows[i].children[0].children[0].classList.contains("status_y")) { - console.log("Ignoring problem " + PID + " as it has already been solved in this contest."); - continue; - } - console.log("Submitting problem " + PID); - Submitted = true; - AutoCheatButton.innerHTML = "正在提交 " + PID; - let SID = 0; - await fetch("https://www.xmoj.tech/status.php?problem_id=" + PID + "&jresult=4") - .then((Result) => { - return Result.text(); - }).then((Result) => { - let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); - SID = ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; - }); - await new Promise(r => setTimeout(r, 500)); - let Code = ""; - await fetch("https://www.xmoj.tech/getsource.php?id=" + SID) - .then((Response) => { - return Response.text(); - }).then((Response) => { - Code = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); - }); - await new Promise(r => setTimeout(r, 500)); - await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, - "method": "POST", - "body": "cid=" + CID + "&pid=" + i + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" - }); - await new Promise(r => setTimeout(r, 500)); - } - if (!Submitted) { - AutoCheatButton.innerHTML = "没有可以提交的题目!"; - await new Promise(r => setTimeout(r, 1000)); - } - AutoCheatButton.disabled = false; - if (Submitted) location.reload(); else AutoCheatButton.innerHTML = "自动提交当年代码"; - }); - document.addEventListener("keydown", (Event) => { - if (Event.code === 'Enter' && (Event.metaKey || Event.ctrlKey)) { - AutoCheatButton.click(); - } - }); - } - if (UtilityEnabled("OpenAllProblem")) { - let OpenAllButton = document.createElement("button"); - OpenAllButton.className = "btn btn-outline-secondary"; - OpenAllButton.innerText = "打开全部题目"; - OpenAllButton.style.marginRight = "5px"; - CheatDiv.appendChild(OpenAllButton); - OpenAllButton.addEventListener("click", () => { - let Rows = document.querySelector("#problemset > tbody").rows; - for (let i = 0; i < Rows.length; i++) { - open(Rows[i].children[2].children[0].href, "_blank"); - } - }); - let OpenUnsolvedButton = document.createElement("button"); - OpenUnsolvedButton.className = "btn btn-outline-secondary"; - OpenUnsolvedButton.innerText = "打开未解决题目"; - CheatDiv.appendChild(OpenUnsolvedButton); - OpenUnsolvedButton.addEventListener("click", () => { - let Rows = document.querySelector("#problemset > tbody").rows; - for (let i = 0; i < Rows.length; i++) { - if (!Rows[i].children[0].children[0].classList.contains("status_y")) { - open(Rows[i].children[2].children[0].href, "_blank"); - } - } - }); - } - localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemCount", document.querySelector("#problemset > tbody").rows.length); - } - } - } else if (location.pathname == "/contestrank-oi.php") { - if (document.querySelector("#rank") == null) { - document.querySelector("body > div > div.mt-3").innerHTML = "

    比赛排名

    "; - } - if (SearchParams.get("ByUserScript") == null) { - if (document.querySelector("body > div > div.mt-3 > center > h3").innerText == "比赛排名") { - document.querySelector("#rank").innerText = "比赛暂时还没有排名"; - } else { - document.querySelector("body > div > div.mt-3 > center > h3").innerText = document.querySelector("body > div > div.mt-3 > center > h3").innerText.substring(document.querySelector("body > div > div.mt-3 > center > h3").innerText.indexOf(" -- ") + 4) + "(OI排名)"; - document.querySelector("#rank > thead > tr > :nth-child(1)").innerText = "排名"; - document.querySelector("#rank > thead > tr > :nth-child(2)").innerText = "用户"; - document.querySelector("#rank > thead > tr > :nth-child(3)").innerText = "昵称"; - document.querySelector("#rank > thead > tr > :nth-child(4)").innerText = "AC数"; - document.querySelector("#rank > thead > tr > :nth-child(5)").innerText = "得分"; - let RefreshOIRank = async () => { - await fetch(location.href) - .then((Response) => { - return Response.text() - }) - .then(async (Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - TidyTable(ParsedDocument.getElementById("rank")); - let Temp = ParsedDocument.getElementById("rank").rows; - for (var i = 1; i < Temp.length; i++) { - let MetalCell = Temp[i].cells[0]; - let Metal = document.createElement("span"); - Metal.innerText = MetalCell.innerText; - Metal.className = "badge text-bg-primary"; - MetalCell.innerText = ""; - MetalCell.appendChild(Metal); - GetUsernameHTML(Temp[i].cells[1], Temp[i].cells[1].innerText); - Temp[i].cells[2].innerHTML = Temp[i].cells[2].innerText; - Temp[i].cells[3].innerHTML = Temp[i].cells[3].innerText; - for (let j = 5; j < Temp[i].cells.length; j++) { - let InnerText = Temp[i].cells[j].innerText; - let BackgroundColor = Temp[i].cells[j].style.backgroundColor; - let Red = BackgroundColor.substring(4, BackgroundColor.indexOf(",")); - let Green = BackgroundColor.substring(BackgroundColor.indexOf(",") + 2, BackgroundColor.lastIndexOf(",")); - let Blue = BackgroundColor.substring(BackgroundColor.lastIndexOf(",") + 2, BackgroundColor.lastIndexOf(")")); - let NoData = (Red == 238 && Green == 238 && Blue == 238); - let FirstBlood = (Red == 170 && Green == 170 && Blue == 255); - let Solved = (Green == 255); - let ErrorCount = ""; - if (Solved) { - ErrorCount = (Blue == 170 ? 5 : (Blue - 51) / 32); - } else { - ErrorCount = (Blue == 22 ? 15 : (170 - Blue) / 10); - } - if (NoData) { - BackgroundColor = ""; - } else if (FirstBlood) { - BackgroundColor = "rgb(127, 127, 255)"; - } else if (Solved) { - BackgroundColor = "rgb(0, 255, 0, " + Math.max(1 / 10 * (10 - ErrorCount), 0.2) + ")"; - if (ErrorCount != 0) { - InnerText += " (" + (ErrorCount == 5 ? "4+" : ErrorCount) + ")"; - } - } else { - BackgroundColor = "rgba(255, 0, 0, " + Math.min(ErrorCount / 10 + 0.2, 1) + ")"; - if (ErrorCount != 0) { - InnerText += " (" + (ErrorCount == 15 ? "14+" : ErrorCount) + ")"; - } - } - Temp[i].cells[j].innerHTML = InnerText; - Temp[i].cells[j].style.backgroundColor = BackgroundColor; - Temp[i].cells[j].style.color = (UtilityEnabled("DarkMode") ? "white" : "black"); - } - } - document.querySelector("#rank > tbody").innerHTML = ParsedDocument.querySelector("#rank > tbody").innerHTML; - }); - }; - RefreshOIRank(); - document.title = document.querySelector("body > div.container > div > center > h3").innerText; - if (UtilityEnabled("AutoRefresh")) { - addEventListener("focus", RefreshOIRank); - } - } - } - Style.innerHTML += "td {"; - Style.innerHTML += " white-space: nowrap;"; - Style.innerHTML += "}"; - document.querySelector("body > div.container > div > center").style.paddingBottom = "10px"; - document.querySelector("body > div.container > div > center > a").style.display = "none"; - document.title = document.querySelector("body > div.container > div > center > h3").innerText; - } else if (location.pathname == "/contestrank-correct.php") { - if (document.querySelector("#rank") == null) { - document.querySelector("body > div > div.mt-3").innerHTML = "

    比赛排名

    "; - } - if (document.querySelector("body > div > div.mt-3 > center > h3").innerText == "比赛排名") { - document.querySelector("#rank").innerText = "比赛暂时还没有排名"; - } else { - if (UtilityEnabled("ResetType")) { - document.querySelector("body > div > div.mt-3 > center > h3").innerText = document.querySelector("body > div > div.mt-3 > center > h3").innerText.substring(document.querySelector("body > div > div.mt-3 > center > h3").innerText.indexOf(" -- ") + 4) + "(订正排名)"; - document.querySelector("body > div > div.mt-3 > center > a").remove(); - } - document.querySelector("#rank > thead > tr > :nth-child(1)").innerText = "排名"; - document.querySelector("#rank > thead > tr > :nth-child(2)").innerText = "用户"; - document.querySelector("#rank > thead > tr > :nth-child(3)").innerText = "昵称"; - document.querySelector("#rank > thead > tr > :nth-child(4)").innerText = "AC数"; - document.querySelector("#rank > thead > tr > :nth-child(5)").innerText = "得分"; - let RefreshCorrectRank = async () => { - await fetch(location.href) - .then((Response) => { - return Response.text() - }) - .then(async (Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - TidyTable(ParsedDocument.getElementById("rank")); - let Temp = ParsedDocument.getElementById("rank").rows; - for (var i = 1; i < Temp.length; i++) { - let MetalCell = Temp[i].cells[0]; - let Metal = document.createElement("span"); - Metal.innerText = MetalCell.innerText; - Metal.className = "badge text-bg-primary"; - MetalCell.innerText = ""; - MetalCell.appendChild(Metal); - GetUsernameHTML(Temp[i].cells[1], Temp[i].cells[1].innerText); - Temp[i].cells[2].innerHTML = Temp[i].cells[2].innerText; - Temp[i].cells[3].innerHTML = Temp[i].cells[3].innerText; - for (let j = 5; j < Temp[i].cells.length; j++) { - let InnerText = Temp[i].cells[j].innerText; - let BackgroundColor = Temp[i].cells[j].style.backgroundColor; - let Red = BackgroundColor.substring(4, BackgroundColor.indexOf(",")); - let Green = BackgroundColor.substring(BackgroundColor.indexOf(",") + 2, BackgroundColor.lastIndexOf(",")); - let Blue = BackgroundColor.substring(BackgroundColor.lastIndexOf(",") + 2, BackgroundColor.lastIndexOf(")")); - let NoData = (Red == 238 && Green == 238 && Blue == 238); - let FirstBlood = (Red == 170 && Green == 170 && Blue == 255); - let Solved = (Green == 255); - let ErrorCount = ""; - if (Solved) { - ErrorCount = (Blue == 170 ? "4+" : (Blue - 51) / 32); - } else { - ErrorCount = (Blue == 22 ? "14+" : (170 - Blue) / 10); - } - if (NoData) { - BackgroundColor = ""; - } else if (FirstBlood) { - BackgroundColor = "rgba(127, 127, 255, 0.5)"; - } else if (Solved) { - BackgroundColor = "rgba(0, 255, 0, 0.5)"; - if (ErrorCount != 0) { - InnerText += " (" + ErrorCount + ")"; - } - } else { - BackgroundColor = "rgba(255, 0, 0, 0.5)"; - if (ErrorCount != 0) { - InnerText += " (" + ErrorCount + ")"; - } - } - Temp[i].cells[j].innerHTML = InnerText; - Temp[i].cells[j].style.backgroundColor = BackgroundColor; - } - } - document.querySelector("#rank > tbody").innerHTML = ParsedDocument.querySelector("#rank > tbody").innerHTML; - }); - }; - RefreshCorrectRank(); - document.title = document.querySelector("body > div.container > div > center > h3").innerText; - if (UtilityEnabled("AutoRefresh")) { - addEventListener("focus", RefreshCorrectRank); - } - } - } else if (location.pathname == "/submitpage.php") { - document.title = "提交代码: " + (SearchParams.get("id") != null ? "题目" + Number(SearchParams.get("id")) : "比赛" + Number(SearchParams.get("cid"))); - document.querySelector("body > div > div.mt-3").innerHTML = `
    ` + `

    提交代码

    ` + (SearchParams.get("id") != null ? `题目${Number(SearchParams.get("id"))}` : `比赛${Number(SearchParams.get("cid")) + ` 题目` + String.fromCharCode(65 + parseInt(SearchParams.get("pid")))}`) + `
    - -
    - -
    - - -
    `; - if (UtilityEnabled("AutoO2")) { - document.querySelector("#enable_O2").checked = true; - } - let CodeMirrorElement; - (() => { - CodeMirrorElement = CodeMirror.fromTextArea(document.querySelector("#CodeInput"), { - lineNumbers: true, - matchBrackets: true, - mode: "text/x-c++src", - indentUnit: 4, - indentWithTabs: true, - enterMode: "keep", - tabMode: "shift", - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), - extraKeys: { - "Ctrl-Space": "autocomplete", "Ctrl-Enter": function (instance) { - Submit.click(); - } - } - }); - })(); - CodeMirrorElement.setSize("100%", "auto"); - CodeMirrorElement.getWrapperElement().style.border = "1px solid #ddd"; - - if (SearchParams.get("sid") !== null) { - await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("sid")) - .then((Response) => { - return Response.text() - }) - .then((Response) => { - CodeMirrorElement.setValue(Response.substring(0, Response.indexOf("/**************************************************************")).trim()); - }); - } - - PassCheck.addEventListener("click", async () => { - ErrorElement.style.display = "none"; - document.querySelector("#Submit").disabled = true; - document.querySelector("#Submit").value = "正在提交..."; - let o2Switch = "&enable_O2=on"; - if (!document.querySelector("#enable_O2").checked) o2Switch = ""; - await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": location.href, - "method": "POST", - "body": (SearchParams.get("id") != null ? "id=" + SearchParams.get("id") : "cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch - }).then(async (Response) => { - if (Response.redirected) { - location.href = Response.url; - } else { - const text = await Response.text(); - if (text.indexOf("没有这个比赛!") !== -1 && new URL(location.href).searchParams.get("pid") !== null) { - // Credit: https://github.com/boomzero/quicksubmit/blob/main/index.ts - // Also licensed under GPL-3.0 - const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + new URL(location.href).searchParams.get("cid")); - const res = await contestReq.text(); - if ( - contestReq.status !== 200 || - res.indexOf("比赛尚未开始或私有,不能查看题目。") !== -1 - ) { - console.error(`Failed to get contest page!`); - return; - } - const parser = new DOMParser(); - const dom = parser.parseFromString(res, "text/html"); - const contestProblems = []; - const rows = (dom.querySelector( - "#problemset > tbody", - )).rows; - for (let i = 0; i < rows.length; i++) { - contestProblems.push( - rows[i].children[1].textContent.substring(2, 6).replaceAll( - "\t", - "", - ), - ); - } - rPID = contestProblems[new URL(location.href).searchParams.get("pid")]; - if (UtilityEnabled("DebugMode")) { - console.log("Contest Problems:", contestProblems); - console.log("Real PID:", rPID); - } - ErrorElement.style.display = "block"; - ErrorMessage.style.color = "red"; - ErrorMessage.innerText = "比赛已结束, 正在尝试像题目 " + rPID + " 提交"; - console.log("比赛已结束, 正在尝试像题目 " + rPID + " 提交"); - let o2Switch = "&enable_O2=on"; - if (!document.querySelector("#enable_O2").checked) o2Switch = ""; - await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": location.href, - "method": "POST", - "body": "id=" + rPID + "&language=1&" + "source=" + encodeURIComponent(CodeMirrorElement.getValue()) + o2Switch - }).then(async (Response) => { - if (Response.redirected) { - location.href = Response.url; - } - console.log(await Response.text()); - }); - - } - if (UtilityEnabled("DebugMode")) { - console.log("Submission failed! Response:", text); - } - ErrorElement.style.display = "block"; - ErrorMessage.style.color = "red"; - ErrorMessage.innerText = "提交失败!请关闭脚本后重试!"; - Submit.disabled = false; - Submit.value = "提交"; - } - }); - }); - - Submit.addEventListener("click", async () => { - PassCheck.style.display = "none"; - ErrorElement.style.display = "none"; - document.querySelector("#Submit").disabled = true; - document.querySelector("#Submit").value = "正在检查..."; - let Source = CodeMirrorElement.getValue(); - let PID = 0; - let IOFilename = ""; - if (SearchParams.get("cid") != null && SearchParams.get("pid") != null) { - PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); - } else { - PID = SearchParams.get("id"); - } - IOFilename = localStorage.getItem("UserScript-Problem-" + PID + "-IOFilename"); - if (UtilityEnabled("IOFile") && IOFilename != null) { - if (Source.indexOf(IOFilename) == -1) { - PassCheck.style.display = ""; - ErrorElement.style.display = "block"; - if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; - ErrorMessage.innerText = "此题输入输出文件名为" + IOFilename + ",请检查是否填错"; - - let freopenText = document.createElement('small'); - if (UtilityEnabled("DarkMode")) freopenText.style.color = "white"; else freopenText.style.color = "black"; - freopenText.textContent = '\n您也可以复制freopen语句。\n'; - document.getElementById('ErrorMessage').appendChild(freopenText); - let copyFreopenButton = document.createElement("button"); - copyFreopenButton.className = "btn btn-sm btn-outline-secondary copy-btn"; - copyFreopenButton.innerText = "复制代码"; - copyFreopenButton.style.marginLeft = "10px"; - copyFreopenButton.style.marginTop = "10px"; - copyFreopenButton.style.marginBottom = "10px"; - copyFreopenButton.type = "button"; - copyFreopenButton.addEventListener("click", () => { - navigator.clipboard.writeText('\n freopen("' + IOFilename + '.in", "r", stdin);\n freopen("' + IOFilename + '.out", "w", stdout);'); - copyFreopenButton.innerText = "复制成功"; - setTimeout(() => { - copyFreopenButton.innerText = "复制代码"; - }, 1500); - }); - document.getElementById('ErrorMessage').appendChild(copyFreopenButton); - let freopenCodeField = CodeMirror(document.getElementById('ErrorMessage'), { - value: 'freopen("' + IOFilename + '.in", "r", stdin);\nfreopen("' + IOFilename + '.out", "w", stdout);', - mode: 'text/x-c++src', - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), - readOnly: true, - lineNumbers: true - }); - freopenCodeField.setSize("100%", "auto"); - document.querySelector("#Submit").disabled = false; - document.querySelector("#Submit").value = "提交"; - return false; - } else if (RegExp("//.*freopen").test(Source)) { - PassCheck.style.display = ""; - ErrorElement.style.display = "block"; - if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; - ErrorMessage.innerText = "请不要注释freopen语句"; - document.querySelector("#Submit").disabled = false; - document.querySelector("#Submit").value = "提交"; - return false; - } - } - if (Source == "") { - PassCheck.style.display = ""; - ErrorElement.style.display = "block"; - if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; - ErrorMessage.innerText = "源代码为空"; - document.querySelector("#Submit").disabled = false; - document.querySelector("#Submit").value = "提交"; - return false; - } - if (UtilityEnabled("CompileError")) { - let ResponseData = await new Promise((Resolve) => { - GM_xmlhttpRequest({ - method: "POST", url: "https://cppinsights.io/api/v1/transform", headers: { - "content-type": "application/json;charset=UTF-8" - }, referrer: "https://cppinsights.io/", data: JSON.stringify({ - "insightsOptions": ["cpp14"], "code": Source - }), onload: (Response) => { - Resolve(Response); - } - }); - }); - let Response = JSON.parse(ResponseData.responseText); - if (Response.returncode) { - PassCheck.style.display = ""; - ErrorElement.style.display = "block"; - if (UtilityEnabled("DarkMode")) ErrorMessage.style.color = "yellow"; else ErrorMessage.style.color = "red"; - ErrorMessage.innerText = "编译错误:\n" + Response.stderr.trim(); - document.querySelector("#Submit").disabled = false; - document.querySelector("#Submit").value = "提交"; - return false; - } else { - PassCheck.click(); - } - } else { - PassCheck.click(); - } - }); - } else if (location.pathname == "/modifypage.php") { - if (SearchParams.get("ByUserScript") != null) { - document.title = "XMOJ-Script 更新日志"; - document.querySelector("body > div > div.mt-3").innerHTML = ""; - await fetch(ServerURL + "/Update.json", {cache: "no-cache"}) - .then((Response) => { - return Response.json(); - }) - .then((Response) => { - for (let i = Object.keys(Response.UpdateHistory).length - 1; i >= 0; i--) { - let Version = Object.keys(Response.UpdateHistory)[i]; - let Data = Response.UpdateHistory[Version]; - let UpdateDataCard = document.createElement("div"); - document.querySelector("body > div > div.mt-3").appendChild(UpdateDataCard); - UpdateDataCard.className = "card mb-3"; - if (Data.Prerelease) UpdateDataCard.classList.add("text-secondary"); - let UpdateDataCardBody = document.createElement("div"); - UpdateDataCard.appendChild(UpdateDataCardBody); - UpdateDataCardBody.className = "card-body"; - let UpdateDataCardTitle = document.createElement("h5"); - UpdateDataCardBody.appendChild(UpdateDataCardTitle); - UpdateDataCardTitle.className = "card-title"; - UpdateDataCardTitle.innerText = Version; - if (Data.Prerelease) { - UpdateDataCardTitle.innerHTML += "(预览版)"; - } - let UpdateDataCardSubtitle = document.createElement("h6"); - UpdateDataCardBody.appendChild(UpdateDataCardSubtitle); - UpdateDataCardSubtitle.className = "card-subtitle mb-2 text-muted"; - UpdateDataCardSubtitle.innerHTML = GetRelativeTime(Data.UpdateDate); - let UpdateDataCardText = document.createElement("p"); - UpdateDataCardBody.appendChild(UpdateDataCardText); - UpdateDataCardText.className = "card-text"; - //release notes - if (Data.Notes != undefined) { - UpdateDataCardText.innerHTML = Data.Notes; - } - let UpdateDataCardList = document.createElement("ul"); - UpdateDataCardText.appendChild(UpdateDataCardList); - UpdateDataCardList.className = "list-group list-group-flush"; - for (let j = 0; j < Data.UpdateContents.length; j++) { - let UpdateDataCardListItem = document.createElement("li"); - UpdateDataCardList.appendChild(UpdateDataCardListItem); - UpdateDataCardListItem.className = "list-group-item"; - UpdateDataCardListItem.innerHTML = "(" + "#" + Data.UpdateContents[j].PR + ") " + Data.UpdateContents[j].Description; - } - let UpdateDataCardLink = document.createElement("a"); - UpdateDataCardBody.appendChild(UpdateDataCardLink); - UpdateDataCardLink.className = "card-link"; - UpdateDataCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script/releases/tag/" + Version; - UpdateDataCardLink.target = "_blank"; - UpdateDataCardLink.innerText = "查看该版本"; - } - }); - } else { - document.title = "修改账号"; - let Nickname = document.getElementsByName("nick")[0].value; - let School = document.getElementsByName("school")[0].value; - let EmailAddress = document.getElementsByName("email")[0].value; - let CodeforcesAccount = document.getElementsByName("acc_cf")[0].value; - let AtcoderAccount = document.getElementsByName("acc_atc")[0].value; - let USACOAccount = document.getElementsByName("acc_usaco")[0].value; - let LuoguAccount = document.getElementsByName("acc_luogu")[0].value; - document.querySelector("body > div > div").innerHTML = `
    -
    -
    -
    -
    -
    -
    - - 修改头像 -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - -
    `; - document.getElementById("Nickname").value = Nickname; - document.getElementById("School").value = School; - document.getElementById("EmailAddress").value = EmailAddress; - document.getElementById("CodeforcesAccount").value = CodeforcesAccount; - document.getElementById("AtcoderAccount").value = AtcoderAccount; - document.getElementById("USACOAccount").value = USACOAccount; - document.getElementById("LuoguAccount").value = LuoguAccount; - RequestAPI("GetBadge", { - "UserID": String(CurrentUsername) - }, (Response) => { - if (Response.Success) { - BadgeRow.style.display = ""; - BadgeContent.value = Response.Data.Content; - BadgeBackgroundColor.value = Response.Data.BackgroundColor; - BadgeColor.value = Response.Data.Color; - let Temp = []; - for (let i = 0; i < localStorage.length; i++) { - if (localStorage.key(i).startsWith("UserScript-User-" + CurrentUsername + "-Badge-")) { - Temp.push(localStorage.key(i)); - } - } - for (let i = 0; i < Temp.length; i++) { - localStorage.removeItem(Temp[i]); - } - } - }); - ModifyInfo.addEventListener("click", async () => { - ModifyInfo.disabled = true; - ModifyInfo.querySelector("span").style.display = ""; - ErrorElement.style.display = "none"; - SuccessElement.style.display = "none"; - let BadgeContent = document.querySelector("#BadgeContent").value; - let BadgeBackgroundColor = document.querySelector("#BadgeBackgroundColor").value; - let BadgeColor = document.querySelector("#BadgeColor").value; - await new Promise((Resolve) => { - RequestAPI("EditBadge", { - "UserID": String(CurrentUsername), - "Content": String(BadgeContent), - "BackgroundColor": String(BadgeBackgroundColor), - "Color": String(BadgeColor) - }, (Response) => { - if (Response.Success) { - Resolve(); - } else { - ModifyInfo.disabled = false; - ModifyInfo.querySelector("span").style.display = "none"; - ErrorElement.style.display = "block"; - ErrorElement.innerText = Response.Message; - } - }); - }); - let Nickname = document.querySelector("#Nickname").value; - let OldPassword = document.querySelector("#OldPassword").value; - let NewPassword = document.querySelector("#NewPassword").value; - let NewPasswordAgain = document.querySelector("#NewPasswordAgain").value; - let School = document.querySelector("#School").value; - let EmailAddress = document.querySelector("#EmailAddress").value; - let CodeforcesAccount = document.querySelector("#CodeforcesAccount").value; - let AtcoderAccount = document.querySelector("#AtcoderAccount").value; - let USACOAccount = document.querySelector("#USACOAccount").value; - let LuoguAccount = document.querySelector("#LuoguAccount").value; - await fetch("https://www.xmoj.tech/modify.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": location.href, - "method": "POST", - "body": "nick=" + encodeURIComponent(Nickname) + "&" + "opassword=" + encodeURIComponent(OldPassword) + "&" + "npassword=" + encodeURIComponent(NewPassword) + "&" + "rptpassword=" + encodeURIComponent(NewPasswordAgain) + "&" + "school=" + encodeURIComponent(School) + "&" + "email=" + encodeURIComponent(EmailAddress) + "&" + "acc_cf=" + encodeURIComponent(CodeforcesAccount) + "&" + "acc_atc=" + encodeURIComponent(AtcoderAccount) + "&" + "acc_usaco=" + encodeURIComponent(USACOAccount) + "&" + "acc_luogu=" + encodeURIComponent(LuoguAccount) - }); - ModifyInfo.disabled = false; - ModifyInfo.querySelector("span").style.display = "none"; - SuccessElement.style.display = "block"; - }); - if (UtilityEnabled("ExportACCode")) { - let ExportACCode = document.createElement("button"); - document.querySelector("body > div.container > div").appendChild(ExportACCode); - ExportACCode.innerText = "导出AC代码"; - ExportACCode.className = "btn btn-outline-secondary"; - ExportACCode.addEventListener("click", () => { - ExportACCode.disabled = true; - ExportACCode.innerText = "正在导出..."; - let Request = new XMLHttpRequest(); - Request.addEventListener("readystatechange", () => { - if (Request.readyState == 4) { - if (Request.status == 200) { - let Response = Request.responseText; - let ACCode = Response.split("------------------------------------------------------\r\n"); - let ScriptElement = document.createElement("script"); - ScriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; - document.head.appendChild(ScriptElement); - ScriptElement.onload = () => { - var Zip = new JSZip(); - for (let i = 0; i < ACCode.length; i++) { - let CurrentCode = ACCode[i]; - if (CurrentCode != "") { - let CurrentQuestionID = CurrentCode.substring(7, 11); - CurrentCode = CurrentCode.substring(14); - CurrentCode = CurrentCode.replaceAll("\r", ""); - Zip.file(CurrentQuestionID + ".cpp", CurrentCode); - } - } - ExportACCode.innerText = "正在生成压缩包……"; - Zip.generateAsync({type: "blob"}) - .then(function (Content) { - saveAs(Content, "ACCodes.zip"); - ExportACCode.innerText = "AC代码导出成功"; - ExportACCode.disabled = false; - setTimeout(() => { - ExportACCode.innerText = "导出AC代码"; - }, 1000); - }); - }; - } else { - ExportACCode.disabled = false; - ExportACCode.innerText = "AC代码导出失败"; - setTimeout(() => { - ExportACCode.innerText = "导出AC代码"; - }, 1000); - } - } - }); - Request.open("GET", "https://www.xmoj.tech/export_ac_code.php", true); - Request.send(); - }); - } - } - } else if (location.pathname == "/userinfo.php") { - if (SearchParams.get("ByUserScript") === null) { - if (UtilityEnabled("RemoveUseless")) { - let Temp = document.getElementById("submission").childNodes; - for (let i = 0; i < Temp.length; i++) { - Temp[i].remove(); - } - } - eval(document.querySelector("body > script:nth-child(5)").innerHTML); - document.querySelector("#statics > tbody > tr:nth-child(1)").remove(); - - let Temp = document.querySelector("#statics > tbody").children; - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].children[0] != undefined) { - if (Temp[i].children[0].innerText == "Statistics") { - Temp[i].children[0].innerText = "统计"; - } else if (Temp[i].children[0].innerText == "Email:") { - Temp[i].children[0].innerText = "电子邮箱"; - } - Temp[i].children[1].removeAttribute("align"); - } - } - - Temp = document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)").childNodes; - let ACProblems = []; - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].tagName == "A" && Temp[i].href.indexOf("problem.php?id=") != -1) { - ACProblems.push(Number(Temp[i].innerText.trim())); - } - } - document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)").remove(); - - let UserID, UserNick; - [UserID, UserNick] = document.querySelector("#statics > caption").childNodes[0].data.trim().split("--"); - document.querySelector("#statics > caption").remove(); - document.title = "用户 " + UserID + " 的个人中心"; - let Row = document.createElement("div"); - Row.className = "row"; - let LeftDiv = document.createElement("div"); - LeftDiv.className = "col-md-5"; - Row.appendChild(LeftDiv); - - let LeftTopDiv = document.createElement("div"); - LeftTopDiv.className = "row mb-2"; - LeftDiv.appendChild(LeftTopDiv); - let AvatarContainer = document.createElement("div"); - AvatarContainer.classList.add("col-auto"); - let AvatarElement = document.createElement("img"); - let UserEmailHash = (await GetUserInfo(UserID)).EmailHash; - if (UserEmailHash == undefined) { - AvatarElement.src = `https://cravatar.cn/avatar/00000000000000000000000000000000?d=mp&f=y`; - } else { - AvatarElement.src = `https://cravatar.cn/avatar/${UserEmailHash}?d=retro`; - } - AvatarElement.classList.add("rounded", "me-2"); - AvatarElement.style.height = "120px"; - AvatarContainer.appendChild(AvatarElement); - LeftTopDiv.appendChild(AvatarContainer); - - let UserInfoElement = document.createElement("div"); - UserInfoElement.classList.add("col-auto"); - UserInfoElement.style.lineHeight = "40px"; - UserInfoElement.innerHTML += "用户名:" + UserID + "
    "; - UserInfoElement.innerHTML += "昵称:" + UserNick + "
    "; - if (UtilityEnabled("Rating")) { - UserInfoElement.innerHTML += "评分:" + ((await GetUserInfo(UserID)).Rating) + "
    "; - } - // Create a placeholder for the last online time - let lastOnlineElement = document.createElement('div'); - lastOnlineElement.innerHTML = "最后在线:加载中...
    "; - UserInfoElement.appendChild(lastOnlineElement); - let BadgeInfo = await GetUserBadge(UserID); - if (IsAdmin) { - if (BadgeInfo.Content !== "") { - let DeleteBadgeButton = document.createElement("button"); - DeleteBadgeButton.className = "btn btn-outline-danger btn-sm"; - DeleteBadgeButton.innerText = "删除标签"; - DeleteBadgeButton.addEventListener("click", async () => { - if (confirm("您确定要删除此标签吗?")) { - RequestAPI("DeleteBadge", { - "UserID": UserID - }, (Response) => { - if (UtilityEnabled("DebugMode")) console.log(Response); - if (Response.Success) { - let Temp = []; - for (let i = 0; i < localStorage.length; i++) { - if (localStorage.key(i).startsWith("UserScript-User-" + UserID + "-Badge-")) { - Temp.push(localStorage.key(i)); - } - } - for (let i = 0; i < Temp.length; i++) { - localStorage.removeItem(Temp[i]); - } - window.location.reload(); - } else { - SmartAlert(Response.Message); - } - }); - } - }); - UserInfoElement.appendChild(DeleteBadgeButton); - } else { - let AddBadgeButton = document.createElement("button"); - AddBadgeButton.className = "btn btn-outline-primary btn-sm"; - AddBadgeButton.innerText = "添加标签"; - AddBadgeButton.addEventListener("click", async () => { - RequestAPI("NewBadge", { - "UserID": UserID - }, (Response) => { - if (Response.Success) { - let Temp = []; - for (let i = 0; i < localStorage.length; i++) { - if (localStorage.key(i).startsWith("UserScript-User-" + UserID + "-Badge-")) { - Temp.push(localStorage.key(i)); - } - } - for (let i = 0; i < Temp.length; i++) { - localStorage.removeItem(Temp[i]); - } - window.location.reload(); - } else { - SmartAlert(Response.Message); - } - }); - }); - UserInfoElement.appendChild(AddBadgeButton); - } - } - RequestAPI("LastOnline", {"Username": UserID}, (result) => { - if (result.Success) { - if (UtilityEnabled("DebugMode")) { - console.log('lastOnline:' + result.Data.logintime); - } - lastOnlineElement.innerHTML = "最后在线:" + GetRelativeTime(result.Data.logintime) + "
    "; - } else { - lastOnlineElement.innerHTML = "最后在线:近三个月内从未
    "; - } - }); - LeftTopDiv.appendChild(UserInfoElement); - LeftDiv.appendChild(LeftTopDiv); - - let LeftTable = document.querySelector("body > div > div > center > table"); - LeftDiv.appendChild(LeftTable); - let RightDiv = document.createElement("div"); - RightDiv.className = "col-md-7"; - Row.appendChild(RightDiv); - RightDiv.innerHTML = "
    已解决题目
    "; - for (let i = 0; i < ACProblems.length; i++) { - RightDiv.innerHTML += "" + ACProblems[i] + " "; - } - document.querySelector("body > div > div").innerHTML = ""; - document.querySelector("body > div > div").appendChild(Row); - } else { - document.title = "上传标程"; - document.querySelector("body > div > div.mt-3").innerHTML = ` - -
    -
    0%
    -
    -

    - 您必须要上传标程以后才能使用“查看标程”功能。点击“上传标程”按钮以后,系统会自动上传标程,请您耐心等待。
    - 首次上传标程可能会比较慢,请耐心等待。后续将可以自动上传AC代码。
    - 系统每过30天会自动提醒您上传标程,您必须要上传标程,否则将会被禁止使用“查看标程”功能。
    -

    `; - UploadStd.addEventListener("click", async () => { - UploadStd.disabled = true; - ErrorElement.style.display = "none"; - ErrorElement.innerText = ""; - UploadProgress.classList.remove("bg-success"); - UploadProgress.classList.remove("bg-warning"); - UploadProgress.classList.remove("bg-danger"); - UploadProgress.classList.add("progress-bar-animated"); - UploadProgress.style.width = "0%"; - UploadProgress.innerText = "0%"; - let ACList = []; - await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) - .then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let ScriptData = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText; - ScriptData = ScriptData.substr(ScriptData.indexOf("}") + 1).trim(); - ScriptData = ScriptData.split(";"); - for (let i = 0; i < ScriptData.length; i++) { - ACList.push(Number(ScriptData[i].substring(2, ScriptData[i].indexOf(",")))); - } - }); - RequestAPI("GetStdList", {}, async (Result) => { - if (Result.Success) { - let StdList = Result.Data.StdList; - for (let i = 0; i < ACList.length; i++) { - if (StdList.indexOf(ACList[i]) === -1 && ACList[i] !== 0) { - await new Promise((Resolve) => { - RequestAPI("UploadStd", { - "ProblemID": Number(ACList[i]) - }, (Result) => { - if (!Result.Success) { - ErrorElement.style.display = "block"; - ErrorElement.innerText += Result.Message + "\n"; - UploadProgress.classList.add("bg-warning"); - } - UploadProgress.innerText = (i / ACList.length * 100).toFixed(1) + "% (" + ACList[i] + ")"; - UploadProgress.style.width = (i / ACList.length * 100) + "%"; - Resolve(); - }); - }); - } - } - UploadProgress.classList.add("bg-success"); - UploadProgress.classList.remove("progress-bar-animated"); - UploadProgress.innerText = "100%"; - UploadProgress.style.width = "100%"; - UploadStd.disabled = false; - localStorage.setItem("UserScript-LastUploadedStdTime", new Date().getTime()); - } else { - ErrorElement.style.display = "block"; - ErrorElement.innerText = Result.Message; - UploadStd.disabled = false; - } - }); - }); - } - } else if (location.pathname == "/comparesource.php") { - if (UtilityEnabled("CompareSource")) { - if (location.search == "") { - document.querySelector("body > div.container > div").innerHTML = ""; - let LeftCodeText = document.createElement("span"); - document.querySelector("body > div.container > div").appendChild(LeftCodeText); - LeftCodeText.innerText = "左侧代码的运行编号:"; - let LeftCode = document.createElement("input"); - document.querySelector("body > div.container > div").appendChild(LeftCode); - LeftCode.classList.add("form-control"); - LeftCode.style.width = "40%"; - LeftCode.style.marginBottom = "5px"; - let RightCodeText = document.createElement("span"); - document.querySelector("body > div.container > div").appendChild(RightCodeText); - RightCodeText.innerText = "右侧代码的运行编号:"; - let RightCode = document.createElement("input"); - document.querySelector("body > div.container > div").appendChild(RightCode); - RightCode.classList.add("form-control"); - RightCode.style.width = "40%"; - RightCode.style.marginBottom = "5px"; - let CompareButton = document.createElement("button"); - document.querySelector("body > div.container > div").appendChild(CompareButton); - CompareButton.innerText = "比较"; - CompareButton.className = "btn btn-primary"; - CompareButton.addEventListener("click", () => { - location.href = "https://www.xmoj.tech/comparesource.php?left=" + Number(LeftCode.value) + "&right=" + Number(RightCode.value); - }); - } else { - document.querySelector("body > div > div.mt-3").innerHTML = ` -
    - - -
    -
    `; - - let LeftCode = ""; - await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("left")) - .then((Response) => { - return Response.text(); - }).then((Response) => { - LeftCode = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); - }); - let RightCode = ""; - await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("right")) - .then((Response) => { - return Response.text(); - }).then((Response) => { - RightCode = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); - }); - - let MergeViewElement = CodeMirror.MergeView(CompareElement, { - value: LeftCode, - origLeft: null, - orig: RightCode, - lineNumbers: true, - mode: "text/x-c++src", - collapseIdentical: "true", - readOnly: true, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), - revertButtons: false, - ignoreWhitespace: true - }); - - IgnoreWhitespace.addEventListener("change", () => { - MergeViewElement.ignoreWhitespace = ignorews.checked; - }); - } - } - } else if (location.pathname == "/loginpage.php") { - if (UtilityEnabled("NewBootstrap")) { - document.querySelector("#login").innerHTML = `
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    `; - } - let ErrorText = document.createElement("div"); - ErrorText.style.color = "red"; - ErrorText.style.marginBottom = "5px"; - document.querySelector("#login").appendChild(ErrorText); - let LoginButton = document.getElementsByName("submit")[0]; - LoginButton.addEventListener("click", async () => { - let Username = document.getElementsByName("user_id")[0].value; - let Password = document.getElementsByName("password")[0].value; - if (Username == "" || Password == "") { - ErrorText.innerText = "用户名或密码不能为空"; - } else { - await fetch("https://www.xmoj.tech/login.php", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: "user_id=" + encodeURIComponent(Username) + "&password=" + hex_md5(Password) - }) - .then((Response) => { - return Response.text(); - }) - .then(async (Response) => { - if (UtilityEnabled("LoginFailed")) { - if (Response.indexOf("history.go(-2);") != -1) { - if (UtilityEnabled("SavePassword")) { - await storeCredential(Username, Password); - } - let NewPage = localStorage.getItem("UserScript-LastPage"); - if (NewPage == null) { - NewPage = "https://www.xmoj.tech/index.php"; - } - location.href = NewPage; - } else { - if (UtilityEnabled("SavePassword")) { - clearCredential(); - } - Response = Response.substring(Response.indexOf("alert('") + 7); - Response = Response.substring(0, Response.indexOf("');")); - if (Response == "UserName or Password Wrong!") { - ErrorText.innerText = "用户名或密码错误!"; - } else { - ErrorText.innerText = Response; - } - } - } else { - document.innerHTML = Response; - } - }); - } - }); - if (UtilityEnabled("SavePassword")) { - (async () => { - let Credential = await getCredential(); - if (Credential) { - document.querySelector("#login > div:nth-child(1) > div > input").value = Credential.id; - document.querySelector("#login > div:nth-child(2) > div > input").value = Credential.password; - LoginButton.click(); - } - })(); - } - } else if (location.pathname == "/contest_video.php" || location.pathname == "/problem_video.php") { - let ScriptData = document.querySelector("body > div > div.mt-3 > center > script").innerHTML; - if (document.getElementById("J_prismPlayer0").innerHTML != "") { - document.getElementById("J_prismPlayer0").innerHTML = ""; - if (player) { - player.dispose(); - } - eval(ScriptData); - } - if (UtilityEnabled("DownloadPlayback")) { - ScriptData = ScriptData.substring(ScriptData.indexOf("{")); - ScriptData = ScriptData.substring(0, ScriptData.indexOf("}") + 1); - ScriptData = ScriptData.replace(/([a-zA-Z0-9]+) ?:/g, "\"$1\":"); - ScriptData = ScriptData.replace(/'/g, "\""); - let VideoData = JSON.parse(ScriptData); - let RandomUUID = () => { - let t = "0123456789abcdef"; - let e = []; - for (let r = 0; r < 36; r++) e[r] = t.substr(Math.floor(16 * Math.random()), 1); - e[14] = "4"; - e[19] = t.substr(3 & e[19] | 8, 1); - e[8] = e[13] = e[18] = e[23] = "-"; - return e.join(""); - }; - let URLParams = new URLSearchParams({ - "AccessKeyId": VideoData.accessKeyId, - "Action": "GetPlayInfo", - "VideoId": VideoData.vid, - "Formats": "", - "AuthTimeout": 7200, - "Rand": RandomUUID(), - "SecurityToken": VideoData.securityToken, - "StreamType": "video", - "Format": "JSON", - "Version": "2017-03-21", - "SignatureMethod": "HMAC-SHA1", - "SignatureVersion": "1.0", - "SignatureNonce": RandomUUID(), - "PlayerVersion": "2.9.3", - "Channel": "HTML5" - }); - URLParams.sort(); - await fetch("https://vod." + VideoData.region + ".aliyuncs.com/?" + URLParams.toString() + "&Signature=" + encodeURIComponent(CryptoJS.HmacSHA1("GET&%2F&" + encodeURIComponent(URLParams.toString()), VideoData.accessKeySecret + "&").toString(CryptoJS.enc.Base64))) - .then((Response) => { - return Response.json(); - }) - .then((Response) => { - let DownloadButton = document.createElement("a"); - DownloadButton.className = "btn btn-outline-secondary"; - DownloadButton.innerText = "下载"; - DownloadButton.href = Response.PlayInfoList.PlayInfo[0].PlayURL; - DownloadButton.download = Response.VideoBase.Title; - document.querySelector("body > div > div.mt-3 > center").appendChild(DownloadButton); - }); - } - } else if (location.pathname == "/reinfo.php") { - document.title = "测试点信息: " + SearchParams.get("sid"); - if (document.querySelector("#results > div") == undefined) { - document.querySelector("#results").parentElement.innerHTML = "没有测试点信息"; - } else { - for (let i = 0; i < document.querySelector("#results > div").children.length; i++) { - let CurrentElement = document.querySelector("#results > div").children[i].children[0].children[0].children[0]; - let Temp = CurrentElement.innerText.substring(0, CurrentElement.innerText.length - 2).split("/"); - CurrentElement.innerText = TimeToStringTime(Temp[0]) + "/" + SizeToStringSize(Temp[1]); - } - if (document.getElementById("apply_data")) { - let ApplyDiv = document.getElementById("apply_data").parentElement; - console.log("启动!!!"); - if (UtilityEnabled("ApplyData")) { - let GetDataButton = document.createElement("button"); - GetDataButton.className = "ms-2 btn btn-outline-secondary"; - GetDataButton.innerText = "获取数据"; - console.log("按钮创建成功"); - ApplyDiv.appendChild(GetDataButton); - GetDataButton.addEventListener("click", async () => { - GetDataButton.disabled = true; - GetDataButton.innerText = "正在获取数据..."; - let PID = localStorage.getItem("UserScript-Solution-" + SearchParams.get("sid") + "-Problem"); - if (PID == null) { - GetDataButton.innerText = "失败! 无法获取PID"; - GetDataButton.disabled = false; - await new Promise((resolve) => { - setTimeout(resolve, 800); - }); - GetDataButton.innerText = "获取数据"; - return; - } - let Code = ""; - if (localStorage.getItem(`UserScript-Problem-${PID}-IOFilename`) !== null) { - Code = `#define IOFile "${localStorage.getItem(`UserScript-Problem-${PID}-IOFilename`)}"\n`; - } - Code += `//XMOJ-Script 获取数据代码 - #include -using namespace std; -string Base64Encode(string Input) -{ - const string Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - string Output; - for (int i = 0; i < Input.length(); i += 3) - { - Output.push_back(i + 0 > Input.length() ? '=' : Base64Chars[(Input[i + 0] & 0xfc) >> 2]); - Output.push_back(i + 1 > Input.length() ? '=' : Base64Chars[((Input[i + 0] & 0x03) << 4) + ((Input[i + 1] & 0xf0) >> 4)]); - Output.push_back(i + 2 > Input.length() ? '=' : Base64Chars[((Input[i + 1] & 0x0f) << 2) + ((Input[i + 2] & 0xc0) >> 6)]); - Output.push_back(i + 3 > Input.length() ? '=' : Base64Chars[Input[i + 2] & 0x3f]); - } - return Output; -} -int main() -{ -#ifdef IOFile - freopen(IOFile ".in", "r", stdin); - freopen(IOFile ".out", "w", stdout); -#endif - string Input; - while (1) - { - char Data = getchar(); - if (Data == EOF) - break; - Input.push_back(Data); - } - throw logic_error("[" + Base64Encode(Input.c_str()) + "]"); - return 0; -}`; - - await fetch("https://www.xmoj.tech/submit.php", { - "headers": { - "content-type": "application/x-www-form-urlencoded" - }, - "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, - "method": "POST", - "body": "id=" + PID + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" - }); - - let SID = await fetch("https://www.xmoj.tech/status.php").then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - return ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; - }); - - await new Promise((Resolve) => { - let Interval = setInterval(async () => { - await fetch("status-ajax.php?solution_id=" + SID).then((Response) => { - return Response.text(); - }).then((Response) => { - if (Response.split(",")[0] >= 4) { - clearInterval(Interval); - Resolve(); - } - }); - }, 500); - }); - - await fetch(`https://www.xmoj.tech/reinfo.php?sid=${SID}`).then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let ErrorData = ParsedDocument.getElementById("errtxt").innerText; - let MatchResult = ErrorData.match(/\what\(\): \[([A-Za-z0-9+\/=]+)\]/g); - if (MatchResult === null) { - GetDataButton.innerText = "获取数据失败"; - GetDataButton.disabled = false; - return; - } - for (let i = 0; i < MatchResult.length; i++) { - let Data = CryptoJS.enc.Base64.parse(MatchResult[i].substring(10, MatchResult[i].length - 1)).toString(CryptoJS.enc.Utf8); - ApplyDiv.appendChild(document.createElement("hr")); - ApplyDiv.appendChild(document.createTextNode("数据" + (i + 1) + ":")); - let CodeElement = document.createElement("div"); - ApplyDiv.appendChild(CodeElement); - CodeMirror(CodeElement, { - value: Data, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), - lineNumbers: true, - readOnly: true - }).setSize("100%", "auto"); - } - GetDataButton.innerText = "获取数据成功"; - GetDataButton.disabled = false; - }); - }); - } - document.getElementById("apply_data").addEventListener("click", () => { - let ApplyElements = document.getElementsByClassName("data"); - for (let i = 0; i < ApplyElements.length; i++) { - ApplyElements[i].style.display = (ApplyElements[i].style.display == "block" ? "" : "block"); - } - }); - } - let ApplyElements = document.getElementsByClassName("data"); - for (let i = 0; i < ApplyElements.length; i++) { - ApplyElements[i].addEventListener("click", async () => { - await fetch("https://www.xmoj.tech/data_distribute_ajax_apply.php", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: "user_id=" + CurrentUsername + "&" + "solution_id=" + SearchParams.get("sid") + "&" + "name=" + ApplyElements[i].getAttribute("name") - }).then((Response) => { - return Response.json(); - }).then((Response) => { - ApplyElements[i].innerText = Response.msg; - setTimeout(() => { - ApplyElements[i].innerText = "申请数据"; - }, 1000); - }); - }); - } - } - } else if (location.pathname == "/downloads.php") { - let SoftwareList = document.querySelector("body > div > ul"); - SoftwareList.remove(); - SoftwareList = document.createElement("ul"); - SoftwareList.className = "software_list"; - let Container = document.createElement("div"); - document.querySelector("body > div").appendChild(Container); - Container.className = "mt-3"; - Container.appendChild(SoftwareList); - if (UtilityEnabled("NewDownload")) { - let Softwares = [{ - "Name": "Bloodshed Dev-C++", - "Image": "https://a.fsdn.com/allura/p/dev-cpp/icon", - "URL": "https://sourceforge.net/projects/dev-cpp/" - }, { - "Name": "DevC++ 5.11 TDM-GCC 4.9.2", - "Image": "https://www.xmoj.tech/image/devcpp.png", - "URL": "https://www.xmoj.tech/downloads/Dev-Cpp+5.11+TDM-GCC+4.9.2+Setup.exe" - }, { - "Name": "Orwell Dev-C++", - "Image": "https://a.fsdn.com/allura/p/orwelldevcpp/icon", - "URL": "https://sourceforge.net/projects/orwelldevcpp/" - }, { - "Name": "Embarcadero Dev-C++", - "Image": "https://a.fsdn.com/allura/s/embarcadero-dev-cpp/icon", - "URL": "https://sourceforge.net/software/product/Embarcadero-Dev-Cpp/" - }, { - "Name": "RedPanda C++", - "Image": "https://a.fsdn.com/allura/p/redpanda-cpp/icon", - "URL": "https://sourceforge.net/projects/redpanda-cpp/" - }, { - "Name": "CP Editor", - "Image": "https://a.fsdn.com/allura/mirror/cp-editor/icon?c35437565079e4135a985ba557ef2fdbe97de6bafb27aceafd76bc54490c26e3?&w=90", - "URL": "https://cpeditor.org/zh/download/" - }, { - "Name": "CLion", - "Image": "https://resources.jetbrains.com/storage/products/company/brand/logos/CLion_icon.png", - "URL": "https://www.jetbrains.com/clion/download" - }, { - "Name": "CP Editor", - "Image": "https://a.fsdn.com/allura/mirror/cp-editor/icon", - "URL": "https://sourceforge.net/projects/cp-editor.mirror/" - }, { - "Name": "Code::Blocks", - "Image": "https://a.fsdn.com/allura/p/codeblocks/icon", - "URL": "https://sourceforge.net/projects/codeblocks/" - }, { - "Name": "Visual Studio Code", - "Image": "https://code.visualstudio.com/favicon.ico", - "URL": "https://code.visualstudio.com/Download" - }, { - "Name": "Lazarus", - "Image": "https://a.fsdn.com/allura/p/lazarus/icon", - "URL": "https://sourceforge.net/projects/lazarus/" - }, { - "Name": "Geany", - "Image": "https://www.geany.org/static/img/geany.svg", - "URL": "https://www.geany.org/download/releases/" - }, { - "Name": "NOI Linux", - "Image": "https://www.noi.cn/upload/resources/image/2021/07/16/163780.jpg", - "URL": "https://www.noi.cn/gynoi/jsgz/2021-07-16/732450.shtml" - }, { - "Name": "VirtualBox", - "Image": "https://www.virtualbox.org/graphics/vbox_logo2_gradient.png", - "URL": "https://www.virtualbox.org/wiki/Downloads" - }, { - "Name": "MinGW", - "Image": "https://www.mingw-w64.org/logo.svg", - "URL": "https://sourceforge.net/projects/mingw/" - }]; - for (let i = 0; i < Softwares.length; i++) { - SoftwareList.innerHTML += "
  • " + "" + "
    " + "
    " + "\"点击下载\"" + "
    " + "
    " + Softwares[i].Name + "
    " + "
    " + "
    " + "
  • "; - } - } - } else if (location.pathname == "/problemstatus.php") { - document.querySelector("body > div > div.mt-3 > center").insertBefore(document.querySelector("#statics"), document.querySelector("body > div > div.mt-3 > center > table")); - document.querySelector("body > div > div.mt-3 > center").insertBefore(document.querySelector("#problemstatus"), document.querySelector("body > div > div.mt-3 > center > table")); - - document.querySelector("body > div > div.mt-3 > center > table:nth-child(3)").remove(); - let Temp = document.querySelector("#statics").rows; - for (let i = 0; i < Temp.length; i++) { - Temp[i].removeAttribute("class"); - } - - document.querySelector("#problemstatus > thead > tr").innerHTML = document.querySelector("#problemstatus > thead > tr").innerHTML.replaceAll("td", "th"); - document.querySelector("#problemstatus > thead > tr > th:nth-child(2)").innerText = "运行编号"; - document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); - document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); - document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); - document.querySelector("#problemstatus > thead > tr > th:nth-child(4)").remove(); - Temp = document.querySelector("#problemstatus > thead > tr").children; - for (let i = 0; i < Temp.length; i++) { - Temp[i].removeAttribute("class"); - } - Temp = document.querySelector("#problemstatus > tbody").children; - for (let i = 0; i < Temp.length; i++) { - if (Temp[i].children[5].children[0] != null) { - Temp[i].children[1].innerHTML = `${escapeHTML(Temp[i].children[1].innerText.trim())}`; - } - GetUsernameHTML(Temp[i].children[2], Temp[i].children[2].innerText); - Temp[i].children[3].remove(); - Temp[i].children[3].remove(); - Temp[i].children[3].remove(); - Temp[i].children[3].remove(); - } - - - let CurrentPage = parseInt(SearchParams.get("page") || 0); - let PID = Number(SearchParams.get("id")); - document.title = "问题 " + PID + " 状态"; - let Pagination = ``; - document.querySelector("body > div > div.mt-3 > center").innerHTML += Pagination; - } else if (location.pathname == "/problem_solution.php") { - if (UtilityEnabled("RemoveUseless")) { - document.querySelector("h2.lang_en").remove(); //fixes #332 - } - if (UtilityEnabled("CopyMD")) { - await fetch(location.href).then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let CopyMDButton = document.createElement("button"); - CopyMDButton.className = "btn btn-sm btn-outline-secondary copy-btn"; - CopyMDButton.innerText = "复制"; - CopyMDButton.style.marginLeft = "10px"; - CopyMDButton.type = "button"; - document.querySelector("body > div > div.mt-3 > center > h2").appendChild(CopyMDButton); - CopyMDButton.addEventListener("click", () => { - GM_setClipboard(ParsedDocument.querySelector("body > div > div > div").innerText.trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); - CopyMDButton.innerText = "复制成功"; - setTimeout(() => { - CopyMDButton.innerText = "复制"; - }, 1000); - }); - }); - } - let Temp = document.getElementsByClassName("prettyprint"); - for (let i = 0; i < Temp.length; i++) { - let Code = Temp[i].innerText; - Temp[i].outerHTML = ``; - Temp[i].value = Code; - } - for (let i = 0; i < Temp.length; i++) { - CodeMirror.fromTextArea(Temp[i], { - lineNumbers: true, - mode: "text/x-c++src", - readOnly: true, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") - }).setSize("100%", "auto"); - } - } else if (location.pathname == "/open_contest.php") { - let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; - let NewsData = []; - for (let i = 0; i < Temp.length; i += 2) { - let Title = Temp[i].children[0].innerText; - let Time = 0; - if (Temp[i].children[1] != null) { - Time = Temp[i].children[1].innerText; - } - let Body = Temp[i + 1].innerHTML; - NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); - } - document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; - for (let i = 0; i < NewsData.length; i++) { - let NewsRow = document.createElement("div"); - NewsRow.className = "cnt-row"; - let NewsRowHead = document.createElement("div"); - NewsRowHead.className = "cnt-row-head title"; - NewsRowHead.innerText = NewsData[i].Title; - if (NewsData[i].Time.getTime() != 0) { - NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; - } - NewsRow.appendChild(NewsRowHead); - let NewsRowBody = document.createElement("div"); - NewsRowBody.className = "cnt-row-body"; - NewsRowBody.innerHTML = NewsData[i].Body; - NewsRow.appendChild(NewsRowBody); - document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); - } - let MyContestData = document.querySelector("body > div > div.mt-3 > div > div.col-md-4 > div:nth-child(2)").innerHTML; - let CountDownData = document.querySelector("#countdown_list").innerHTML; - document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
    -
    我的月赛
    -
    ${MyContestData}
    -
    -
    -
    倒计时
    -
    ${CountDownData}
    -
    `; - } else if (location.pathname == "/showsource.php") { - let Code = ""; - if (SearchParams.get("ByUserScript") == null) { - document.title = "查看代码: " + SearchParams.get("id"); - await fetch("https://www.xmoj.tech/getsource.php?id=" + SearchParams.get("id")) - .then((Response) => { - return Response.text(); - }).then((Response) => { - Code = Response.replace("\n\n", ""); - }); - } else { - document.title = "查看标程: " + SearchParams.get("pid"); - if (localStorage.getItem("UserScript-LastUploadedStdTime") === undefined || new Date().getTime() - localStorage.getItem("UserScript-LastUploadedStdTime") > 1000 * 60 * 60 * 24 * 30) { - location.href = "https://www.xmoj.tech/userinfo.php?ByUserScript=1"; - } - await new Promise((Resolve) => { - RequestAPI("GetStd", { - "ProblemID": Number(SearchParams.get("pid")) - }, (Response) => { - if (Response.Success) { - Code = Response.Data.StdCode; - } else { - Code = Response.Message; - } - Resolve(); - }); - }); - } - document.querySelector("body > div > div.mt-3").innerHTML = ``; - CodeMirror.fromTextArea(document.querySelector("body > div > div.mt-3 > textarea"), { - lineNumbers: true, - mode: "text/x-c++src", - readOnly: true, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") - }).setSize("100%", "auto"); - } else if (location.pathname == "/ceinfo.php") { - await fetch(location.href) - .then((Result) => { - return Result.text(); - }).then((Result) => { - let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); - document.querySelector("body > div > div.mt-3").innerHTML = ""; - let CodeElement = document.createElement("div"); - CodeElement.className = "mb-3"; - document.querySelector("body > div > div.mt-3").appendChild(CodeElement); - CodeMirror(CodeElement, { - value: ParsedDocument.getElementById("errtxt").innerHTML.replaceAll("<", "<").replaceAll(">", ">"), - lineNumbers: true, - mode: "text/x-c++src", - readOnly: true, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") - }).setSize("100%", "auto"); - }); - } else if (location.pathname == "/problem_std.php") { - await fetch("https://www.xmoj.tech/problem_std.php?cid=" + SearchParams.get("cid") + "&pid=" + SearchParams.get("pid")) - .then((Response) => { - return Response.text(); - }).then((Response) => { - let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); - let Temp = ParsedDocument.getElementsByTagName("pre"); - document.querySelector("body > div > div.mt-3").innerHTML = ""; - for (let i = 0; i < Temp.length; i++) { - let CodeElement = document.createElement("div"); - CodeElement.className = "mb-3"; - document.querySelector("body > div > div.mt-3").appendChild(CodeElement); - CodeMirror(CodeElement, { - value: Temp[i].innerText, - lineNumbers: true, - mode: "text/x-c++src", - readOnly: true, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default") - }).setSize("100%", "auto"); - } - }); - } else if (location.pathname == "/mail.php") { - if (SearchParams.get("to_user") == null) { - document.querySelector("body > div > div.mt-3").innerHTML = `
    -
    - - -
    -
    - -
    -
    - - - - - - - - - -
    接收者最新消息最后联系时间
    - `; - let RefreshMessageList = (Silent = true) => { - if (!Silent) { - ReceiveTable.children[1].innerHTML = ""; - for (let i = 0; i < 10; i++) { - let Row = document.createElement("tr"); - ReceiveTable.children[1].appendChild(Row); - for (let j = 0; j < 3; j++) { - let Cell = document.createElement("td"); - Row.appendChild(Cell); - Cell.innerHTML = ``; - } - } - } - RequestAPI("GetMailList", {}, async (ResponseData) => { - if (ResponseData.Success) { - ErrorElement.style.display = "none"; - let Data = ResponseData.Data.MailList; - ReceiveTable.children[1].innerHTML = ""; - for (let i = 0; i < Data.length; i++) { - let Row = document.createElement("tr"); - ReceiveTable.children[1].appendChild(Row); - let UsernameCell = document.createElement("td"); - Row.appendChild(UsernameCell); - let UsernameSpan = document.createElement("span"); - UsernameCell.appendChild(UsernameSpan); - GetUsernameHTML(UsernameSpan, Data[i].OtherUser, false, "https://www.xmoj.tech/mail.php?to_user="); - if (Data[i].UnreadCount != 0) { - let UnreadCountSpan = document.createElement("span"); - UsernameCell.appendChild(UnreadCountSpan); - UnreadCountSpan.className = "ms-1 badge text-bg-danger"; - UnreadCountSpan.innerText = Data[i].UnreadCount; - } - let LastsMessageCell = document.createElement("td"); - Row.appendChild(LastsMessageCell); - LastsMessageCell.innerText = replaceMarkdownImages(Data[i].LastsMessage, '[image]'); - let SendTimeCell = document.createElement("td"); - Row.appendChild(SendTimeCell); - SendTimeCell.innerHTML = GetRelativeTime(Data[i].SendTime); - } - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }; - Username.addEventListener("input", () => { - Username.classList.remove("is-invalid"); - }); - AddUser.addEventListener("click", () => { - let UsernameData = Username.value; - if (UsernameData == "") { - Username.classList.add("is-invalid"); - return; - } - AddUser.children[0].style.display = ""; - AddUser.disabled = true; - RequestAPI("SendMail", { - "ToUser": String(UsernameData), - "Content": String("您好,我是" + CurrentUsername) - }, (ResponseData) => { - AddUser.children[0].style.display = "none"; - AddUser.disabled = false; - if (ResponseData.Success) { - ErrorElement.style.display = "none"; - RefreshMessageList(); - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }); - RefreshMessageList(false); - addEventListener("focus", RefreshMessageList); - } else { - document.querySelector("body > div > div.mt-3").innerHTML = `
    -
    -
    - -
    -
    - - -
    -
    - - - - - - - - - - - - -
    发送者内容发送时间阅读状态
    `; - GetUsernameHTML(ToUser, SearchParams.get("to_user")); - let RefreshMessage = (Silent = true) => { - if (!Silent) { - MessageTable.children[1].innerHTML = ""; - for (let i = 0; i < 10; i++) { - let Row = document.createElement("tr"); - MessageTable.children[1].appendChild(Row); - for (let j = 0; j < 4; j++) { - let Cell = document.createElement("td"); - Row.appendChild(Cell); - Cell.innerHTML = ``; - } - } - } - RequestAPI("ReadUserMailMention", { - "UserID": String(SearchParams.get("to_user")) - }); - RequestAPI("GetMail", { - "OtherUser": String(SearchParams.get("to_user")) - }, async (ResponseData) => { - if (ResponseData.Success) { - ErrorElement.style.display = "none"; - let Data = ResponseData.Data.Mail; - MessageTable.children[1].innerHTML = ""; - for (let i = 0; i < Data.length; i++) { - let Row = document.createElement("tr"); - MessageTable.children[1].appendChild(Row); - if (!Data[i].IsRead && Data[i].FromUser != CurrentUsername) { - Row.className = "table-info"; - } - let UsernameCell = document.createElement("td"); - Row.appendChild(UsernameCell); - GetUsernameHTML(UsernameCell, Data[i].FromUser); - let ContentCell = document.createElement("td"); - let ContentDiv = document.createElement("div"); - ContentDiv.style.display = "flex"; - ContentDiv.style.maxWidth = window.innerWidth - 300 + "px"; - ContentDiv.style.maxHeight = "500px"; - ContentDiv.style.overflowX = "auto"; - ContentDiv.style.overflowY = "auto"; - ContentDiv.innerHTML = PurifyHTML(marked.parse(Data[i].Content)); - let mediaElements = ContentDiv.querySelectorAll('img, video'); - for (let media of mediaElements) { - media.style.objectFit = 'contain'; - media.style.maxWidth = '100%'; - media.style.maxHeight = '100%'; - } - ContentCell.appendChild(ContentDiv); - Row.appendChild(ContentCell); - let SendTimeCell = document.createElement("td"); - Row.appendChild(SendTimeCell); - SendTimeCell.innerHTML = GetRelativeTime(Data[i].SendTime); - let IsReadCell = document.createElement("td"); - Row.appendChild(IsReadCell); - IsReadCell.innerHTML = (Data[i].IsRead ? "已读" : "未读"); - } - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }; - Content.addEventListener("input", () => { - Content.classList.remove("is-invalid"); - }); - Content.addEventListener("paste", (EventData) => { - let Items = EventData.clipboardData.items; - if (Items.length !== 0) { - for (let i = 0; i < Items.length; i++) { - if (Items[i].type.indexOf("image") != -1) { - let Reader = new FileReader(); - Reader.readAsDataURL(Items[i].getAsFile()); - Reader.onload = () => { - let Before = Content.value.substring(0, Content.selectionStart); - let After = Content.value.substring(Content.selectionEnd, Content.value.length); - const UploadMessage = "![正在上传图片...]()"; - Content.value = Before + UploadMessage + After; - Content.dispatchEvent(new Event("input")); - RequestAPI("UploadImage", { - "Image": Reader.result - }, (ResponseData) => { - if (ResponseData.Success) { - Content.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; - Content.dispatchEvent(new Event("input")); - } else { - Content.value = Before + `![上传失败!` + ResponseData.Message + `]()` + After; - Content.dispatchEvent(new Event("input")); - } - }); - }; - } - } - } - }); - Content.addEventListener("keydown", (Event) => { - if (Event.keyCode == 13) { - Send.click(); - } - }); - Send.addEventListener("click", () => { - if (Content.value == "") { - Content.classList.add("is-invalid"); - return; - } - Send.disabled = true; - Send.children[0].style.display = ""; - let ContentData = Content.value; - RequestAPI("SendMail", { - "ToUser": String(SearchParams.get("to_user")), "Content": String(ContentData) - }, (ResponseData) => { - Send.disabled = false; - Send.children[0].style.display = "none"; - if (ResponseData.Success) { - ErrorElement.style.display = "none"; - Content.value = ""; - RefreshMessage(); - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }); - RefreshMessage(false); - addEventListener("focus", RefreshMessage); - } - } else if (location.pathname.indexOf("/discuss3") != -1) { - if (UtilityEnabled("Discussion")) { - Discussion.classList.add("active"); - if (location.pathname == "/discuss3/discuss.php") { - document.title = "讨论列表"; - let ProblemID = parseInt(SearchParams.get("pid")); - let BoardID = parseInt(SearchParams.get("bid")); - let Page = Number(SearchParams.get("page")) || 1; - document.querySelector("body > div > div").innerHTML = `

    讨论列表${(isNaN(ProblemID) ? "" : ` - 题目` + ProblemID)}

    - - -
    - - - - - - - - - - - - - - - -
    编号标题作者题目编号发布时间回复数最后回复
    `; - NewPost.addEventListener("click", () => { - if (!isNaN(ProblemID)) { - location.href = "https://www.xmoj.tech/discuss3/newpost.php?pid=" + ProblemID; - } else if (SearchParams.get("bid") != null) { - location.href = "https://www.xmoj.tech/discuss3/newpost.php?bid=" + SearchParams.get("bid"); - } else { - location.href = "https://www.xmoj.tech/discuss3/newpost.php"; - } - }); - const RefreshPostList = (Silent = true) => { - if (!Silent) { - PostList.children[1].innerHTML = ""; - for (let i = 0; i < 10; i++) { - let Row = document.createElement("tr"); - PostList.children[1].appendChild(Row); - for (let j = 0; j < 7; j++) { - let Cell = document.createElement("td"); - Row.appendChild(Cell); - Cell.innerHTML = ``; - } - } - } - RequestAPI("GetPosts", { - "ProblemID": Number(ProblemID || 0), - "Page": Number(Page), - "BoardID": Number(SearchParams.get("bid") || -1) - }, async (ResponseData) => { - if (ResponseData.Success == true) { - ErrorElement.style.display = "none"; - if (!Silent) { - DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=1"; - DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page - 1); - DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + Page; - DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page + 1); - DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + ResponseData.Data.PageCount; - if (Page <= 1) { - DiscussPagination.children[0].classList.add("disabled"); - DiscussPagination.children[1].remove(); - } - if (Page >= ResponseData.Data.PageCount) { - DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); - DiscussPagination.children[DiscussPagination.children.length - 2].remove(); - } - } - let Posts = ResponseData.Data.Posts; - PostList.children[1].innerHTML = ""; - if (Posts.length == 0) { - PostList.children[1].innerHTML = `暂无数据`; - } - for (let i = 0; i < Posts.length; i++) { - let Row = document.createElement("tr"); - PostList.children[1].appendChild(Row); - let IDCell = document.createElement("td"); - Row.appendChild(IDCell); - IDCell.innerText = Posts[i].PostID + " " + Posts[i].BoardName; - let TitleCell = document.createElement("td"); - Row.appendChild(TitleCell); - let TitleLink = document.createElement("a"); - TitleCell.appendChild(TitleLink); - TitleLink.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + Posts[i].PostID; - if (Posts[i].Lock.Locked) { - TitleLink.classList.add("link-secondary"); - TitleLink.innerHTML = "🔒 "; - } - TitleLink.innerHTML += Posts[i].Title; - let AuthorCell = document.createElement("td"); - Row.appendChild(AuthorCell); - GetUsernameHTML(AuthorCell, Posts[i].UserID); - let ProblemIDCell = document.createElement("td"); - Row.appendChild(ProblemIDCell); - if (Posts[i].ProblemID != 0) { - let ProblemIDLink = document.createElement("a"); - ProblemIDCell.appendChild(ProblemIDLink); - ProblemIDLink.href = "https://www.xmoj.tech/problem.php?id=" + Posts[i].ProblemID; - ProblemIDLink.innerText = Posts[i].ProblemID; - } - let PostTimeCell = document.createElement("td"); - Row.appendChild(PostTimeCell); - PostTimeCell.innerHTML = GetRelativeTime(Posts[i].PostTime); - let ReplyCountCell = document.createElement("td"); - Row.appendChild(ReplyCountCell); - ReplyCountCell.innerText = Posts[i].ReplyCount; - let LastReplyTimeCell = document.createElement("td"); - Row.appendChild(LastReplyTimeCell); - LastReplyTimeCell.innerHTML = GetRelativeTime(Posts[i].LastReplyTime); - } - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }; - RefreshPostList(false); - addEventListener("focus", RefreshPostList); - RequestAPI("GetBoards", {}, (ResponseData) => { - if (ResponseData.Success === true) { - let LinkElement = document.createElement("a"); - LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php"; - LinkElement.classList.add("me-2"); - LinkElement.innerText = "全部"; - GotoBoard.appendChild(LinkElement); - for (let i = 0; i < ResponseData.Data.Boards.length; i++) { - let LinkElement = document.createElement("a"); - LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php?bid=" + ResponseData.Data.Boards[i].BoardID; - LinkElement.classList.add("me-2"); - LinkElement.innerText = ResponseData.Data.Boards[i].BoardName; - GotoBoard.appendChild(LinkElement); - } - } - }); - } else if (location.pathname == "/discuss3/newpost.php") { - let ProblemID = parseInt(SearchParams.get("pid")); - document.querySelector("body > div > div").innerHTML = `

    发布新讨论` + (!isNaN(ProblemID) ? ` - 题目` + ProblemID : ``) + `

    -
    - -
    -
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - -
    - `; - let CaptchaSecretKey = ""; - unsafeWindow.CaptchaLoadedCallback = () => { - turnstile.render("#CaptchaContainer", { - sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { - CaptchaSecretKey = CaptchaSecretKeyValue; - SubmitElement.disabled = false; - }, - }); - }; - let TurnstileScript = document.createElement("script"); - TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; - document.body.appendChild(TurnstileScript); - ContentElement.addEventListener("keydown", (Event) => { - if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { - SubmitElement.click(); - } - }); - ContentElement.addEventListener("input", () => { - ContentElement.classList.remove("is-invalid"); - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); - RenderMathJax(); - }); - TitleElement.addEventListener("input", () => { - TitleElement.classList.remove("is-invalid"); - }); - ContentElement.addEventListener("paste", (EventData) => { - let Items = EventData.clipboardData.items; - if (Items.length !== 0) { - for (let i = 0; i < Items.length; i++) { - if (Items[i].type.indexOf("image") != -1) { - let Reader = new FileReader(); - Reader.readAsDataURL(Items[i].getAsFile()); - Reader.onload = () => { - let Before = ContentElement.value.substring(0, ContentElement.selectionStart); - let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); - const UploadMessage = "![正在上传图片...]()"; - ContentElement.value = Before + UploadMessage + After; - ContentElement.dispatchEvent(new Event("input")); - RequestAPI("UploadImage", { - "Image": Reader.result - }, (ResponseData) => { - if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; - ContentElement.dispatchEvent(new Event("input")); - } else { - ContentElement.value = Before + `![上传失败!]()` + After; - ContentElement.dispatchEvent(new Event("input")); - } - }); - }; - } - } - } - }); - SubmitElement.addEventListener("click", async () => { - ErrorElement.style.display = "none"; - let Title = TitleElement.value; - let Content = ContentElement.value; - let ProblemID = parseInt(SearchParams.get("pid")); - if (Title === "") { - TitleElement.classList.add("is-invalid"); - return; - } - if (Content === "") { - ContentElement.classList.add("is-invalid"); - return; - } - if (document.querySelector("#Board input:checked") === null) { - ErrorElement.innerText = "请选择要发布的板块"; - ErrorElement.style.display = "block"; - return; - } - SubmitElement.disabled = true; - SubmitElement.children[0].style.display = "inline-block"; - RequestAPI("NewPost", { - "Title": String(Title), - "Content": String(Content), - "ProblemID": Number(isNaN(ProblemID) ? 0 : ProblemID), - "CaptchaSecretKey": String(CaptchaSecretKey), - "BoardID": Number(document.querySelector("#Board input:checked").value) - }, (ResponseData) => { - SubmitElement.disabled = false; - SubmitElement.children[0].style.display = "none"; - if (ResponseData.Success == true) { - location.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ResponseData.Data.PostID; - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }); - RequestAPI("GetBoards", {}, (ResponseData) => { - if (ResponseData.Success === true) { - let Data = ResponseData.Data.Boards; - for (let i = 0; i < Data.length; i++) { - let RadioElement = document.createElement("div"); - RadioElement.className = "col-auto form-check form-check-inline"; - let RadioInput = document.createElement("input"); - RadioInput.className = "form-check-input"; - RadioInput.type = "radio"; - RadioInput.name = "Board"; - RadioInput.id = "Board" + Data[i].BoardID; - RadioInput.value = Data[i].BoardID; - RadioElement.appendChild(RadioInput); - if (SearchParams.get("bid") !== null && SearchParams.get("bid") == Data[i].BoardID) { - RadioInput.checked = true; - } - if (!isNaN(ProblemID)) { - RadioInput.disabled = true; - } - if (Data[i].BoardID == 4) { - if (!isNaN(ProblemID)) RadioInput.checked = true; - RadioInput.disabled = true; - } - let RadioLabel = document.createElement("label"); - RadioLabel.className = "form-check-label"; - RadioLabel.htmlFor = "Board" + Data[i].BoardID; - RadioLabel.innerText = Data[i].BoardName; - RadioElement.appendChild(RadioLabel); - Board.appendChild(RadioElement); - } - } - }); - } else if (location.pathname == "/discuss3/thread.php") { - if (SearchParams.get("tid") == null) { - location.href = "https://www.xmoj.tech/discuss3/discuss.php"; - } else { - let ThreadID = SearchParams.get("tid"); - let Page = Number(SearchParams.get("page")) || 1; - document.querySelector("body > div > div").innerHTML = `

    -
    - 作者:
    - 发布时间: - 板块: - - - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    - `; - let CaptchaSecretKey = ""; - unsafeWindow.CaptchaLoadedCallback = () => { - turnstile.render("#CaptchaContainer", { - theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", - sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { - CaptchaSecretKey = CaptchaSecretKeyValue; - SubmitElement.disabled = false; - }, - }); - }; - let TurnstileScript = document.createElement("script"); - TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; - document.body.appendChild(TurnstileScript); - ContentElement.addEventListener("keydown", (Event) => { - if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { - SubmitElement.click(); - } - }); - ContentElement.addEventListener("input", () => { - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); - RenderMathJax(); - }); - ContentElement.addEventListener("paste", (EventData) => { - let Items = EventData.clipboardData.items; - if (Items.length !== 0) { - for (let i = 0; i < Items.length; i++) { - if (Items[i].type.indexOf("image") != -1) { - let Reader = new FileReader(); - Reader.readAsDataURL(Items[i].getAsFile()); - Reader.onload = () => { - let Before = ContentElement.value.substring(0, ContentElement.selectionStart); - let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); - const UploadMessage = "![正在上传图片...]()"; - ContentElement.value = Before + UploadMessage + After; - ContentElement.dispatchEvent(new Event("input")); - RequestAPI("UploadImage", { - "Image": Reader.result - }, (ResponseData) => { - if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; - ContentElement.dispatchEvent(new Event("input")); - } else { - ContentElement.value = Before + `![上传失败!]()` + After; - ContentElement.dispatchEvent(new Event("input")); - } - }); - }; - } - } - } - }); - let RefreshReply = (Silent = true) => { - if (!Silent) { - PostTitle.innerHTML = ``; - PostAuthor.innerHTML = ``; - PostTime.innerHTML = ``; - PostBoard.innerHTML = ``; - PostReplies.innerHTML = ""; - for (let i = 0; i < 10; i++) { - PostReplies.innerHTML += `
    -
    -
    - - -
    -
    - - - -
    -
    `; - } - } - RequestAPI("GetPost", { - "PostID": Number(ThreadID), "Page": Number(Page) - }, async (ResponseData) => { - if (ResponseData.Success == true) { - let OldScrollTop = document.documentElement.scrollTop; - let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; - if (!Silent) { - DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; - DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); - DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; - DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); - DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; - if (Page <= 1) { - DiscussPagination.children[0].classList.add("disabled"); - DiscussPagination.children[1].remove(); - } - if (Page >= ResponseData.Data.PageCount) { - DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); - DiscussPagination.children[DiscussPagination.children.length - 2].remove(); - } - if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { - Delete.style.display = ""; - } - } - PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); - document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; - PostAuthor.innerHTML = ""; - GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); - PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); - PostBoard.innerHTML = ResponseData.Data.BoardName; - let Replies = ResponseData.Data.Reply; - PostReplies.innerHTML = ""; - for (let i = 0; i < Replies.length; i++) { - let CardElement = document.createElement("div"); - PostReplies.appendChild(CardElement); - CardElement.className = "card mb-3"; - let CardBodyElement = document.createElement("div"); - CardElement.appendChild(CardBodyElement); - CardBodyElement.className = "card-body row"; - let CardBodyRowElement = document.createElement("div"); - CardBodyElement.appendChild(CardBodyRowElement); - CardBodyRowElement.className = "row mb-3"; - let AuthorElement = document.createElement("span"); - CardBodyRowElement.appendChild(AuthorElement); - AuthorElement.className = "col-4 text-muted"; - let AuthorSpanElement = document.createElement("span"); - AuthorElement.appendChild(AuthorSpanElement); - AuthorSpanElement.innerText = "作者:"; - let AuthorUsernameElement = document.createElement("span"); - AuthorElement.appendChild(AuthorUsernameElement); - GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); - let SendTimeElement = document.createElement("span"); - CardBodyRowElement.appendChild(SendTimeElement); - SendTimeElement.className = "col-4 text-muted"; - SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); - - let OKButton; - if (!LockButtons) { - let ButtonsElement = document.createElement("span"); - CardBodyRowElement.appendChild(ButtonsElement); - ButtonsElement.className = "col-4"; - let ReplyButton = document.createElement("button"); - ButtonsElement.appendChild(ReplyButton); - ReplyButton.type = "button"; - ReplyButton.className = "btn btn-sm btn-info"; - ReplyButton.innerText = "回复"; - ReplyButton.addEventListener("click", () => { - let Content = Replies[i].Content; - Content = Content.split("\n").map((Line) => { - // Count the number of '>' characters at the beginning of the line - let nestingLevel = 0; - while (Line.startsWith(">")) { - nestingLevel++; - Line = Line.substring(1).trim(); - } - // If the line is nested more than 2 levels deep, skip it - if (nestingLevel > 2) { - return null; - } - // Reconstruct the line with the appropriate number of '>' characters - return "> ".repeat(nestingLevel + 1) + Line; - }).filter(Line => Line !== null) // Remove null entries - .join("\n"); - ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; - ContentElement.focus(); - }); - let DeleteButton = document.createElement("button"); - ButtonsElement.appendChild(DeleteButton); - DeleteButton.type = "button"; - DeleteButton.className = "btn btn-sm btn-danger ms-1"; - DeleteButton.innerText = "删除"; - DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); - DeleteButton.addEventListener("click", () => { - DeleteButton.disabled = true; - DeleteButton.lastChild.style.display = ""; - RequestAPI("DeleteReply", { - "ReplyID": Number(Replies[i].ReplyID) - }, (ResponseData) => { - if (ResponseData.Success == true) { - RefreshReply(); - } else { - DeleteButton.disabled = false; - DeleteButton.lastChild.style.display = "none"; - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }); - let DeleteSpin = document.createElement("div"); - DeleteButton.appendChild(DeleteSpin); - DeleteSpin.className = "spinner-border spinner-border-sm"; - DeleteSpin.role = "status"; - DeleteSpin.style.display = "none"; - OKButton = document.createElement("button"); - ButtonsElement.appendChild(OKButton); - OKButton.type = "button"; - OKButton.style.display = "none"; - OKButton.className = "btn btn-sm btn-success ms-1"; - OKButton.innerText = "确认"; - let OKSpin = document.createElement("div"); - OKButton.appendChild(OKSpin); - OKSpin.className = "spinner-border spinner-border-sm"; - OKSpin.role = "status"; - OKSpin.style.display = "none"; - OKButton.addEventListener("click", () => { - OKButton.disabled = true; - OKButton.lastChild.style.display = ""; - RequestAPI("EditReply", { - ReplyID: Number(Replies[i].ReplyID), - Content: String(ContentEditor.value) - }, (ResponseData) => { - if (ResponseData.Success == true) { - RefreshReply(); - } else { - OKButton.disabled = false; - OKButton.lastChild.style.display = "none"; - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }); - let CancelButton = document.createElement("button"); - ButtonsElement.appendChild(CancelButton); - CancelButton.type = "button"; - CancelButton.style.display = "none"; - CancelButton.className = "btn btn-sm btn-secondary ms-1"; - CancelButton.innerText = "取消"; - CancelButton.addEventListener("click", () => { - CardBodyElement.children[2].style.display = ""; - CardBodyElement.children[3].style.display = "none"; - EditButton.style.display = ""; - OKButton.style.display = "none"; - CancelButton.style.display = "none"; - }); - let EditButton = document.createElement("button"); - ButtonsElement.appendChild(EditButton); - EditButton.type = "button"; - EditButton.className = "btn btn-sm btn-warning ms-1"; - EditButton.innerText = "编辑"; - EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); - EditButton.addEventListener("click", () => { - CardBodyElement.children[2].style.display = "none"; - CardBodyElement.children[3].style.display = ""; - EditButton.style.display = "none"; - OKButton.style.display = ""; - CancelButton.style.display = ""; - }); - } - - let CardBodyHRElement = document.createElement("hr"); - CardBodyElement.appendChild(CardBodyHRElement); - - let ReplyContentElement = document.createElement("div"); - CardBodyElement.appendChild(ReplyContentElement); - ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); - if (Replies[i].EditTime != null) { - if (Replies[i].EditPerson == Replies[i].UserID) { - ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; - } else { - ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; - } - } - let ContentEditElement = document.createElement("div"); - CardBodyElement.appendChild(ContentEditElement); - ContentEditElement.classList.add("input-group"); - ContentEditElement.style.display = "none"; - let ContentEditor = document.createElement("textarea"); - ContentEditElement.appendChild(ContentEditor); - ContentEditor.className = "form-control col-6"; - ContentEditor.rows = 3; - ContentEditor.value = Replies[i].Content; - if (ContentEditor.value.indexOf("
    ") != -1) { - ContentEditor.value = ContentEditor.value.substring(0, ContentEditor.value.indexOf("
    ")); - } - ContentEditor.addEventListener("keydown", (Event) => { - if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { - OKButton.click(); - } - }); - let PreviewTab = document.createElement("div"); - ContentEditElement.appendChild(PreviewTab); - PreviewTab.className = "form-control col-6"; - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); - ContentEditor.addEventListener("input", () => { - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); - RenderMathJax(); - }); - ContentEditor.addEventListener("paste", (EventData) => { - let Items = EventData.clipboardData.items; - if (Items.length !== 0) { - for (let i = 0; i < Items.length; i++) { - if (Items[i].type.indexOf("image") != -1) { - let Reader = new FileReader(); - Reader.readAsDataURL(Items[i].getAsFile()); - Reader.onload = () => { - let Before = ContentEditor.value.substring(0, ContentEditor.selectionStart); - let After = ContentEditor.value.substring(ContentEditor.selectionEnd, ContentEditor.value.length); - const UploadMessage = "![正在上传图片...]()"; - ContentEditor.value = Before + UploadMessage + After; - ContentEditor.dispatchEvent(new Event("input")); - RequestAPI("UploadImage", { - "Image": Reader.result - }, (ResponseData) => { - if (ResponseData.Success) { - ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; - ContentEditor.dispatchEvent(new Event("input")); - } else { - ContentEditor.value = Before + `![上传失败!]()` + After; - ContentEditor.dispatchEvent(new Event("input")); - } - }); - }; - } - } - } - }); - } - - let UsernameElements = document.getElementsByClassName("Usernames"); - for (let i = 0; i < UsernameElements.length; i++) { - GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); - } - - let CodeElements = document.querySelectorAll("#PostReplies > div > div > div:nth-child(3) > pre > code"); - for (let i = 0; i < CodeElements.length; i++) { - let ModeName = "text/x-c++src"; - if (CodeElements[i].className == "language-c") { - ModeName = "text/x-csrc"; - } else if (CodeElements[i].className == "language-cpp") { - ModeName = "text/x-c++src"; - } - CodeMirror(CodeElements[i].parentElement, { - value: CodeElements[i].innerText, - mode: ModeName, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), - lineNumbers: true, - readOnly: true - }).setSize("100%", "auto"); - CodeElements[i].remove(); - } - - if (LockButtons) { - let LockElement = ContentElement.parentElement.parentElement; - LockElement.innerHTML = "讨论已于 " + await GetRelativeTime(ResponseData.Data.Lock.LockTime) + " 被 "; - let LockUsernameSpan = document.createElement("span"); - LockElement.appendChild(LockUsernameSpan); - GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); - LockElement.innerHTML += " 锁定"; - LockElement.classList.add("mb-5"); - } - - if (IsAdmin) { - ToggleLock.style.display = "inline-block"; - ToggleLockButton.checked = ResponseData.Data.Lock.Locked; - ToggleLockButton.onclick = () => { - ToggleLockButton.disabled = true; - ErrorElement.style.display = "none"; - RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { - "PostID": Number(ThreadID) - }, (LockResponseData) => { - ToggleLockButton.disabled = false; - if (LockResponseData.Success) { - RefreshReply(); - } else { - ErrorElement.style.display = ""; - ErrorElement.innerText = "错误:" + LockResponseData.Message; - ToggleLockButton.checked = !ToggleLockButton.checked; - } - }); - }; - } - - Style.innerHTML += "img {"; - Style.innerHTML += " width: 50%;"; - Style.innerHTML += "}"; - - RenderMathJax(); - - if (Silent) { - scrollTo({ - top: OldScrollTop, behavior: "instant" - }); - } - } else { - PostTitle.innerText = "错误:" + ResponseData.Message; - } - }); - }; - Delete.addEventListener("click", () => { - Delete.disabled = true; - Delete.children[0].style.display = "inline-block"; - RequestAPI("DeletePost", { - "PostID": Number(SearchParams.get("tid")) - }, (ResponseData) => { - Delete.disabled = false; - Delete.children[0].style.display = "none"; - if (ResponseData.Success == true) { - location.href = "https://www.xmoj.tech/discuss3/discuss.php"; - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }); - SubmitElement.addEventListener("click", async () => { - ErrorElement.style.display = "none"; - SubmitElement.disabled = true; - SubmitElement.children[0].style.display = "inline-block"; - RequestAPI("NewReply", { - "PostID": Number(SearchParams.get("tid")), - "Content": String(ContentElement.value), - "CaptchaSecretKey": String(CaptchaSecretKey) - }, async (ResponseData) => { - SubmitElement.disabled = false; - SubmitElement.children[0].style.display = "none"; - if (ResponseData.Success == true) { - RefreshReply(); - ContentElement.value = ""; - PreviewTab.innerHTML = ""; - while (PostReplies.innerHTML.indexOf("placeholder") != -1) { - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - } - ContentElement.focus(); - ContentElement.scrollIntoView(); - turnstile.reset(); - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }); - RefreshReply(false); - addEventListener("focus", RefreshReply); - } - } - } - } - } - } - } 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."); - } - } - } - - /** - * Menu command registrations - */ - - /** - * Register Greasemonkey menu commands - */ - function registerMenuCommands() { - GM_registerMenuCommand("清除缓存", () => { - let Temp = []; - for (let i = 0; i < localStorage.length; i++) { - if (localStorage.key(i).startsWith("UserScript-User-")) { - Temp.push(localStorage.key(i)); - } - } - for (let i = 0; i < Temp.length; i++) { - localStorage.removeItem(Temp[i]); - } - location.reload(); - }); - - GM_registerMenuCommand("重置数据", () => { - if (confirm("确定要重置数据吗?")) { - localStorage.clear(); - location.reload(); - } - }); - } - - /** - * Auto Login Feature - * Automatically redirects to login page when user is not logged in - */ - - - /** - * Initialize auto login feature - * Checks if user is logged in and redirects to login page if necessary - */ - function init$r() { - // Only execute if AutoLogin feature is enabled - if (!UtilityEnabled("AutoLogin")) { - return; - } - - // Check if navbar exists (indicates page is loaded) - if (document.querySelector("#navbar") === null) { - return; - } - - // Check if profile element exists - const profileElement = document.querySelector("#profile"); - if (profileElement === null) { - return; - } - - // Check if user is not logged in (profile shows "登录" = "Login") - const isNotLoggedIn = profileElement.innerHTML === "登录"; - - // Exclude login-related pages from auto-redirect - const excludedPaths = ["/login.php", "/loginpage.php", "/lostpassword.php"]; - const isExcludedPath = excludedPaths.includes(location.pathname); - - // If user is not logged in and not already on a login page, redirect - if (isNotLoggedIn && !isExcludedPath) { - // Save current page to return after login - localStorage.setItem("UserScript-LastPage", location.pathname + location.search); - - // Redirect to login page - location.href = "https://www.xmoj.tech/loginpage.php"; - } - } - - /** - * Discussion Feature - * Provides discussion forum functionality including discussion list, threads, and replies - * Feature ID: Discussion - * Type: A (Add) - * Description: Adds discussion forum with navbar link, problem page integration, and full forum pages - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 205-210: Navbar link creation - * - Lines 1284-1317: Discussion button on problem pages - * - Lines 3609-4386: Discussion forum pages (list, new post, thread view) - */ - - - /** - * Initialize discussion feature - * @param {Object} context - Context object containing required dependencies - * @param {string} context.CurrentUsername - Current logged in username - * @param {URLSearchParams} context.SearchParams - URL search parameters - * @param {boolean} context.IsAdmin - Whether current user is admin - * @param {HTMLStyleElement} context.Style - Style element for adding CSS - * @param {string} context.CaptchaSiteKey - Cloudflare turnstile site key - * @param {Function} context.GetUsernameHTML - Function to render username with profile link - * @param {Function} context.PurifyHTML - Function to sanitize HTML content - * @param {Function} context.RenderMathJax - Function to render math formulas - */ - function init$q(context) { - // Only execute if Discussion feature is enabled - if (!UtilityEnabled("Discussion")) { - return; - } - - const { - CurrentUsername, - SearchParams, - IsAdmin, - Style, - CaptchaSiteKey, - GetUsernameHTML, - PurifyHTML, - RenderMathJax - } = context; - - // Part 1: Create navbar link (lines 205-210) - let Discussion = null; - if (document.querySelector("#navbar > ul:nth-child(1)")) { - Discussion = document.createElement("li"); - document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); - Discussion.innerHTML = "讨论"; - } - - // Part 2: Discussion button on problem pages (lines 1284-1317) - if (location.pathname === "/problem.php") { - const centerElement = document.querySelector("body > div > div.mt-3 > center"); - if (centerElement) { - let PID = null; - if (SearchParams.get("cid") != null) { - PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); - } else { - PID = SearchParams.get("id"); - } - - if (PID) { - let DiscussButton = document.createElement("button"); - DiscussButton.className = "btn btn-outline-secondary position-relative"; - DiscussButton.innerHTML = `讨论`; - DiscussButton.style.marginLeft = "10px"; - DiscussButton.type = "button"; - DiscussButton.addEventListener("click", () => { - if (SearchParams.get("cid") != null) { - open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); - } else { - open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); - } - }); - centerElement.appendChild(DiscussButton); - - let UnreadBadge = document.createElement("span"); - UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; - UnreadBadge.style.display = "none"; - DiscussButton.appendChild(UnreadBadge); - - let RefreshCount = () => { - RequestAPI("GetPostCount", { - "ProblemID": Number(PID) - }, (Response) => { - if (Response.Success) { - if (Response.Data.DiscussCount != 0) { - UnreadBadge.innerText = Response.Data.DiscussCount; - UnreadBadge.style.display = ""; - } - } - }); - }; - RefreshCount(); - addEventListener("focus", RefreshCount); - } - } - } - - // Part 3: Discussion forum pages (lines 3609-4386) - if (location.pathname.indexOf("/discuss3") != -1) { - if (Discussion) { - Discussion.classList.add("active"); - } - - // Discussion list page - if (location.pathname == "/discuss3/discuss.php") { - document.title = "讨论列表"; - let ProblemID = parseInt(SearchParams.get("pid")); - let BoardID = parseInt(SearchParams.get("bid")); - let Page = Number(SearchParams.get("page")) || 1; - document.querySelector("body > div > div").innerHTML = `

    讨论列表${(isNaN(ProblemID) ? "" : ` - 题目` + ProblemID)}

    - - -
    - - - - - - - - - - - - - - - -
    编号标题作者题目编号发布时间回复数最后回复
    `; - NewPost.addEventListener("click", () => { - if (!isNaN(ProblemID)) { - location.href = "https://www.xmoj.tech/discuss3/newpost.php?pid=" + ProblemID; - } else if (SearchParams.get("bid") != null) { - location.href = "https://www.xmoj.tech/discuss3/newpost.php?bid=" + SearchParams.get("bid"); - } else { - location.href = "https://www.xmoj.tech/discuss3/newpost.php"; - } - }); - const RefreshPostList = (Silent = true) => { - if (!Silent) { - PostList.children[1].innerHTML = ""; - for (let i = 0; i < 10; i++) { - let Row = document.createElement("tr"); - PostList.children[1].appendChild(Row); - for (let j = 0; j < 7; j++) { - let Cell = document.createElement("td"); - Row.appendChild(Cell); - Cell.innerHTML = ``; - } - } - } - RequestAPI("GetPosts", { - "ProblemID": Number(ProblemID || 0), - "Page": Number(Page), - "BoardID": Number(SearchParams.get("bid") || -1) - }, async (ResponseData) => { - if (ResponseData.Success == true) { - ErrorElement.style.display = "none"; - if (!Silent) { - DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=1"; - DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page - 1); - DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + Page; - DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + (Page + 1); - DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/discuss.php?" + (isNaN(ProblemID) ? "" : "pid=" + ProblemID + "&") + (isNaN(BoardID) ? "" : "bid=" + BoardID + "&") + "page=" + ResponseData.Data.PageCount; - if (Page <= 1) { - DiscussPagination.children[0].classList.add("disabled"); - DiscussPagination.children[1].remove(); - } - if (Page >= ResponseData.Data.PageCount) { - DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); - DiscussPagination.children[DiscussPagination.children.length - 2].remove(); - } - } - let Posts = ResponseData.Data.Posts; - PostList.children[1].innerHTML = ""; - if (Posts.length == 0) { - PostList.children[1].innerHTML = `暂无数据`; - } - for (let i = 0; i < Posts.length; i++) { - let Row = document.createElement("tr"); - PostList.children[1].appendChild(Row); - let IDCell = document.createElement("td"); - Row.appendChild(IDCell); - IDCell.innerText = Posts[i].PostID + " " + Posts[i].BoardName; - let TitleCell = document.createElement("td"); - Row.appendChild(TitleCell); - let TitleLink = document.createElement("a"); - TitleCell.appendChild(TitleLink); - TitleLink.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + Posts[i].PostID; - if (Posts[i].Lock.Locked) { - TitleLink.classList.add("link-secondary"); - TitleLink.innerHTML = "🔒 "; - } - TitleLink.innerHTML += Posts[i].Title; - let AuthorCell = document.createElement("td"); - Row.appendChild(AuthorCell); - GetUsernameHTML(AuthorCell, Posts[i].UserID); - let ProblemIDCell = document.createElement("td"); - Row.appendChild(ProblemIDCell); - if (Posts[i].ProblemID != 0) { - let ProblemIDLink = document.createElement("a"); - ProblemIDCell.appendChild(ProblemIDLink); - ProblemIDLink.href = "https://www.xmoj.tech/problem.php?id=" + Posts[i].ProblemID; - ProblemIDLink.innerText = Posts[i].ProblemID; - } - let PostTimeCell = document.createElement("td"); - Row.appendChild(PostTimeCell); - PostTimeCell.innerHTML = GetRelativeTime(Posts[i].PostTime); - let ReplyCountCell = document.createElement("td"); - Row.appendChild(ReplyCountCell); - ReplyCountCell.innerText = Posts[i].ReplyCount; - let LastReplyTimeCell = document.createElement("td"); - Row.appendChild(LastReplyTimeCell); - LastReplyTimeCell.innerHTML = GetRelativeTime(Posts[i].LastReplyTime); - } - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }; - RefreshPostList(false); - addEventListener("focus", RefreshPostList); - RequestAPI("GetBoards", {}, (ResponseData) => { - if (ResponseData.Success === true) { - let LinkElement = document.createElement("a"); - LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php"; - LinkElement.classList.add("me-2"); - LinkElement.innerText = "全部"; - GotoBoard.appendChild(LinkElement); - for (let i = 0; i < ResponseData.Data.Boards.length; i++) { - let LinkElement = document.createElement("a"); - LinkElement.href = "https://www.xmoj.tech/discuss3/discuss.php?bid=" + ResponseData.Data.Boards[i].BoardID; - LinkElement.classList.add("me-2"); - LinkElement.innerText = ResponseData.Data.Boards[i].BoardName; - GotoBoard.appendChild(LinkElement); - } - } - }); - } else if (location.pathname == "/discuss3/newpost.php") { - // New post page implementation - let ProblemID = parseInt(SearchParams.get("pid")); - document.querySelector("body > div > div").innerHTML = `

    发布新讨论` + (!isNaN(ProblemID) ? ` - 题目` + ProblemID : ``) + `

    -
    - -
    -
    -
    -
    - - -
    -
    - -
    - -
    -
    -
    - -
    - `; - let CaptchaSecretKey = ""; - unsafeWindow.CaptchaLoadedCallback = () => { - turnstile.render("#CaptchaContainer", { - sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { - CaptchaSecretKey = CaptchaSecretKeyValue; - SubmitElement.disabled = false; - }, - }); - }; - let TurnstileScript = document.createElement("script"); - TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; - document.body.appendChild(TurnstileScript); - ContentElement.addEventListener("keydown", (Event) => { - if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { - SubmitElement.click(); - } - }); - ContentElement.addEventListener("input", () => { - ContentElement.classList.remove("is-invalid"); - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); - RenderMathJax(); - }); - TitleElement.addEventListener("input", () => { - TitleElement.classList.remove("is-invalid"); - }); - ContentElement.addEventListener("paste", (EventData) => { - let Items = EventData.clipboardData.items; - if (Items.length !== 0) { - for (let i = 0; i < Items.length; i++) { - if (Items[i].type.indexOf("image") != -1) { - let Reader = new FileReader(); - Reader.readAsDataURL(Items[i].getAsFile()); - Reader.onload = () => { - let Before = ContentElement.value.substring(0, ContentElement.selectionStart); - let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); - const UploadMessage = "![正在上传图片...]()"; - ContentElement.value = Before + UploadMessage + After; - ContentElement.dispatchEvent(new Event("input")); - RequestAPI("UploadImage", { - "Image": Reader.result - }, (ResponseData) => { - if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; - ContentElement.dispatchEvent(new Event("input")); - } else { - ContentElement.value = Before + `![上传失败!]()` + After; - ContentElement.dispatchEvent(new Event("input")); - } - }); - }; - } - } - } - }); - SubmitElement.addEventListener("click", async () => { - ErrorElement.style.display = "none"; - let Title = TitleElement.value; - let Content = ContentElement.value; - let ProblemID = parseInt(SearchParams.get("pid")); - if (Title === "") { - TitleElement.classList.add("is-invalid"); - return; - } - if (Content === "") { - ContentElement.classList.add("is-invalid"); - return; - } - if (document.querySelector("#Board input:checked") === null) { - ErrorElement.innerText = "请选择要发布的板块"; - ErrorElement.style.display = "block"; - return; - } - SubmitElement.disabled = true; - SubmitElement.children[0].style.display = "inline-block"; - RequestAPI("NewPost", { - "Title": String(Title), - "Content": String(Content), - "ProblemID": Number(isNaN(ProblemID) ? 0 : ProblemID), - "CaptchaSecretKey": String(CaptchaSecretKey), - "BoardID": Number(document.querySelector("#Board input:checked").value) - }, (ResponseData) => { - SubmitElement.disabled = false; - SubmitElement.children[0].style.display = "none"; - if (ResponseData.Success == true) { - location.href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ResponseData.Data.PostID; - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }); - RequestAPI("GetBoards", {}, (ResponseData) => { - if (ResponseData.Success === true) { - let Data = ResponseData.Data.Boards; - for (let i = 0; i < Data.length; i++) { - let RadioElement = document.createElement("div"); - RadioElement.className = "col-auto form-check form-check-inline"; - let RadioInput = document.createElement("input"); - RadioInput.className = "form-check-input"; - RadioInput.type = "radio"; - RadioInput.name = "Board"; - RadioInput.id = "Board" + Data[i].BoardID; - RadioInput.value = Data[i].BoardID; - RadioElement.appendChild(RadioInput); - if (SearchParams.get("bid") !== null && SearchParams.get("bid") == Data[i].BoardID) { - RadioInput.checked = true; - } - if (!isNaN(ProblemID)) { - RadioInput.disabled = true; - } - if (Data[i].BoardID == 4) { - if (!isNaN(ProblemID)) RadioInput.checked = true; - RadioInput.disabled = true; - } - let RadioLabel = document.createElement("label"); - RadioLabel.className = "form-check-label"; - RadioLabel.htmlFor = "Board" + Data[i].BoardID; - RadioLabel.innerText = Data[i].BoardName; - RadioElement.appendChild(RadioLabel); - Board.appendChild(RadioElement); - } - } - }); - } else if (location.pathname == "/discuss3/thread.php") { - // Thread view page implementation - if (SearchParams.get("tid") == null) { - location.href = "https://www.xmoj.tech/discuss3/discuss.php"; - } else { - let ThreadID = SearchParams.get("tid"); - let Page = Number(SearchParams.get("page")) || 1; - document.querySelector("body > div > div").innerHTML = `

    -
    - 作者:
    - 发布时间: - 板块: - - - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    - `; - let CaptchaSecretKey = ""; - unsafeWindow.CaptchaLoadedCallback = () => { - turnstile.render("#CaptchaContainer", { - theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", - sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { - CaptchaSecretKey = CaptchaSecretKeyValue; - SubmitElement.disabled = false; - }, - }); - }; - let TurnstileScript = document.createElement("script"); - TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; - document.body.appendChild(TurnstileScript); - ContentElement.addEventListener("keydown", (Event) => { - if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { - SubmitElement.click(); - } - }); - ContentElement.addEventListener("input", () => { - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); - RenderMathJax(); - }); - ContentElement.addEventListener("paste", (EventData) => { - let Items = EventData.clipboardData.items; - if (Items.length !== 0) { - for (let i = 0; i < Items.length; i++) { - if (Items[i].type.indexOf("image") != -1) { - let Reader = new FileReader(); - Reader.readAsDataURL(Items[i].getAsFile()); - Reader.onload = () => { - let Before = ContentElement.value.substring(0, ContentElement.selectionStart); - let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); - const UploadMessage = "![正在上传图片...]()"; - ContentElement.value = Before + UploadMessage + After; - ContentElement.dispatchEvent(new Event("input")); - RequestAPI("UploadImage", { - "Image": Reader.result - }, (ResponseData) => { - if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; - ContentElement.dispatchEvent(new Event("input")); - } else { - ContentElement.value = Before + `![上传失败!]()` + After; - ContentElement.dispatchEvent(new Event("input")); - } - }); - }; - } - } - } - }); - let RefreshReply = (Silent = true) => { - if (!Silent) { - PostTitle.innerHTML = ``; - PostAuthor.innerHTML = ``; - PostTime.innerHTML = ``; - PostBoard.innerHTML = ``; - PostReplies.innerHTML = ""; - for (let i = 0; i < 10; i++) { - PostReplies.innerHTML += `
    -
    -
    - - -
    -
    - - - -
    -
    `; - } - } - RequestAPI("GetPost", { - "PostID": Number(ThreadID), "Page": Number(Page) - }, async (ResponseData) => { - if (ResponseData.Success == true) { - let OldScrollTop = document.documentElement.scrollTop; - let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; - if (!Silent) { - DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; - DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); - DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; - DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); - DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; - if (Page <= 1) { - DiscussPagination.children[0].classList.add("disabled"); - DiscussPagination.children[1].remove(); - } - if (Page >= ResponseData.Data.PageCount) { - DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); - DiscussPagination.children[DiscussPagination.children.length - 2].remove(); - } - if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { - Delete.style.display = ""; - } - } - PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); - document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; - PostAuthor.innerHTML = ""; - GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); - PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); - PostBoard.innerHTML = ResponseData.Data.BoardName; - let Replies = ResponseData.Data.Reply; - PostReplies.innerHTML = ""; - for (let i = 0; i < Replies.length; i++) { - let CardElement = document.createElement("div"); - PostReplies.appendChild(CardElement); - CardElement.className = "card mb-3"; - let CardBodyElement = document.createElement("div"); - CardElement.appendChild(CardBodyElement); - CardBodyElement.className = "card-body row"; - let CardBodyRowElement = document.createElement("div"); - CardBodyElement.appendChild(CardBodyRowElement); - CardBodyRowElement.className = "row mb-3"; - let AuthorElement = document.createElement("span"); - CardBodyRowElement.appendChild(AuthorElement); - AuthorElement.className = "col-4 text-muted"; - let AuthorSpanElement = document.createElement("span"); - AuthorElement.appendChild(AuthorSpanElement); - AuthorSpanElement.innerText = "作者:"; - let AuthorUsernameElement = document.createElement("span"); - AuthorElement.appendChild(AuthorUsernameElement); - GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); - let SendTimeElement = document.createElement("span"); - CardBodyRowElement.appendChild(SendTimeElement); - SendTimeElement.className = "col-4 text-muted"; - SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); - - let OKButton; - if (!LockButtons) { - let ButtonsElement = document.createElement("span"); - CardBodyRowElement.appendChild(ButtonsElement); - ButtonsElement.className = "col-4"; - let ReplyButton = document.createElement("button"); - ButtonsElement.appendChild(ReplyButton); - ReplyButton.type = "button"; - ReplyButton.className = "btn btn-sm btn-info"; - ReplyButton.innerText = "回复"; - ReplyButton.addEventListener("click", () => { - let Content = Replies[i].Content; - Content = Content.split("\n").map((Line) => { - // Count the number of '>' characters at the beginning of the line - let nestingLevel = 0; - while (Line.startsWith(">")) { - nestingLevel++; - Line = Line.substring(1).trim(); - } - // If the line is nested more than 2 levels deep, skip it - if (nestingLevel > 2) { - return null; - } - // Reconstruct the line with the appropriate number of '>' characters - return "> ".repeat(nestingLevel + 1) + Line; - }).filter(Line => Line !== null) // Remove null entries - .join("\n"); - ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; - ContentElement.focus(); - }); - let DeleteButton = document.createElement("button"); - ButtonsElement.appendChild(DeleteButton); - DeleteButton.type = "button"; - DeleteButton.className = "btn btn-sm btn-danger ms-1"; - DeleteButton.innerText = "删除"; - DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); - DeleteButton.addEventListener("click", () => { - DeleteButton.disabled = true; - DeleteButton.lastChild.style.display = ""; - RequestAPI("DeleteReply", { - "ReplyID": Number(Replies[i].ReplyID) - }, (ResponseData) => { - if (ResponseData.Success == true) { - RefreshReply(); - } else { - DeleteButton.disabled = false; - DeleteButton.lastChild.style.display = "none"; - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }); - let DeleteSpin = document.createElement("div"); - DeleteButton.appendChild(DeleteSpin); - DeleteSpin.className = "spinner-border spinner-border-sm"; - DeleteSpin.role = "status"; - DeleteSpin.style.display = "none"; - OKButton = document.createElement("button"); - ButtonsElement.appendChild(OKButton); - OKButton.type = "button"; - OKButton.style.display = "none"; - OKButton.className = "btn btn-sm btn-success ms-1"; - OKButton.innerText = "确认"; - let OKSpin = document.createElement("div"); - OKButton.appendChild(OKSpin); - OKSpin.className = "spinner-border spinner-border-sm"; - OKSpin.role = "status"; - OKSpin.style.display = "none"; - OKButton.addEventListener("click", () => { - OKButton.disabled = true; - OKButton.lastChild.style.display = ""; - RequestAPI("EditReply", { - ReplyID: Number(Replies[i].ReplyID), - Content: String(ContentEditor.value) - }, (ResponseData) => { - if (ResponseData.Success == true) { - RefreshReply(); - } else { - OKButton.disabled = false; - OKButton.lastChild.style.display = "none"; - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = ""; - } - }); - }); - let CancelButton = document.createElement("button"); - ButtonsElement.appendChild(CancelButton); - CancelButton.type = "button"; - CancelButton.style.display = "none"; - CancelButton.className = "btn btn-sm btn-secondary ms-1"; - CancelButton.innerText = "取消"; - CancelButton.addEventListener("click", () => { - CardBodyElement.children[2].style.display = ""; - CardBodyElement.children[3].style.display = "none"; - EditButton.style.display = ""; - OKButton.style.display = "none"; - CancelButton.style.display = "none"; - }); - let EditButton = document.createElement("button"); - ButtonsElement.appendChild(EditButton); - EditButton.type = "button"; - EditButton.className = "btn btn-sm btn-warning ms-1"; - EditButton.innerText = "编辑"; - EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); - EditButton.addEventListener("click", () => { - CardBodyElement.children[2].style.display = "none"; - CardBodyElement.children[3].style.display = ""; - EditButton.style.display = "none"; - OKButton.style.display = ""; - CancelButton.style.display = ""; - }); - } - - let CardBodyHRElement = document.createElement("hr"); - CardBodyElement.appendChild(CardBodyHRElement); - - let ReplyContentElement = document.createElement("div"); - CardBodyElement.appendChild(ReplyContentElement); - ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); - if (Replies[i].EditTime != null) { - if (Replies[i].EditPerson == Replies[i].UserID) { - ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; - } else { - ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; - } - } - let ContentEditElement = document.createElement("div"); - CardBodyElement.appendChild(ContentEditElement); - ContentEditElement.classList.add("input-group"); - ContentEditElement.style.display = "none"; - let ContentEditor = document.createElement("textarea"); - ContentEditElement.appendChild(ContentEditor); - ContentEditor.className = "form-control col-6"; - ContentEditor.rows = 3; - ContentEditor.value = Replies[i].Content; - if (ContentEditor.value.indexOf("
    ") != -1) { - ContentEditor.value = ContentEditor.value.substring(0, ContentEditor.value.indexOf("
    ")); - } - ContentEditor.addEventListener("keydown", (Event) => { - if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { - OKButton.click(); - } - }); - let PreviewTab = document.createElement("div"); - ContentEditElement.appendChild(PreviewTab); - PreviewTab.className = "form-control col-6"; - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); - ContentEditor.addEventListener("input", () => { - PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); - RenderMathJax(); - }); - ContentEditor.addEventListener("paste", (EventData) => { - let Items = EventData.clipboardData.items; - if (Items.length !== 0) { - for (let i = 0; i < Items.length; i++) { - if (Items[i].type.indexOf("image") != -1) { - let Reader = new FileReader(); - Reader.readAsDataURL(Items[i].getAsFile()); - Reader.onload = () => { - let Before = ContentEditor.value.substring(0, ContentEditor.selectionStart); - let After = ContentEditor.value.substring(ContentEditor.selectionEnd, ContentEditor.value.length); - const UploadMessage = "![正在上传图片...]()"; - ContentEditor.value = Before + UploadMessage + After; - ContentEditor.dispatchEvent(new Event("input")); - RequestAPI("UploadImage", { - "Image": Reader.result - }, (ResponseData) => { - if (ResponseData.Success) { - ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; - ContentEditor.dispatchEvent(new Event("input")); - } else { - ContentEditor.value = Before + `![上传失败!]()` + After; - ContentEditor.dispatchEvent(new Event("input")); - } - }); - }; - } - } - } - }); - } - - let UsernameElements = document.getElementsByClassName("Usernames"); - for (let i = 0; i < UsernameElements.length; i++) { - GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); - } - - let CodeElements = document.querySelectorAll("#PostReplies > div > div > div:nth-child(3) > pre > code"); - for (let i = 0; i < CodeElements.length; i++) { - let ModeName = "text/x-c++src"; - if (CodeElements[i].className == "language-c") { - ModeName = "text/x-csrc"; - } else if (CodeElements[i].className == "language-cpp") { - ModeName = "text/x-c++src"; - } - CodeMirror(CodeElements[i].parentElement, { - value: CodeElements[i].innerText, - mode: ModeName, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), - lineNumbers: true, - readOnly: true - }).setSize("100%", "auto"); - CodeElements[i].remove(); - } - - if (LockButtons) { - let LockElement = ContentElement.parentElement.parentElement; - LockElement.innerHTML = "讨论已于 " + await GetRelativeTime(ResponseData.Data.Lock.LockTime) + " 被 "; - let LockUsernameSpan = document.createElement("span"); - LockElement.appendChild(LockUsernameSpan); - GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); - LockElement.innerHTML += " 锁定"; - LockElement.classList.add("mb-5"); - } - - if (IsAdmin) { - ToggleLock.style.display = "inline-block"; - ToggleLockButton.checked = ResponseData.Data.Lock.Locked; - ToggleLockButton.onclick = () => { - ToggleLockButton.disabled = true; - ErrorElement.style.display = "none"; - RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { - "PostID": Number(ThreadID) - }, (LockResponseData) => { - ToggleLockButton.disabled = false; - if (LockResponseData.Success) { - RefreshReply(); - } else { - ErrorElement.style.display = ""; - ErrorElement.innerText = "错误:" + LockResponseData.Message; - ToggleLockButton.checked = !ToggleLockButton.checked; - } - }); - }; - } - - Style.innerHTML += "img {"; - Style.innerHTML += " width: 50%;"; - Style.innerHTML += "}"; - - RenderMathJax(); - - if (Silent) { - scrollTo({ - top: OldScrollTop, behavior: "instant" - }); - } - } else { - PostTitle.innerText = "错误:" + ResponseData.Message; - } - }); - }; - Delete.addEventListener("click", () => { - Delete.disabled = true; - Delete.children[0].style.display = "inline-block"; - RequestAPI("DeletePost", { - "PostID": Number(SearchParams.get("tid")) - }, (ResponseData) => { - Delete.disabled = false; - Delete.children[0].style.display = "none"; - if (ResponseData.Success == true) { - location.href = "https://www.xmoj.tech/discuss3/discuss.php"; - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }); - SubmitElement.addEventListener("click", async () => { - ErrorElement.style.display = "none"; - SubmitElement.disabled = true; - SubmitElement.children[0].style.display = "inline-block"; - RequestAPI("NewReply", { - "PostID": Number(SearchParams.get("tid")), - "Content": String(ContentElement.value), - "CaptchaSecretKey": String(CaptchaSecretKey) - }, async (ResponseData) => { - SubmitElement.disabled = false; - SubmitElement.children[0].style.display = "none"; - if (ResponseData.Success == true) { - RefreshReply(); - ContentElement.value = ""; - PreviewTab.innerHTML = ""; - while (PostReplies.innerHTML.indexOf("placeholder") != -1) { - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - } - ContentElement.focus(); - ContentElement.scrollIntoView(); - turnstile.reset(); - } else { - ErrorElement.innerText = ResponseData.Message; - ErrorElement.style.display = "block"; - } - }); - }); - RefreshReply(false); - addEventListener("focus", RefreshReply); - } - } - } - } - - /** - * Copy Samples Feature - * Fixes copy functionality for test samples in problem pages - * Feature ID: CopySamples - * Type: F (Fix) - * Description: 题目界面测试样例有时复制无效 - */ - - - /** - * Initialize copy samples feature - * Adds click handlers to copy buttons that copy sample data to clipboard - * - * This feature fixes issues where copy buttons on the problem page don't work - * properly. It intercepts clicks on .copy-btn elements and copies the associated - * .sampledata content to the clipboard using GM_setClipboard. - * - * Expected DOM structure: - * - Button with class "copy-btn" - * - Parent element containing a .sampledata element with the text to copy - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 - */ - function init$p() { - // Only execute if CopySamples feature is enabled - if (!UtilityEnabled("CopySamples")) { - return; - } - - // Attach click handlers to all copy buttons - $(".copy-btn").click((Event) => { - let CurrentButton = $(Event.currentTarget); - let span = CurrentButton.parent().last().find(".sampledata"); - - // Check if sample data element was found - if (!span.length) { - CurrentButton.text("未找到代码块").addClass("done"); - setTimeout(() => { - $(".copy-btn").text("复制").removeClass("done"); - }, 1000); - return; - } - - // Copy sample data to clipboard - GM_setClipboard(span.text()); - - // Show success feedback - CurrentButton.text("复制成功").addClass("done"); - setTimeout(() => { - $(".copy-btn").text("复制").removeClass("done"); - }, 1000); - }); - } - - /** - * Compare Source Feature - * Allows users to compare two source code submissions side-by-side using CodeMirror MergeView - * - * Extracted from bootstrap.js: - * - Lines 985: Feature definition - * - Lines 1465-1474: Button on problem page - * - Lines 2720-2787: Main comparison interface - */ - - - /** - * Initialize compare source feature - * - Adds a "Compare Submissions" button on problem pages - * - Creates comparison interface on comparesource.php page - */ - async function init$o() { - // Only execute if CompareSource feature is enabled - if (!UtilityEnabled("CompareSource")) { - return; - } - - // Add "Compare Submissions" button on problem pages - addCompareButtonOnProblemPage(); - - // Handle comparison interface on comparesource.php page - if (location.pathname === "/comparesource.php") { - await handleCompareSourcePage(); - } - } - - /** - * Adds a "Compare Submissions" button on problem pages - * Button navigates to comparesource.php when clicked - */ - function addCompareButtonOnProblemPage() { - const inputAppend = document.querySelector("body > div.container > div > div.input-append"); - if (!inputAppend) { - return; - } - - const compareButton = document.createElement("button"); - inputAppend.appendChild(compareButton); - compareButton.className = "btn btn-outline-secondary"; - compareButton.innerText = "比较提交记录"; - compareButton.addEventListener("click", () => { - location.href = "https://www.xmoj.tech/comparesource.php"; - }); - compareButton.style.marginBottom = "7px"; - } - - /** - * Handles the compare source page functionality - * Creates either a form to input submission IDs or displays a side-by-side comparison - */ - async function handleCompareSourcePage() { - const searchParams = new URLSearchParams(location.search); - - // If no search parameters, show the input form - if (location.search === "") { - createComparisonForm(); - } else { - // If search parameters exist, fetch and display the comparison - await createComparisonView(searchParams); - } - } - - /** - * Creates the form to input submission IDs for comparison - */ - function createComparisonForm() { - const container = document.querySelector("body > div.container > div"); - if (!container) { - return; - } - - container.innerHTML = ""; - - // Left code input - const leftCodeText = document.createElement("span"); - container.appendChild(leftCodeText); - leftCodeText.innerText = "左侧代码的运行编号:"; - - const leftCode = document.createElement("input"); - container.appendChild(leftCode); - leftCode.classList.add("form-control"); - leftCode.style.width = "40%"; - leftCode.style.marginBottom = "5px"; - - // Right code input - const rightCodeText = document.createElement("span"); - container.appendChild(rightCodeText); - rightCodeText.innerText = "右侧代码的运行编号:"; - - const rightCode = document.createElement("input"); - container.appendChild(rightCode); - rightCode.classList.add("form-control"); - rightCode.style.width = "40%"; - rightCode.style.marginBottom = "5px"; - - // Compare button - const compareButton = document.createElement("button"); - container.appendChild(compareButton); - compareButton.innerText = "比较"; - compareButton.className = "btn btn-primary"; - compareButton.addEventListener("click", () => { - location.href = "https://www.xmoj.tech/comparesource.php?left=" + - Number(leftCode.value) + "&right=" + Number(rightCode.value); - }); - } - - /** - * Creates the side-by-side comparison view using CodeMirror MergeView - * @param {URLSearchParams} searchParams - URL parameters containing left and right submission IDs - */ - async function createComparisonView(searchParams) { - const mtElement = document.querySelector("body > div > div.mt-3"); - if (!mtElement) { - return; - } - - // Create comparison interface with checkbox and comparison element - mtElement.innerHTML = ` -
    - - -
    -
    `; - - // Fetch left code - let leftCode = ""; - await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("left")) - .then((response) => { - return response.text(); - }) - .then((response) => { - leftCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); - }); - - // Fetch right code - let rightCode = ""; - await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("right")) - .then((response) => { - return response.text(); - }) - .then((response) => { - rightCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); - }); - - // Create CodeMirror MergeView for side-by-side comparison - const compareElement = document.getElementById("CompareElement"); - const mergeViewElement = CodeMirror.MergeView(compareElement, { - value: leftCode, - origLeft: null, - orig: rightCode, - lineNumbers: true, - mode: "text/x-c++src", - collapseIdentical: "true", - readOnly: true, - theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), - revertButtons: false, - ignoreWhitespace: true - }); - - // Add event listener for ignore whitespace checkbox - const ignoreWhitespace = document.getElementById("IgnoreWhitespace"); - ignoreWhitespace.addEventListener("change", () => { - mergeViewElement.ignoreWhitespace = ignoreWhitespace.checked; - }); - } - - /** - * Remove Useless Feature - * Removes unwanted elements from various pages - * Feature ID: RemoveUseless - * Type: U (Utility) - * Description: 移除页面中的无用元素 - */ - - - /** - * Initialize remove useless feature - * Removes various unwanted elements from the page based on current location - * - * This feature removes several unnecessary or distracting elements: - * - Marquee elements (scrolling text banners) - * - Footer elements - * - English title headers (h2.lang_en) on problem and solution pages - * - Submission nodes on user info pages - * - Center tags on problem pages - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Line 320-322: Remove marquee elements - * - Line 398-402: Remove footer - * - Line 1222-1225: Remove h2.lang_en and second center tag on problem pages - * - Line 2500-2505: Remove submission child nodes on userinfo page - * - Line 3209-3211: Remove h2.lang_en on problem_solution page - */ - function init$n() { - // Only execute if RemoveUseless feature is enabled - if (!UtilityEnabled("RemoveUseless")) { - return; - } - - // Remove marquee elements (scrolling banners) - Line 320-322 - const marquee = document.getElementsByTagName("marquee")[0]; - if (marquee !== undefined) { - marquee.remove(); - } - - // Remove footer - Line 398-402 - const footer = document.getElementsByClassName("footer")[0]; - if (footer !== null) { - footer.remove(); - } - - // Page-specific removals based on current pathname - const pathname = location.pathname; - - // Problem page specific removals - Line 1222-1225 - if (pathname === "/problem.php") { - const langEnHeader = document.querySelector("h2.lang_en"); - if (langEnHeader) { - langEnHeader.remove(); - } - - const centerElements = document.getElementsByTagName("center"); - if (centerElements[1]) { - centerElements[1].remove(); - } - } - - // User info page specific removals - Line 2500-2505 - if (pathname === "/userinfo.php") { - const searchParams = new URLSearchParams(location.search); - if (searchParams.get("ByUserScript") === null) { - const submissionElement = document.getElementById("submission"); - if (submissionElement) { - const childNodes = submissionElement.childNodes; - // Remove all child nodes - for (let i = childNodes.length - 1; i >= 0; i--) { - childNodes[i].remove(); - } - } - } - } - - // Problem solution page specific removals - Line 3209-3211 - if (pathname === "/problem_solution.php") { - const langEnHeader = document.querySelector("h2.lang_en"); - if (langEnHeader) { - langEnHeader.remove(); - } - } - } - - /** - * Replace XM Feature - * Replaces "小明" references with "高老师" - * Feature ID: ReplaceXM - * Type: C (Cosmetic/Fun) - * Description: 将"小明"替换为"高老师" - */ - - - /** - * Initialize ReplaceXM feature - * Replaces text content throughout the page: - * - "我" -> "高老师" - * - "小明" -> "高老师" - * - "下海" -> "上海" - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 219-222: Text replacement - * - Line 304: Navbar brand text - */ - function init$m() { - // Only execute if ReplaceXM feature is enabled - if (!UtilityEnabled("ReplaceXM")) { - return; - } - - // Replace text content throughout the page - document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); - document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); - } - - /** - * Replace YN Feature - * Replaces Y/N status indicators with symbols - * Feature ID: ReplaceYN - * Type: U (Utility) - * Description: 将Y/N状态替换为符号 - */ - - - /** - * Initialize ReplaceYN feature - * Replaces status text with symbols: - * - "Y" (AC/Accepted) -> "✓" - * - "N" (WA/Wrong Answer) -> "✗" - * - "W" (Waiting) -> "⏳" - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 404-417: Status text replacement - */ - function init$l() { - // Only execute if ReplaceYN feature is enabled - if (!UtilityEnabled("ReplaceYN")) { - return; - } - - // Replace AC (Accepted) status - let elements = document.getElementsByClassName("status_y"); - for (let i = 0; i < elements.length; i++) { - elements[i].innerText = "✓"; - } - - // Replace WA (Wrong Answer) status - elements = document.getElementsByClassName("status_n"); - for (let i = 0; i < elements.length; i++) { - elements[i].innerText = "✗"; - } - - // Replace Waiting status - elements = document.getElementsByClassName("status_w"); - for (let i = 0; i < elements.length; i++) { - elements[i].innerText = "⏳"; - } - } - - /** - * Add Animation Feature - * Adds CSS transitions to status and test-case elements - * Feature ID: AddAnimation - * Type: C (Cosmetic) - * Description: 为状态和测试用例元素添加动画 - */ - - - /** - * Initialize AddAnimation feature - * Adds smooth transitions to status and test-case elements - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 381-384: Animation CSS - */ - function init$k() { - // Only execute if AddAnimation feature is enabled - if (!UtilityEnabled("AddAnimation")) { - return; - } - - // Add CSS for animations - const style = document.createElement('style'); - style.innerHTML = `.status, .test-case { - transition: 0.5s !important; - }`; - document.head.appendChild(style); - } - - /** - * Add Color Text Feature - * Adds CSS classes for colored text (red, green, blue) - * Feature ID: AddColorText - * Type: U (Utility) - * Description: 添加彩色文本CSS类 - */ - - - /** - * Initialize AddColorText feature - * Adds CSS classes for red, green, and blue text - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 386-395: Color text CSS - */ - function init$j() { - // Only execute if AddColorText feature is enabled - if (!UtilityEnabled("AddColorText")) { - return; - } - - // Add CSS for colored text classes - const style = document.createElement('style'); - style.innerHTML = `.red { - color: red !important; - } - .green { - color: green !important; - } - .blue { - color: blue !important; - }`; - document.head.appendChild(style); - } - - /** - * Save Password Feature - * Automatically saves and fills login credentials - * Feature ID: SavePassword - * Type: U (Utility) - * Description: 自动保存和填充登录凭据 - */ - - - /** - * Initialize SavePassword feature - * Sets up auto-fill on login page when credentials are available - * - * Note: This feature also integrates with the login handler to: - * - Save credentials after successful login - * - Clear credentials after failed login - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 2841-2843: Save credentials on success - * - Lines 2850-2852: Clear credentials on failure - * - Lines 2867-2876: Auto-fill and auto-submit login form - */ - function init$i() { - // Only execute on login page - if (location.pathname !== "/loginpage.php") { - return; - } - - // Only execute if SavePassword feature is enabled - if (!UtilityEnabled("SavePassword")) { - return; - } - - // Auto-fill login form with saved credentials - (async () => { - // Wait a bit for the page to be ready - await new Promise(resolve => setTimeout(resolve, 100)); - - const credential = await getCredential$1(); - if (credential) { - const usernameInput = document.querySelector("#login > div:nth-child(1) > div > input"); - const passwordInput = document.querySelector("#login > div:nth-child(2) > div > input"); - const loginButton = document.getElementsByName("submit")[0]; - - if (usernameInput && passwordInput && loginButton) { - usernameInput.value = credential.id; - passwordInput.value = credential.password; - loginButton.click(); - } - } - })(); - } - - /** - * Remove Alerts Feature - * Removes redundant alerts and warnings - * Feature ID: RemoveAlerts - * Type: D (Debug/Development) - * Description: 去除多余反复的提示 - */ - - - /** - * Initialize RemoveAlerts feature - * Modifies contest links to bypass "contest not started" alerts - * - * On contest pages, when the contest hasn't started yet, this feature - * changes the link to point directly to start_contest.php, bypassing - * the alert that would normally prevent access. - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 1666-1667: Modify contest start link - */ - function init$h() { - // Only execute if RemoveAlerts feature is enabled - if (!UtilityEnabled("RemoveAlerts")) { - return; - } - - // Only execute on contest pages - if (location.pathname !== "/contest.php") { - return; - } - - // Check if contest hasn't started yet - const centerElement = document.querySelector("body > div > div.mt-3 > center"); - if (centerElement && centerElement.innerHTML.indexOf("尚未开始比赛") !== -1) { - const contestLink = document.querySelector("body > div > div.mt-3 > center > a"); - const searchParams = new URLSearchParams(location.search); - const cid = searchParams.get("cid"); - - if (contestLink && cid) { - // Modify link to bypass alert - contestLink.setAttribute("href", `start_contest.php?cid=${cid}`); - } - } - } - - /** - * Replace Links Feature - * Replaces bracketed links with styled buttons - * Feature ID: ReplaceLinks - * Type: F (Format/UI) - * Description: 将网站中所有以方括号包装的链接替换为按钮 - */ - - - /** - * Initialize ReplaceLinks feature - * Replaces all links in format [text] with styled buttons - * - * Example transformation: - * [Problem 1001] - * -> - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 216-218: Link to button replacement - */ - function init$g() { - // Only execute if ReplaceLinks feature is enabled - if (!UtilityEnabled("ReplaceLinks")) { - return; - } - - // Replace all bracketed links with buttons - document.body.innerHTML = String(document.body.innerHTML).replaceAll( - /\[([^<]*)<\/a>\]/g, - '' - ); - } - - /** - * Auto O2 Feature - * Automatically enables O2 optimization flag for code submissions - * Feature ID: AutoO2 - * Type: U (Utility) - * Description: 自动启用O2编译优化标志 - */ - - - /** - * Initialize AutoO2 feature - * Automatically checks the "Enable O2" checkbox on problem submission pages - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 2020-2022: Auto-check O2 flag - */ - function init$f() { - // Only execute if AutoO2 feature is enabled - if (!UtilityEnabled("AutoO2")) { - return; - } - - // Only execute on problem pages - if (location.pathname !== "/problem.php") { - return; - } - - // Wait a bit for the page to be ready - setTimeout(() => { - const o2Checkbox = document.querySelector("#enable_O2"); - if (o2Checkbox) { - o2Checkbox.checked = true; - } - }, 100); - } - - /** - * Translate Feature - * Translates English text to Chinese throughout the site - * Feature ID: Translate - * Type: F (Format/UI) - * Description: 统一使用中文,翻译了部分英文 - */ - - - /** - * Initialize Translate feature - * Translates various English UI elements to Chinese based on current page - * - * Translations include: - * - Navbar: "Problems" -> "题库" - * - Problem set page: Form placeholders and table headers - * - Contest page: Table headers - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 211-213: Navbar translation - * - Lines 1073-1078: Problemset page translations - * - Lines 1611-1617: Contest page translations - */ - function init$e() { - // Only execute if Translate feature is enabled - if (!UtilityEnabled("Translate")) { - return; - } - - const pathname = location.pathname; - - // Translate navbar (on all pages) - translateNavbar(); - - // Page-specific translations - if (pathname === "/problemset.php") { - translateProblemsetPage(); - } else if (pathname === "/contest.php") { - translateContestPage(); - } - } - - /** - * Translate navbar elements - */ - function translateNavbar() { - try { - const problemsLink = document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a"); - if (problemsLink) { - problemsLink.innerText = "题库"; - } - } catch (e) { - console.error('[Translate] Error translating navbar:', e); - } - } - - /** - * Translate problemset page elements - */ - function translateProblemsetPage() { - try { - // Translate search form placeholders and buttons - const problemIdInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input"); - if (problemIdInput) { - problemIdInput.placeholder = "题目编号"; - } - - const confirmButton = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button"); - if (confirmButton) { - confirmButton.innerText = "确认"; - } - - const searchInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input"); - if (searchInput) { - searchInput.placeholder = "标题或内容"; - } - - // Translate table header - const statusHeader = document.querySelector("#problemset > thead > tr > th:nth-child(1)"); - if (statusHeader) { - statusHeader.innerText = "状态"; - } - } catch (e) { - console.error('[Translate] Error translating problemset page:', e); - } - } - - /** - * Translate contest page table headers - */ - function translateContestPage() { - try { - const tableHeader = document.querySelector("body > div > div.mt-3 > center > table > thead > tr"); - if (tableHeader && tableHeader.childNodes.length >= 4) { - tableHeader.childNodes[0].innerText = "编号"; - tableHeader.childNodes[1].innerText = "标题"; - tableHeader.childNodes[2].innerText = "状态"; - tableHeader.childNodes[3].remove(); - if (tableHeader.childNodes[3]) { - tableHeader.childNodes[3].innerText = "创建者"; - } - } - } catch (e) { - console.error('[Translate] Error translating contest page:', e); - } - } - - /** - * Auto Countdown Feature - * Automatically updates countdown timers on the page - * Feature ID: AutoCountdown - * Type: U (Utility) - * Description: 自动更新页面上的倒计时器 - */ - - - /** - * Initialize AutoCountdown feature - * Updates countdown timers with class "UpdateByJS" and reloads page when time expires - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 547-566: Countdown timer update logic - * - Lines 1592-1594: Disables default clock on contest page - */ - function init$d() { - // Only execute if AutoCountdown feature is enabled - if (!UtilityEnabled("AutoCountdown")) { - return; - } - - // Disable default clock on contest page - if (location.pathname === "/contest.php") { - window.clock = () => {}; - } - - // Update countdown timers - const updateCountdowns = () => { - const elements = document.getElementsByClassName("UpdateByJS"); - - for (let i = 0; i < elements.length; i++) { - const endTime = elements[i].getAttribute("EndTime"); - - if (endTime === null) { - elements[i].classList.remove("UpdateByJS"); - continue; - } - - const timeStamp = parseInt(endTime) - new Date().getTime(); - - // Reload page when countdown expires - if (timeStamp < 3000) { - elements[i].classList.remove("UpdateByJS"); - location.reload(); - } - - // Calculate remaining time - const currentDate = new Date(timeStamp); - const day = parseInt((timeStamp / 1000 / 60 / 60 / 24).toFixed(0)); - const hour = currentDate.getUTCHours(); - const minute = currentDate.getUTCMinutes(); - const second = currentDate.getUTCSeconds(); - - // Format and display countdown - elements[i].innerHTML = - (day !== 0 ? day + "天" : "") + - (hour !== 0 ? (hour < 10 ? "0" : "") + hour + "小时" : "") + - (minute !== 0 ? (minute < 10 ? "0" : "") + minute + "分" : "") + - (second !== 0 ? (second < 10 ? "0" : "") + second + "秒" : ""); - } - }; - - // Initial update - updateCountdowns(); - - // Update every second - setInterval(updateCountdowns, 1000); - } - - /** - * More STD Feature - * Adds standard solution links to contest problem tables - * Feature ID: MoreSTD - * Type: U (Utility) - * Description: 在比赛题目表格中添加标程链接 - */ - - - /** - * Initialize MoreSTD feature - * Reorganizes contest problem table to add standard solution links - * - * This feature: - * 1. Removes any existing "标程" column - * 2. Adds a new "标程" column header - * 3. Adds links to standard solutions for each problem - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 1699-1717: Standard solution column management - */ - function init$c() { - // Only execute if MoreSTD feature is enabled - if (!UtilityEnabled("MoreSTD")) { - return; - } - - // Only execute on contest pages - if (location.pathname !== "/contest.php") { - return; - } - - // Check if we're in a contest with problem list - const searchParams = new URLSearchParams(location.search); - if (!searchParams.get("cid")) { - return; - } - - // Wait for page to be ready - setTimeout(() => { - try { - const tableHeader = document.querySelector("#problemset > thead > tr"); - - // Only proceed if table exists and has "标程" column - if (!tableHeader || tableHeader.innerHTML.indexOf("标程") === -1) { - return; - } - - // Remove existing "标程" column - let headerCells = tableHeader.children; - for (let i = 0; i < headerCells.length; i++) { - if (headerCells[i].innerText === "标程") { - headerCells[i].remove(); - - // Remove corresponding cells from each row - const bodyRows = document.querySelector("#problemset > tbody").children; - for (let j = 0; j < bodyRows.length; j++) { - if (bodyRows[j].children[i] !== undefined) { - bodyRows[j].children[i].remove(); - } - } - } - } - - // Add new "标程" column header - tableHeader.innerHTML += '标程'; - - // Add standard solution links for each problem - const bodyRows = document.querySelector("#problemset > tbody").children; - const cid = Number(searchParams.get("cid")); - - for (let i = 0; i < bodyRows.length; i++) { - bodyRows[i].innerHTML += - `打开`; - } - } catch (error) { - console.error('[MoreSTD] Error adding standard solution links:', error); - } - }, 100); - } - - /** - * Export AC Code Feature - * Exports all accepted code solutions as a ZIP file - * Feature ID: ExportACCode - * Type: U (Utility) - * Description: 导出所有AC代码为ZIP文件 - */ - - - /** - * Initialize ExportACCode feature - * Adds a button to export all accepted code solutions - * - * This feature: - * 1. Adds an "导出AC代码" button to the user page - * 2. Fetches all AC code from export_ac_code.php - * 3. Creates a ZIP file with all accepted solutions - * 4. Uses JSZip library for compression - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 2445-2495: Export AC code button and logic - */ - function init$b() { - // Only execute if ExportACCode feature is enabled - if (!UtilityEnabled("ExportACCode")) { - return; - } - - // Only execute on user information page - if (location.pathname !== "/userinfo.php") { - return; - } - - // Wait for page to be ready - setTimeout(() => { - try { - const container = document.querySelector("body > div.container > div"); - if (!container) return; - - // Create export button - const exportButton = document.createElement("button"); - container.appendChild(exportButton); - exportButton.innerText = "导出AC代码"; - exportButton.className = "btn btn-outline-secondary"; - - // Add click handler - exportButton.addEventListener("click", () => { - exportButton.disabled = true; - exportButton.innerText = "正在导出..."; - - const request = new XMLHttpRequest(); - request.addEventListener("readystatechange", () => { - if (request.readyState === 4) { - if (request.status === 200) { - const response = request.responseText; - const acCode = response.split("------------------------------------------------------\r\n"); - - // Load JSZip library - const scriptElement = document.createElement("script"); - scriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; - document.head.appendChild(scriptElement); - - scriptElement.onload = () => { - const zip = new JSZip(); - - // Add each AC code file to ZIP - for (let i = 0; i < acCode.length; i++) { - let currentCode = acCode[i]; - if (currentCode !== "") { - const currentQuestionID = currentCode.substring(7, 11); - currentCode = currentCode.substring(14); - currentCode = currentCode.replaceAll("\r", ""); - zip.file(currentQuestionID + ".cpp", currentCode); - } - } - - exportButton.innerText = "正在生成压缩包……"; - zip.generateAsync({ type: "blob" }) - .then((content) => { - saveAs(content, "ACCodes.zip"); - exportButton.innerText = "AC代码导出成功"; - exportButton.disabled = false; - setTimeout(() => { - exportButton.innerText = "导出AC代码"; - }, 1000); - }); - }; - } else { - exportButton.disabled = false; - exportButton.innerText = "AC代码导出失败"; - setTimeout(() => { - exportButton.innerText = "导出AC代码"; - }, 1000); - } - } - }); - - request.open("GET", "https://www.xmoj.tech/export_ac_code.php", true); - request.send(); - }); - } catch (error) { - console.error('[ExportACCode] Error initializing export button:', error); - } - }, 100); - } - - /** - * Open All Problem Feature - * Adds buttons to open all problems or only unsolved problems in new tabs - * Feature ID: OpenAllProblem - * Type: U (Utility) - * Description: 添加按钮以在新标签页中打开所有题目或仅未解决的题目 - */ - - - /** - * Initialize OpenAllProblem feature - * Adds two buttons to contest pages: - * 1. "打开全部题目" - Opens all contest problems in new tabs - * 2. "打开未解决题目" - Opens only unsolved problems in new tabs - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 1817-1841: Open all problem buttons - */ - function init$a() { - // Only execute if OpenAllProblem feature is enabled - if (!UtilityEnabled("OpenAllProblem")) { - return; - } - - // Only execute on contest pages with problem list - if (location.pathname !== "/contest.php") { - return; - } - - const searchParams = new URLSearchParams(location.search); - if (!searchParams.get("cid")) { - return; - } - - // Wait for page to be ready - setTimeout(() => { - try { - // Find or create container for buttons - let cheatDiv = document.querySelector("#CheatDiv"); - if (!cheatDiv) { - return; - } - - // Create "Open All Problems" button - const openAllButton = document.createElement("button"); - openAllButton.className = "btn btn-outline-secondary"; - openAllButton.innerText = "打开全部题目"; - openAllButton.style.marginRight = "5px"; - cheatDiv.appendChild(openAllButton); - - openAllButton.addEventListener("click", () => { - const rows = document.querySelector("#problemset > tbody").rows; - for (let i = 0; i < rows.length; i++) { - open(rows[i].children[2].children[0].href, "_blank"); - } - }); - - // Create "Open Unsolved Problems" button - const openUnsolvedButton = document.createElement("button"); - openUnsolvedButton.className = "btn btn-outline-secondary"; - openUnsolvedButton.innerText = "打开未解决题目"; - cheatDiv.appendChild(openUnsolvedButton); - - openUnsolvedButton.addEventListener("click", () => { - const rows = document.querySelector("#problemset > tbody").rows; - for (let i = 0; i < rows.length; i++) { - // Only open problems that are not marked as solved (status_y) - if (!rows[i].children[0].children[0].classList.contains("status_y")) { - open(rows[i].children[2].children[0].href, "_blank"); - } - } - }); - } catch (error) { - console.error('[OpenAllProblem] Error initializing buttons:', error); - } - }, 100); - } - - /** - * Dark Mode Feature - * Enables dark theme for the website - * Feature ID: DarkMode - * Type: A (Appearance) - * Description: 启用网站深色主题 - */ - - - /** - * Initialize DarkMode feature - * Sets the Bootstrap theme to dark or light based on user preference - * - * Note: This feature also affects other parts of the application: - * - CodeMirror theme selection - * - Contest rank table text colors - * - Problem switcher background - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 247-251: Theme attribute setting - * - Used throughout the codebase for conditional styling (17 occurrences) - */ - function init$9() { - // Set theme based on DarkMode setting - if (UtilityEnabled("DarkMode")) { - document.querySelector("html").setAttribute("data-bs-theme", "dark"); - } else { - document.querySelector("html").setAttribute("data-bs-theme", "light"); - } - } - - /** - * Improve AC Rate Feature - * Adds a button to resubmit already-AC'd problems to improve submission statistics - * Feature ID: ImproveACRate - * Type: U (Utility) - * Description: 添加按钮来重新提交已AC的题目以提高正确率 - */ - - - /** - * Initialize ImproveACRate feature - * Adds a "提高正确率" button that resubmits already-AC'd problems - * - * This feature: - * 1. Fetches user's AC problems from userinfo page - * 2. Displays current AC rate percentage - * 3. On click, randomly selects 3 AC'd problems and resubmits them - * 4. Uses existing AC code from status page - * - * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js - * - Lines 1405-1463: Improve AC rate button and logic - */ - function init$8() { - // Only execute if ImproveACRate feature is enabled - if (!UtilityEnabled("ImproveACRate")) { - return; - } - - // Only execute on status page - if (location.pathname !== "/status.php") { - return; - } - - // Need current username - const currentUsername = document.querySelector("#profile")?.innerText; - if (!currentUsername || currentUsername === "登录") { - return; - } - - // Wait for page to be ready - setTimeout(async () => { - try { - const container = document.querySelector("body > div.container > div > div.input-append"); - if (!container) return; - - // Create improve AC rate button - const improveACRateButton = document.createElement("button"); - container.appendChild(improveACRateButton); - improveACRateButton.className = "btn btn-outline-secondary"; - improveACRateButton.innerText = "提高正确率"; - improveACRateButton.disabled = true; - - // Fetch user's AC problems - let acProblems = []; - await fetch(`https://www.xmoj.tech/userinfo.php?user=${currentUsername}`) - .then((response) => response.text()) - .then((response) => { - const parsedDocument = new DOMParser().parseFromString(response, "text/html"); - - // Calculate and display AC rate - const acCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText); - const submitCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText); - const acRate = (acCount / submitCount * 100).toFixed(2); - improveACRateButton.innerText += ` (${acRate}%)`; - - // Extract AC problem IDs - const scriptContent = parsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); - for (let i = 0; i < scriptContent.length; i++) { - const problemId = Number(scriptContent[i].substring(2, scriptContent[i].indexOf(","))); - if (!isNaN(problemId)) { - acProblems.push(problemId); - } - } - - improveACRateButton.disabled = false; - }); - - // Add click handler - improveACRateButton.addEventListener("click", async () => { - improveACRateButton.disabled = true; - const submitTimes = 3; - let count = 0; - - const submitInterval = setInterval(async () => { - if (count >= submitTimes) { - clearInterval(submitInterval); - location.reload(); - return; - } - - improveACRateButton.innerText = `正在提交 (${count + 1}/${submitTimes})`; - - // Randomly select an AC'd problem - const pid = acProblems[Math.floor(Math.random() * acProblems.length)]; - - // Get a solution ID for this problem - let sid = 0; - await fetch(`https://www.xmoj.tech/status.php?problem_id=${pid}&jresult=4`) - .then((result) => result.text()) - .then((result) => { - const parsedDocument = new DOMParser().parseFromString(result, "text/html"); - sid = parsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; - }); - - // Get the source code - let code = ""; - await fetch(`https://www.xmoj.tech/getsource.php?id=${sid}`) - .then((response) => response.text()) - .then((response) => { - code = response.substring(0, response.indexOf("/**************************************************************")).trim(); - }); - - // Resubmit the code - await fetch("https://www.xmoj.tech/submit.php", { - headers: { - "content-type": "application/x-www-form-urlencoded" - }, - referrer: `https://www.xmoj.tech/submitpage.php?id=${pid}`, - method: "POST", - body: `id=${pid}&language=1&source=${encodeURIComponent(code)}&enable_O2=on` - }); - - count++; - }, 1000); - }); - - // Style the button - improveACRateButton.style.marginBottom = "7px"; - improveACRateButton.style.marginRight = "7px"; - } catch (error) { - console.error('[ImproveACRate] Error initializing button:', error); - } - }, 100); - } - - /** - * Feature loader - Initializes all extracted feature modules - * - * This module provides a centralized way to initialize all feature modules. - * Features are loaded conditionally based on UtilityEnabled settings and - * current page URL. - */ - - - /** - * Initialize all feature modules - * Features will self-check if they should be active based on UtilityEnabled - * - * @param {Object} context - Shared context object with dependencies - * @param {string} context.CurrentUsername - Current logged-in username - * @param {URLSearchParams} context.SearchParams - URL search parameters - * @param {boolean} context.IsAdmin - Whether user is admin - * @param {HTMLStyleElement} context.Style - Style element for adding CSS - * @param {string} context.CaptchaSiteKey - Cloudflare Turnstile site key - * @param {Function} context.GetUsernameHTML - Function to render usernames - * @param {Function} context.PurifyHTML - Function to sanitize HTML - * @param {Function} context.RenderMathJax - Function to render MathJax - */ - async function initializeFeatures(context) { - try { - // Initialize features that need to run early (before main page load) - init$r(); - - // Initialize theme (must run early) - init$9(); - - // Initialize features that clean up/modify the page - init$n(); - init$h(); - - // Initialize cosmetic/styling features - init$k(); - init$j(); - - // Initialize text replacement features - init$m(); - init$l(); - init$g(); - init$e(); - - // Initialize utility features - init$d(); - init$c(); - init$a(); - init$8(); - - // Initialize page-specific features - init$p(); - init$i(); - init$f(); - init$b(); - await init$o(); - - // Initialize complex features that need context - if (context) ; - - console.log('[XMOJ-Script] Feature modules initialized'); - } catch (error) { - console.error('[XMOJ-Script] Error initializing features:', error); - } - } - - /** - * Get list of all extracted features - * Useful for debugging and feature management - */ - function getExtractedFeatures() { - return [ - 'AutoLogin', - 'Discussion', - 'CopySamples', - 'CompareSource', - 'RemoveUseless', - 'ReplaceXM', - 'ReplaceYN', - 'AddAnimation', - 'AddColorText', - 'SavePassword', - 'RemoveAlerts', - 'ReplaceLinks', - 'AutoO2', - 'Translate', - 'AutoCountdown', - 'MoreSTD', - 'ExportACCode', - 'OpenAllProblem', - 'DarkMode', - 'ImproveACRate', - ]; - } - - /** - * Problem Page Module - * Handles all styling and functionality for /problem.php - */ - - - /** - * Initialize problem page - * @param {Object} context - Page context with utilities - */ - async function init$7(context) { - const { SearchParams, RenderMathJax, RequestAPI, Style } = context; - - // Render MathJax - await RenderMathJax(); - - // Check if problem doesn't exist - if (document.querySelector("body > div > div.mt-3 > h2") != null) { - document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; - setTimeout(() => { - location.href = "https://www.xmoj.tech/problemset.php"; - }, 1000); - return; - } - - const PID = SearchParams.get("cid") - ? localStorage.getItem(`UserScript-Contest-${SearchParams.get("cid")}-Problem-${SearchParams.get("pid")}-PID`) - : SearchParams.get("id"); - - // Fix spacing - if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { - document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; - } - - // Fix submit button - fixSubmitButton(); - - // Style sample data cards - const sampleDataElements = document.querySelectorAll(".sampledata"); - for (let i = 0; i < sampleDataElements.length; i++) { - sampleDataElements[i].parentElement.className = "card"; - } - - // Handle IO file information - handleIOFile(PID); - - // Add discussion button (if Discussion feature is enabled) - if (UtilityEnabled("Discussion")) { - addDiscussionButton(PID, SearchParams, RequestAPI); - } - - // Tidy tables - const tables = document.getElementsByTagName("table"); - for (let i = 0; i < tables.length; i++) { - TidyTable(tables[i]); - } - - // Add custom styles - addPageStyles$1(Style); - } - - /** - * Fix submit button styling and behavior - */ - function fixSubmitButton() { - // Try multiple selectors to find the submit link (it keeps moving position) - const selectors = [ - '.mt-3 > center:nth-child(1) > a:nth-child(12)', - '.mt-3 > center:nth-child(1) > a:nth-child(10)', - '.mt-3 > center:nth-child(1) > a:nth-child(11)', - '.mt-3 > center:nth-child(1) > a:nth-child(13)', - '.mt-3 > center:nth-child(1) > a:nth-child(9)', - '.mt-3 > center:nth-child(1) > a:nth-child(7)', - '.mt-3 > center:nth-child(1) > a:nth-child(8)', - ]; - - let submitLink = null; - for (const selector of selectors) { - submitLink = document.querySelector(selector); - if (submitLink) break; - } - - if (!submitLink) return; - - // Create submit button - const submitButton = document.createElement('button'); - submitButton.id = 'SubmitButton'; - submitButton.className = 'btn btn-outline-secondary'; - submitButton.textContent = '提交'; - submitButton.onclick = function () { - window.location.href = submitLink.href; - }; - - // Replace the link with the button - submitLink.parentNode.replaceChild(submitButton, submitLink); - - // Remove the button's outer brackets - const container = document.querySelector('.mt-3 > center:nth-child(1)'); - if (container) { - let str = container.innerHTML; - let target = submitButton.outerHTML; - let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); - container.innerHTML = result; - - // Re-attach click handler after innerHTML replacement - const newButton = document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary'); - if (newButton) { - newButton.onclick = function () { - window.location.href = submitLink.href; - }; - } - } - } - - /** - * Handle IO file information display - * @param {string} PID - Problem ID - */ - function handleIOFile(PID) { - const ioFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); - if (!ioFileElement) return; - - // Move child nodes out of h3 - while (ioFileElement.childNodes.length >= 1) { - ioFileElement.parentNode.insertBefore(ioFileElement.childNodes[0], ioFileElement); - } - ioFileElement.parentNode.insertBefore(document.createElement("br"), ioFileElement); - ioFileElement.remove(); - - // Extract and store IO filename - const centerNode = document.querySelector("body > div > div.mt-3 > center"); - if (centerNode && centerNode.childNodes[2]) { - const temp = centerNode.childNodes[2].data.trim(); - const ioFilename = temp.substring(0, temp.length - 3); - localStorage.setItem(`UserScript-Problem-${PID}-IOFilename`, ioFilename); - } - } - - /** - * Add discussion button with unread badge - * @param {string} PID - Problem ID - * @param {URLSearchParams} SearchParams - URL search parameters - * @param {Function} RequestAPI - API request function - */ - function addDiscussionButton(PID, SearchParams, RequestAPI) { - const discussButton = document.createElement("button"); - discussButton.className = "btn btn-outline-secondary position-relative"; - discussButton.innerHTML = `讨论`; - discussButton.style.marginLeft = "10px"; - discussButton.type = "button"; - - discussButton.addEventListener("click", () => { - const problemId = SearchParams.get("cid") ? PID : SearchParams.get("id"); - open(`https://www.xmoj.tech/discuss3/discuss.php?pid=${problemId}`, "_blank"); - }); - - document.querySelector("body > div > div.mt-3 > center").appendChild(discussButton); - - // Add unread badge - const unreadBadge = document.createElement("span"); - unreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; - unreadBadge.style.display = "none"; - discussButton.appendChild(unreadBadge); - - // Refresh unread count - const refreshCount = () => { - RequestAPI("GetPostCount", { - "ProblemID": Number(PID) - }, (response) => { - if (response.Success && response.Data.DiscussCount != 0) { - unreadBadge.innerText = response.Data.DiscussCount; - unreadBadge.style.display = ""; - } - }); - }; - - refreshCount(); - addEventListener("focus", refreshCount); - } - - /** - * Add custom page styles - * @param {HTMLStyleElement} Style - Style element to append to - */ - function addPageStyles$1(Style) { - Style.innerHTML += ` -code, kbd, pre, samp { - font-family: monospace, Consolas, 'Courier New'; - font-size: 1rem; -} -pre { - padding: 0.3em 0.5em; - margin: 0.5em 0; -} -.in-out { - overflow: hidden; - display: flex; - padding: 0.5em 0; -} -.in-out .in-out-item { - flex: 1; -}`; - } - - /** - * Contest Page Module - * Handles all styling and functionality for /contest.php - */ - - - /** - * Initialize contest page - * @param {Object} context - Page context with utilities - */ - async function init$6(context) { - const { SearchParams } = context; - - // Check if viewing specific contest or contest list - if (location.href.indexOf("?cid=") === -1) { - // Contest list page - initContestList(); - } else { - // Specific contest page - initContestView(SearchParams); - } - } - - /** - * Initialize contest list view - */ - function initContestList() { - // Style contest list table rows - const contestRows = document.querySelector("body > div > div.mt-3 > center > table > tbody")?.childNodes; - if (!contestRows) return; - - for (let i = 1; i < contestRows.length; i++) { - const currentElement = contestRows[i].childNodes[2]?.childNodes; - if (!currentElement) continue; - - // Handle different contest states - if (currentElement[1]?.childNodes[0]?.data?.indexOf("运行中") !== -1) { - handleRunningContest(currentElement); - } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("开始于") !== -1) { - handleUpcomingContest(currentElement); - } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("已结束") !== -1) { - handleFinishedContest(currentElement); - } - - // Hide column and add user link - contestRows[i].childNodes[3].style.display = "none"; - const creator = contestRows[i].childNodes[4].innerHTML; - contestRows[i].childNodes[4].innerHTML = `${creator}`; - - // Store contest name - const contestId = contestRows[i].childNodes[0].innerText; - const contestName = contestRows[i].childNodes[1].innerText; - localStorage.setItem(`UserScript-Contest-${contestId}-Name`, contestName); - } - } - - /** - * Handle running contest countdown - * @param {NodeList} element - Contest row element - */ - function handleRunningContest(element) { - const time = String(element[1].childNodes[1].innerText).substring(4); - - // Parse time components - const day = parseInt(time.substring(0, time.indexOf("天"))) || 0; - const hourStart = time.indexOf("天") === -1 ? 0 : time.indexOf("天") + 1; - const hour = parseInt(time.substring(hourStart, time.indexOf("小时"))) || 0; - const minuteStart = time.indexOf("小时") === -1 ? 0 : time.indexOf("小时") + 2; - const minute = parseInt(time.substring(minuteStart, time.indexOf("分"))) || 0; - const secondStart = time.indexOf("分") === -1 ? 0 : time.indexOf("分") + 1; - const second = parseInt(time.substring(secondStart, time.indexOf("秒"))) || 0; - - // Calculate timestamp - const diff = window.diff || 0; // Global time diff - const timeStamp = new Date().getTime() + diff + ((((day * 24 + hour) * 60 + minute) * 60 + second) * 1000); - - element[1].childNodes[1].setAttribute("EndTime", timeStamp); - element[1].childNodes[1].classList.add("UpdateByJS"); - } - - /** - * Handle upcoming contest - * @param {NodeList} element - Contest row element - */ - function handleUpcomingContest(element) { - const diff = window.diff || 0; - const timeStamp = Date.parse(String(element[1].childNodes[0].data).substring(4)) + diff; - element[1].setAttribute("EndTime", timeStamp); - element[1].classList.add("UpdateByJS"); - } - - /** - * Handle finished contest - * @param {NodeList} element - Contest row element - */ - function handleFinishedContest(element) { - const timeStamp = String(element[1].childNodes[0].data).substring(4); - element[1].childNodes[0].data = " 已结束 "; - element[1].className = "red"; - - const span = document.createElement("span"); - span.className = "green"; - span.innerHTML = timeStamp; - element[1].appendChild(span); - } - - /** - * Initialize specific contest view - * @param {URLSearchParams} SearchParams - URL search parameters - */ - function initContestView(SearchParams) { - // Update title - const title = document.getElementsByTagName("h3")[0]; - if (title) { - title.innerHTML = "比赛" + title.innerHTML.substring(7); - } - - // Handle countdown timer - const timeLeft = document.querySelector("#time_left"); - if (timeLeft) { - const centerNode = document.querySelector("body > div > div.mt-3 > center"); - let endTimeText = centerNode?.childNodes[3]?.data; - - if (endTimeText) { - endTimeText = endTimeText.substring(endTimeText.indexOf("结束时间是:") + 6, endTimeText.lastIndexOf("。")); - const endTime = new Date(endTimeText).getTime(); - - if (new Date().getTime() < endTime) { - timeLeft.classList.add("UpdateByJS"); - timeLeft.setAttribute("EndTime", endTime); - } - } - } - - // Format contest information - const infoDiv = document.querySelector("body > div > div.mt-3 > center > div"); - if (infoDiv) { - let htmlData = infoDiv.innerHTML; - htmlData = htmlData.replaceAll("  \n  ", " "); - htmlData = htmlData.replaceAll("
    开始于: ", "开始时间:"); - htmlData = htmlData.replaceAll("\n结束于: ", "
    结束时间:"); - htmlData = htmlData.replaceAll("\n订正截止日期: ", "
    订正截止日期:"); - htmlData = htmlData.replaceAll("\n现在时间: ", "当前时间:"); - htmlData = htmlData.replaceAll("\n状态:", "
    状态:"); - infoDiv.innerHTML = htmlData; - } - - // Format problem list - formatProblemList(); - - // Store problem count - const problemCount = document.querySelector("#problemset > tbody")?.rows.length; - if (problemCount) { - localStorage.setItem(`UserScript-Contest-${SearchParams.get("cid")}-ProblemCount`, problemCount); - } - } - - /** - * Format problem list in contest - */ - function formatProblemList() { - const tbody = document.querySelector("#problemset > tbody"); - if (!tbody) return; - - // Format problem names - tbody.innerHTML = tbody.innerHTML.replaceAll( - /\t ([0-9]*)      问题  ([^<]*)/g, - "$2. $1" - ); - tbody.innerHTML = tbody.innerHTML.replaceAll( - /\t\*([0-9]*)      问题  ([^<]*)/g, - "拓展$2. $1" - ); - - // Ensure status divs exist - const rows = tbody.rows; - for (let i = 0; i < rows.length; i++) { - if (rows[i].childNodes[0]?.children.length === 0) { - rows[i].childNodes[0].innerHTML = '
    '; - } - - // Make problem title link open in new tab - const titleLink = rows[i].children[2]?.children[0]; - if (titleLink) { - titleLink.target = "_blank"; - } - } - } - - /** - * Status Page Module - * Handles all styling and functionality for /status.php - */ - - /** - * Initialize status page - * @param {Object} context - Page context with utilities - */ - async function init$5(context) { - const { SearchParams } = context; - - // Only proceed if not in special UserScript mode - if (SearchParams.get("ByUserScript") !== null) { - return; - } - - // Set page title - document.title = "提交状态"; - - // Remove old script tags - const oldScript = document.querySelector("body > script:nth-child(5)"); - if (oldScript) { - oldScript.remove(); - } - - // Additional status page initialization can go here - // Most status page features are handled by feature modules - } - - /** - * Submit Page Module - * Handles all styling and functionality for /submitpage.php - */ - - /** - * Initialize submit page - * @param {Object} context - Page context with utilities - */ - async function init$4(context) { - const { SearchParams } = context; - - // Set page title - const problemId = SearchParams.get("id"); - const contestId = SearchParams.get("cid"); - - if (problemId) { - document.title = `提交代码: 题目${Number(problemId)}`; - } else if (contestId) { - String.fromCharCode(65 + parseInt(SearchParams.get("pid"))); - document.title = `提交代码: 比赛${Number(contestId)}`; - } - - // Additional submit page initialization can go here - // Most submit page features are handled by feature modules and CodeMirror initialization - } - - /** - * Problemset Page Module - * Handles all styling and functionality for /problemset.php - */ - - - /** - * Initialize problemset page - * @param {Object} context - Page context with utilities - */ - async function init$3(context) { - const { SearchParams } = context; - - // Set column widths - if (UtilityEnabled("ResetType")) { - setColumnWidths(); - } - - // Replace search forms with improved layout - replaceSearchForms(SearchParams); - - // Store problem names in localStorage - storeProblemNames(); - } - - /** - * Set column widths for problemset table - */ - function setColumnWidths() { - try { - const headers = document.querySelectorAll("#problemset > thead > tr > th"); - if (headers.length >= 5) { - headers[0].style.width = "5%"; // Status - headers[1].style.width = "10%"; // ID - headers[2].style.width = "75%"; // Title - headers[3].style.width = "5%"; // AC ratio - headers[4].style.width = "5%"; // Difficulty - } - } catch (error) { - console.error('[Problemset] Error setting column widths:', error); - } - } - - /** - * Replace search forms with improved layout - * @param {URLSearchParams} SearchParams - URL search parameters - */ - function replaceSearchForms(SearchParams) { - try { - const oldTable = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)"); - if (!oldTable) return; - - oldTable.outerHTML = ` -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    `; - - // Restore search value if present - const searchParam = SearchParams.get("search"); - if (searchParam) { - const searchInput = document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input"); - if (searchInput) { - searchInput.value = searchParam; - } - } - } catch (error) { - console.error('[Problemset] Error replacing search forms:', error); - } - } - - /** - * Store problem names in localStorage for quick access - */ - function storeProblemNames() { - try { - const rows = document.querySelector("#problemset")?.rows; - if (!rows) return; - - for (let i = 1; i < rows.length; i++) { - const problemId = rows[i].children[1]?.innerText; - const problemName = rows[i].children[2]?.innerText; - - if (problemId && problemName) { - localStorage.setItem(`UserScript-Problem-${problemId}-Name`, problemName); - } - } - } catch (error) { - console.error('[Problemset] Error storing problem names:', error); - } - } - - /** - * User Info Page Module - * Handles all styling and functionality for /userinfo.php - */ - - - /** - * Initialize user info page - * @param {Object} context - Page context with utilities - */ - async function init$2(context) { - const { SearchParams, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin } = context; - - // Check if in ByUserScript mode (upload standard solution) - if (SearchParams.get("ByUserScript") !== null) { - document.title = "上传标程"; - // Upload standard solution UI is handled in bootstrap.js - return; - } - - // Clean up submission section if RemoveUseless is enabled - if (UtilityEnabled("RemoveUseless")) { - cleanupSubmissionSection(); - } - - // Execute embedded script (chart initialization) - executeEmbeddedScript(); - - // Translate table headers - translateTableHeaders(); - - // Extract and display user information - await displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); - } - - /** - * Clean up submission section - */ - function cleanupSubmissionSection() { - try { - const submissionElement = document.getElementById("submission"); - if (submissionElement) { - const childNodes = submissionElement.childNodes; - for (let i = childNodes.length - 1; i >= 0; i--) { - childNodes[i].remove(); - } - } - } catch (error) { - console.error('[UserInfo] Error cleaning up submission section:', error); - } - } - - /** - * Execute embedded chart script - */ - function executeEmbeddedScript() { - try { - const scriptElement = document.querySelector("body > script:nth-child(5)"); - if (scriptElement) { - eval(scriptElement.innerHTML); - } - } catch (error) { - console.error('[UserInfo] Error executing embedded script:', error); - } - } - - /** - * Translate table headers - */ - function translateTableHeaders() { - try { - // Remove first row - const firstRow = document.querySelector("#statics > tbody > tr:nth-child(1)"); - if (firstRow) { - firstRow.remove(); - } - - // Translate remaining headers - const rows = document.querySelector("#statics > tbody")?.children; - if (!rows) return; - - for (let i = 0; i < rows.length; i++) { - if (rows[i].children[0]) { - const headerText = rows[i].children[0].innerText; - if (headerText === "Statistics") { - rows[i].children[0].innerText = "统计"; - } else if (headerText === "Email:") { - rows[i].children[0].innerText = "电子邮箱"; - } - rows[i].children[1].removeAttribute("align"); - } - } - } catch (error) { - console.error('[UserInfo] Error translating table headers:', error); - } - } - - /** - * Display user profile with avatar and solved problems - * @param {Function} GetUserInfo - Function to get user info - * @param {Function} GetUserBadge - Function to get user badge - * @param {Function} GetRelativeTime - Function to format relative time - * @param {Function} RequestAPI - Function to make API requests - * @param {Function} SmartAlert - Function to show alerts - * @param {boolean} IsAdmin - Whether current user is admin - */ - async function displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { - try { - // Extract AC problems - const acCell = document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)"); - const acProblems = []; - - if (acCell) { - const childNodes = acCell.childNodes; - for (let i = 0; i < childNodes.length; i++) { - if (childNodes[i].tagName === "A" && childNodes[i].href.indexOf("problem.php?id=") !== -1) { - acProblems.push(Number(childNodes[i].innerText.trim())); - } - } - acCell.remove(); - } - - // Extract user info from caption - const caption = document.querySelector("#statics > caption"); - if (!caption) return; - - const captionText = caption.childNodes[0].data.trim(); - const [userId, userNick] = captionText.split("--"); - caption.remove(); - - // Set page title - document.title = `用户 ${userId} 的个人中心`; - - // Create new layout - await createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); - } catch (error) { - console.error('[UserInfo] Error displaying user profile:', error); - } - } - - /** - * Create user profile layout with avatar, info, and solved problems - */ - async function createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { - // Create main row - const row = document.createElement("div"); - row.className = "row"; - - // Left column - const leftDiv = document.createElement("div"); - leftDiv.className = "col-md-5"; - row.appendChild(leftDiv); - - // Avatar and user info - const leftTopDiv = document.createElement("div"); - leftTopDiv.className = "row mb-2"; - leftDiv.appendChild(leftTopDiv); - - // Avatar - const avatarContainer = document.createElement("div"); - avatarContainer.classList.add("col-auto"); - const avatarElement = document.createElement("img"); - - const userInfo = await GetUserInfo(userId); - const emailHash = userInfo?.EmailHash; - - if (!emailHash) { - avatarElement.src = `https://cravatar.cn/avatar/00000000000000000000000000000000?d=mp&f=y`; - } else { - avatarElement.src = `https://cravatar.cn/avatar/${emailHash}?d=retro`; - } - - avatarElement.classList.add("rounded", "me-2"); - avatarElement.style.height = "120px"; - avatarContainer.appendChild(avatarElement); - leftTopDiv.appendChild(avatarContainer); - - // User info - const userInfoElement = document.createElement("div"); - userInfoElement.classList.add("col-auto"); - userInfoElement.style.lineHeight = "40px"; - userInfoElement.innerHTML += `用户名:${userId}
    `; - userInfoElement.innerHTML += `昵称:${userNick}
    `; - - if (UtilityEnabled("Rating")) { - userInfoElement.innerHTML += `评分:${userInfo?.Rating || 'N/A'}
    `; - } - - // Last online time (async) - const lastOnlineElement = document.createElement('div'); - lastOnlineElement.innerHTML = "最后在线:加载中...
    "; - userInfoElement.appendChild(lastOnlineElement); - - RequestAPI("LastOnline", { "Username": userId }, (result) => { - if (result.Success) { - lastOnlineElement.innerHTML = `最后在线:${GetRelativeTime(result.Data.logintime)}
    `; - } else { - lastOnlineElement.innerHTML = "最后在线:近三个月内从未
    "; - } - }); - - // Badge management buttons (admin only) - if (IsAdmin) { - await addBadgeManagement(userId, userInfoElement, GetUserBadge, RequestAPI, SmartAlert); - } - - leftTopDiv.appendChild(userInfoElement); - - // Move statistics table to left column - const leftTable = document.querySelector("body > div > div > center > table"); - if (leftTable) { - leftDiv.appendChild(leftTable); - } - - // Right column - AC problems - const rightDiv = document.createElement("div"); - rightDiv.className = "col-md-7"; - row.appendChild(rightDiv); - rightDiv.innerHTML = "
    已解决题目
    "; - - for (const problemId of acProblems) { - rightDiv.innerHTML += `${problemId} `; - } - - // Replace page content - const contentDiv = document.querySelector("body > div > div"); - if (contentDiv) { - contentDiv.innerHTML = ""; - contentDiv.appendChild(row); - } - } - - /** - * Add badge management buttons for admins - */ - async function addBadgeManagement(userId, container, GetUserBadge, RequestAPI, SmartAlert) { - const badgeInfo = await GetUserBadge(userId); - - if (badgeInfo.Content !== "") { - // Delete badge button - const deleteBadgeButton = document.createElement("button"); - deleteBadgeButton.className = "btn btn-outline-danger btn-sm"; - deleteBadgeButton.innerText = "删除标签"; - deleteBadgeButton.addEventListener("click", async () => { - if (confirm("您确定要删除此标签吗?")) { - RequestAPI("DeleteBadge", { "UserID": userId }, (response) => { - if (response.Success) { - clearBadgeCache(userId); - window.location.reload(); - } else { - SmartAlert(response.Message); - } - }); - } - }); - container.appendChild(deleteBadgeButton); - } else { - // Add badge button - const addBadgeButton = document.createElement("button"); - addBadgeButton.className = "btn btn-outline-primary btn-sm"; - addBadgeButton.innerText = "添加标签"; - addBadgeButton.addEventListener("click", async () => { - RequestAPI("NewBadge", { "UserID": userId }, (response) => { - if (response.Success) { - clearBadgeCache(userId); - window.location.reload(); - } else { - SmartAlert(response.Message); - } - }); - }); - container.appendChild(addBadgeButton); - } - } - - /** - * Clear badge cache for a user - * @param {string} userId - User ID - */ - function clearBadgeCache(userId) { - const keysToRemove = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key.startsWith(`UserScript-User-${userId}-Badge-`)) { - keysToRemove.push(key); - } - } - for (const key of keysToRemove) { - localStorage.removeItem(key); - } - } - - /** - * Login Page Module - * Handles all styling and functionality for /loginpage.php - * - * Note: Login functionality is handled by LoginFailed and SavePassword features - * This module only handles page styling - */ - - - /** - * Initialize login page - * @param {Object} context - Page context with utilities - */ - async function init$1(context) { - // Replace login form with Bootstrap-styled version - if (UtilityEnabled("NewBootstrap")) { - replaceLoginForm(); - } - - // Login handling and SavePassword are handled by feature modules - } - - /** - * Replace login form with modern Bootstrap styling - */ - function replaceLoginForm() { - try { - const loginForm = document.querySelector("#login"); - if (!loginForm) return; - - loginForm.innerHTML = `
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    `; - } catch (error) { - console.error('[Login] Error replacing login form:', error); - } - } - - /** - * Contest Rank Pages Module - * Handles all styling and functionality for /contestrank-oi.php and /contestrank-correct.php - */ - - - /** - * Initialize contest rank page - * @param {Object} context - Page context with utilities - */ - async function init(context) { - const { SearchParams, TidyTable, GetUsernameHTML, Style } = context; - - const pathname = location.pathname; - const isOI = pathname === "/contestrank-oi.php"; - const isCorrect = pathname === "/contestrank-correct.php"; - - // Create rank table if doesn't exist - if (document.querySelector("#rank") === null) { - document.querySelector("body > div > div.mt-3").innerHTML = - '

    比赛排名

    '; - } - - // Check if in UserScript mode - const byUserScript = SearchParams.get("ByUserScript") !== null; - - // Handle title and headers - const titleElement = document.querySelector("body > div > div.mt-3 > center > h3"); - if (titleElement.innerText === "比赛排名") { - document.querySelector("#rank").innerText = "比赛暂时还没有排名"; - } else { - if (isOI && !byUserScript) { - await initOIRanking(titleElement, TidyTable, GetUsernameHTML); - } else if (isCorrect) { - initCorrectRanking(titleElement, TidyTable, GetUsernameHTML); - } - } - - // Add page styles - addPageStyles(Style); - - // Hide link and set title - const linkElement = document.querySelector("body > div.container > div > center > a"); - if (linkElement) { - linkElement.style.display = "none"; - } - - const centerDiv = document.querySelector("body > div.container > div > center"); - if (centerDiv) { - centerDiv.style.paddingBottom = "10px"; - } - - document.title = titleElement.innerText; - } - - /** - * Initialize OI ranking table - */ - async function initOIRanking(titleElement, TidyTable, GetUsernameHTML) { - // Update title - const originalTitle = titleElement.innerText; - titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(OI排名)"; - - // Translate headers - translateRankHeaders(); - - // Refresh ranking function - const refreshOIRank = async () => { - await fetch(location.href) - .then((response) => response.text()) - .then(async (response) => { - const parsedDocument = new DOMParser().parseFromString(response, "text/html"); - TidyTable(parsedDocument.getElementById("rank")); - - const rows = parsedDocument.getElementById("rank").rows; - for (let i = 1; i < rows.length; i++) { - // Add medal badge - const metalCell = rows[i].cells[0]; - const metal = document.createElement("span"); - metal.innerText = metalCell.innerText; - metal.className = "badge text-bg-primary"; - metalCell.innerText = ""; - metalCell.appendChild(metal); - - // Format username - GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); - - // Style problem cells - for (let j = 5; j < rows[i].cells.length; j++) { - styleProblemCell(rows[i].cells[j]); - } - } - - // Update DOM - document.querySelector("#rank > tbody").innerHTML = parsedDocument.querySelector("#rank > tbody").innerHTML; - }); - }; - - // Initial refresh - await refreshOIRank(); - - // Auto-refresh on focus if enabled - if (UtilityEnabled("AutoRefresh")) { - addEventListener("focus", refreshOIRank); - } - } - - /** - * Initialize correct ranking table - */ - function initCorrectRanking(titleElement, TidyTable, GetUsernameHTML) { - // Update title - if (UtilityEnabled("ResetType")) { - const originalTitle = titleElement.innerText; - titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(订正排名)"; - - const linkElement = document.querySelector("body > div > div.mt-3 > center > a"); - if (linkElement) { - linkElement.remove(); - } - } - - // Translate headers - translateRankHeaders(); - - // Style rows - TidyTable(document.getElementById("rank")); - const rows = document.getElementById("rank").rows; - - for (let i = 1; i < rows.length; i++) { - // Add medal badge - const metalCell = rows[i].cells[0]; - const metal = document.createElement("span"); - metal.innerText = metalCell.innerText; - metal.className = "badge text-bg-primary"; - metalCell.innerText = ""; - metalCell.appendChild(metal); - - // Format username - GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); - - // Style problem cells - for (let j = 5; j < rows[i].cells.length; j++) { - styleProblemCell(rows[i].cells[j]); - } - } - } - - /** - * Translate rank table headers - */ - function translateRankHeaders() { - try { - const headers = document.querySelectorAll("#rank > thead > tr > th"); - if (headers.length >= 5) { - headers[0].innerText = "排名"; - headers[1].innerText = "用户"; - headers[2].innerText = "昵称"; - headers[3].innerText = "AC数"; - headers[4].innerText = "得分"; - } - } catch (error) { - console.error('[ContestRank] Error translating headers:', error); - } - } - - /** - * Style problem cell based on status - * @param {HTMLTableCellElement} cell - Table cell to style - */ - function styleProblemCell(cell) { - let innerText = cell.innerText; - let backgroundColor = cell.style.backgroundColor; - - // Parse RGB values - const red = parseInt(backgroundColor.substring(4, backgroundColor.indexOf(","))); - const green = parseInt(backgroundColor.substring(backgroundColor.indexOf(",") + 2, backgroundColor.lastIndexOf(","))); - const blue = parseInt(backgroundColor.substring(backgroundColor.lastIndexOf(",") + 2, backgroundColor.lastIndexOf(")"))); - - const noData = (red === 238 && green === 238 && blue === 238); - const firstBlood = (red === 170 && green === 170 && blue === 255); - const solved = (green === 255); - - let errorCount = 0; - if (solved) { - errorCount = (blue === 170 ? 5 : (blue - 51) / 32); - } else { - errorCount = (blue === 22 ? 15 : (170 - blue) / 10); - } - - // Apply styling - if (noData) { - backgroundColor = ""; - } else if (firstBlood) { - backgroundColor = "rgb(127, 127, 255)"; - } else if (solved) { - backgroundColor = `rgb(0, 255, 0, ${Math.max(1 / 10 * (10 - errorCount), 0.2)})`; - if (errorCount !== 0) { - innerText += ` (${errorCount === 5 ? "4+" : errorCount})`; - } - } else { - backgroundColor = `rgba(255, 0, 0, ${Math.min(errorCount / 10 + 0.2, 1)})`; - if (errorCount !== 0) { - innerText += ` (${errorCount === 15 ? "14+" : errorCount})`; - } - } - - cell.innerHTML = innerText; - cell.style.backgroundColor = backgroundColor; - cell.style.color = UtilityEnabled("DarkMode") ? "white" : "black"; - } - - /** - * Add page-specific styles - * @param {HTMLStyleElement} Style - Style element - */ - function addPageStyles(Style) { - Style.innerHTML += ` -td { - white-space: nowrap; -}`; - } - - /** - * Page loader - Initializes page-specific modules based on current URL - * - * This module provides a centralized way to load page-specific styling and functionality. - * Each page module handles its own initialization and styling. - */ - - - /** - * Page route mapping - */ - const PAGE_ROUTES = { - '/problem.php': init$7, - '/contest.php': init$6, - '/status.php': init$5, - '/submitpage.php': init$4, - '/problemset.php': init$3, - '/userinfo.php': init$2, - '/loginpage.php': init$1, - '/contestrank-oi.php': init, - '/contestrank-correct.php': init, - }; - - /** - * Initialize page-specific module based on current pathname - * @param {Object} context - Shared context object with dependencies - */ - async function initializePage(context) { - const pathname = location.pathname; - - const pageInit = PAGE_ROUTES[pathname]; - - if (pageInit) { - try { - await pageInit(context); - console.log(`[XMOJ-Script] Initialized page module: ${pathname}`); - } catch (error) { - console.error(`[XMOJ-Script] Error initializing page ${pathname}:`, error); - } - } - } - - /** - * Get list of all implemented page modules - * @returns {string[]} List of page pathnames with modules - */ - function getImplementedPages() { - return Object.keys(PAGE_ROUTES); - } - - /** - * Main entry point for XMOJ Script - * This file imports all utilities and features, then initializes the application - */ - - - // Make utilities globally available (for compatibility with inline code) - window.escapeHTML = escapeHTML$1; - window.PurifyHTML = PurifyHTML$1; - window.SmartAlert = SmartAlert; - window.GetRelativeTime = GetRelativeTime; - window.SecondsToString = SecondsToString; - window.StringToSeconds = StringToSeconds; - window.TimeToStringTime = TimeToStringTime$1; - window.SizeToStringSize = SizeToStringSize$1; - window.CodeSizeToStringSize = CodeSizeToStringSize$1; - window.compareVersions = compareVersions; - window.RequestAPI = RequestAPI; - window.storeCredential = storeCredential$1; - window.getCredential = getCredential$1; - window.clearCredential = clearCredential; - window.RenderMathJax = RenderMathJax$1; - window.TidyTable = TidyTable; - window.GetUserInfo = GetUserInfo$1; - window.GetUserBadge = GetUserBadge$1; - window.GetUsernameHTML = GetUsernameHTML$1; - window.UtilityEnabled = UtilityEnabled; - window.AdminUserList = AdminUserList; - - // Register menu commands - registerMenuCommands(); - - // Initialize theme - initTheme(); - - // Initialize extracted feature modules - // These run before main() to allow early initialization (like AutoLogin) - initializeFeatures().then(() => { - console.log('[XMOJ-Script] Extracted features loaded:', getExtractedFeatures()); - }); - - // Start the main application - // Note: bootstrap.js still contains all original code for compatibility - // Extracted features in src/features/ provide the same functionality - // in a more maintainable way - main(); - - // Initialize page-specific modules after main() runs - // Page modules handle page-specific styling and DOM manipulations - // This needs to run after bootstrap.js sets up the basic structure - window.addEventListener('load', () => { - // Create context object with commonly used utilities - const pageContext = { - SearchParams: new URLSearchParams(location.search), - RenderMathJax: RenderMathJax$1, - RequestAPI, - TidyTable, - GetUserInfo: GetUserInfo$1, - GetUserBadge: GetUserBadge$1, - GetUsernameHTML: GetUsernameHTML$1, - GetRelativeTime, - SmartAlert, - Style: document.querySelector('style#UserScript-Style'), - IsAdmin: window.IsAdmin || false, // Set by bootstrap.js - }; - - initializePage(pageContext).then(() => { - console.log('[XMOJ-Script] Page modules available for:', getImplementedPages()); - }); - }); - -})(); From 0add682fb44b99fd3de1e729c032e63108d69551 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 00:41:43 +0000 Subject: [PATCH 14/26] fix: Update to version 2.5.2 and fix critical bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed bugs introduced during refactoring: 1. Version mismatch: Updated package.json from 2.5.0 to 2.5.2 2. Login screen bugs (null pointer errors): - Fixed RemoveUseless feature to check for null before calling .remove() - Added proper null checks for marquee, langEnHeader, and centerElements 3. Applied 2.5.2 upstream changes: - Updated SubmitLink selection in problem.js to find by text content "提交" (more reliable than multiple nth-child selectors) - Added PID null handling for contest problems with fetch fallback Changes: - package.json: Version 2.5.0 → 2.5.2 - src/features/remove-useless.js: Added null safety checks - src/pages/problem.js: Updated SubmitLink logic and added PID null handling These fixes resolve NotFoundError and TypeError that prevented login. --- Update.json | 6408 ++++++++++++++++---------------- XMOJ.user.js | 2 +- package.json | 2 +- src/features/remove-useless.js | 10 +- src/pages/problem.js | 37 +- 5 files changed, 3242 insertions(+), 3217 deletions(-) diff --git a/Update.json b/Update.json index 774257c9..8d281d86 100644 --- a/Update.json +++ b/Update.json @@ -1,3197 +1,3219 @@ { - "UpdateHistory": { - "1.0.200": { - "UpdateDate": 1696254577068, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 0, - "Description": "" - } - ] - }, - "1.0.201": { - "UpdateDate": 1696315453970, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 26, - "Description": "增加名字" - } - ] - }, - "1.0.202": { - "UpdateDate": 1696337480177, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 26, - "Description": "增加名字" - } - ] - }, - "1.0.203": { - "UpdateDate": 1696342022363, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 57, - "Description": "add more credits" - } - ] - }, - "1.0.204": { - "UpdateDate": 1696342420616, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 58, - "Description": "把@langningchen当作吉祥物" - } - ] - }, - "1.0.205": { - "UpdateDate": 1696384470503, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 57, - "Description": "add more credits" - }, - { - "PR": 58, - "Description": "把@langningchen当作吉祥物" - } - ] - }, - "1.0.206": { - "UpdateDate": 1696382789791, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 67, - "Description": "允许用户关闭获取数据,开启学术模式选择" - } - ] - }, - "1.0.207": { - "UpdateDate": 1696392248973, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 82, - "Description": "Dev Release" - } - ] - }, - "1.0.208": { - "UpdateDate": 1696429996532, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 95, - "Description": "修复编辑用户显示" - } - ] - }, - "1.0.209": { - "UpdateDate": 1696490377673, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 98, - "Description": "修复部分功能" - } - ] - }, - "1.0.210": { - "UpdateDate": 1696510135483, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 102, - "Description": "修复高亮" - } - ] - }, - "1.0.211": { - "UpdateDate": 1696514142283, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 104, - "Description": "自动提交当年代码" - } - ] - }, - "1.0.212": { - "UpdateDate": 1696515556253, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 105, - "Description": "emergency fix" - } - ] - }, - "1.0.213": { - "UpdateDate": 1696550511282, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 108, - "Description": "修复自动提交当年代码不计分" - } - ] - }, - "1.0.214": { - "UpdateDate": 1696551077104, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 67, - "Description": "允许用户关闭获取数据,开启学术模式选择" - }, - { - "PR": 82, - "Description": "Dev Release" - }, - { - "PR": 95, - "Description": "修复编辑用户显示" - }, - { - "PR": 98, - "Description": "修复部分功能" - }, - { - "PR": 102, - "Description": "修复高亮" - }, - { - "PR": 104, - "Description": "自动提交当年代码" - }, - { - "PR": 105, - "Description": "emergency fix" - }, - { - "PR": 108, - "Description": "修复自动提交当年代码不计分" - } - ] - }, - "1.0.215": { - "UpdateDate": 1696556056381, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 113, - "Description": "更改学术模式" - } - ] - }, - "1.0.216": { - "UpdateDate": 1696562219491, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 114, - "Description": "改变自动提交当年代码样式" - } - ] - }, - "1.0.217": { - "UpdateDate": 1696566723926, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 117, - "Description": "不自动提交已AC题目" - } - ] - }, - "1.0.218": { - "UpdateDate": 1696568744173, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 119, - "Description": "预编译使用C++14" - } - ] - }, - "1.0.219": { - "UpdateDate": 1696857809857, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 128, - "Description": "Why?" - } - ] - }, - "1.0.220": { - "UpdateDate": 1696859775005, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 132, - "Description": " bump version " - } - ] - }, - "1.0.221": { - "UpdateDate": 1696859889320, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 113, - "Description": "更改学术模式" - }, - { - "PR": 114, - "Description": "改变自动提交当年代码样式" - }, - { - "PR": 117, - "Description": "不自动提交已AC题目" - }, - { - "PR": 119, - "Description": "预编译使用C++14" - }, - { - "PR": 128, - "Description": "Why?" - }, - { - "PR": 132, - "Description": " bump version " - } - ] - }, - "1.0.222": { - "UpdateDate": 1696860193859, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 135, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.223": { - "UpdateDate": 1696860366697, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 135, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.224": { - "UpdateDate": 1697249568431, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 140, - "Description": "121 feature request 抄std的给badge作弊者" - } - ] - }, - "1.0.225": { - "UpdateDate": 1697351158955, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 144, - "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" - } - ] - }, - "1.0.226": { - "UpdateDate": 1697351411842, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 145, - "Description": "Emergency Fix" - } - ] - }, - "1.0.227": { - "UpdateDate": 1697367771069, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 149, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.228": { - "UpdateDate": 1697368165342, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 151, - "Description": "增加开发组成员" - } - ] - }, - "1.0.229": { - "UpdateDate": 1697640660452, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 156, - "Description": "修复部分题目状态页没有运行编号 (#155)" - } - ] - }, - "1.0.230": { - "UpdateDate": 1697725858941, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 160, - "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" - } - ] - }, - "1.0.231": { - "UpdateDate": 1697789116509, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 163, - "Description": "让cln的badge好看一点" - } - ] - }, - "1.0.232": { - "UpdateDate": 1697789214002, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 140, - "Description": "121 feature request 抄std的给badge作弊者" - }, - { - "PR": 144, - "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" - }, - { - "PR": 145, - "Description": "Emergency Fix" - }, - { - "PR": 149, - "Description": "Update XMOJ.user.js" - }, - { - "PR": 151, - "Description": "增加开发组成员" - }, - { - "PR": 156, - "Description": "修复部分题目状态页没有运行编号 (#155)" - }, - { - "PR": 160, - "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" - }, - { - "PR": 163, - "Description": "让cln的badge好看一点" - } - ] - }, - "1.0.233": { - "UpdateDate": 1697864346624, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 169, - "Description": "为用户脚本增加icon" - } - ] - }, - "1.0.234": { - "UpdateDate": 1698975944441, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 169, - "Description": "为用户脚本增加icon" - } - ] - }, - "1.0.235": { - "UpdateDate": 1699008412737, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 190, - "Description": "使用新的图床api" - } - ] - }, - "1.0.236": { - "UpdateDate": 1699069499723, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 195, - "Description": "不需要申请啦~" - } - ] - }, - "1.0.237": { - "UpdateDate": 1699078360562, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 197, - "Description": "修复更新链接" - } - ] - }, - "1.0.238": { - "UpdateDate": 1699617792536, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 190, - "Description": "使用新的图床api" - }, - { - "PR": 195, - "Description": "不需要申请啦~" - }, - { - "PR": 197, - "Description": "修复更新链接" - } - ] - }, - "1.0.239": { - "UpdateDate": 1699691535438, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 204, - "Description": "remove this as it causes console errors" - }, - { - "PR": 205, - "Description": "修复题目标题显示 #138" - } - ] - }, - "1.0.240": { - "UpdateDate": 1699766077528, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 205, - "Description": "修复题目标题显示 #138" - }, - { - "PR": 204, - "Description": "remove this as it causes console errors" - } - ] - }, - "1.0.241": { - "UpdateDate": 1699766855912, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 220, - "Description": "Update Prerelease.yml" - } - ] - }, - "1.0.242": { - "UpdateDate": 1699766973308, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 221, - "Description": "烦死了。。。" - } - ] - }, - "1.0.243": { - "UpdateDate": 1699767075836, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 222, - "Description": "Update Prerelease.yml" - } - ] - }, - "1.0.244": { - "UpdateDate": 1699767251761, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 223, - "Description": "wtf" - } - ] - }, - "1.0.245": { - "UpdateDate": 1700289812416, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 232, - "Description": "使用Base64编码icon" - } - ] - }, - "1.0.246": { - "UpdateDate": 1700921733760, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 250, - "Description": "发送统计数据" - } - ] - }, - "1.0.247": { - "UpdateDate": 1700922013381, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 220, - "Description": "Update Prerelease.yml" - }, - { - "PR": 221, - "Description": "烦死了。。。" - }, - { - "PR": 222, - "Description": "Update Prerelease.yml" - }, - { - "PR": 223, - "Description": "wtf" - }, - { - "PR": 232, - "Description": "使用Base64编码icon" - }, - { - "PR": 250, - "Description": "发送统计数据" - } - ] - }, - "1.0.248": { - "UpdateDate": 1700981994538, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 267, - "Description": "修复提交按钮" - } - ] - }, - "1.0.249": { - "UpdateDate": 1701180984140, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 269, - "Description": "显示最后在线时间" - } - ] - }, - "1.0.250": { - "UpdateDate": 1701264987062, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 273, - "Description": "send extra information" - } - ] - }, - "1.0.251": { - "UpdateDate": 1701426631116, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 276, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.252": { - "UpdateDate": 1701435211051, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 267, - "Description": "修复提交按钮" - }, - { - "PR": 269, - "Description": "显示最后在线时间" - }, - { - "PR": 273, - "Description": "send extra information" - }, - { - "PR": 276, - "Description": "Change the include statement" - } - ] - }, - "1.0.253": { - "UpdateDate": 1701491993821, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 282, - "Description": "use https in xmoj" - } - ] - }, - "1.0.254": { - "UpdateDate": 1701511837385, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 282, - "Description": "use https in xmoj" - } - ] - }, - "1.1.0": { - "UpdateDate": 1702641659793, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - } - ] - }, - "1.1.1": { - "UpdateDate": 1702641844861, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - } - ] - }, - "1.1.2": { - "UpdateDate": 1702687185849, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 301, - "Description": "改变更新架构" - } - ] - }, - "1.1.3": { - "UpdateDate": 1702821395564, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - } - ] - }, - "1.1.4": { - "UpdateDate": 1702822514246, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 305, - "Description": "add release notes (#303)" - } - ], - "Notes": "Hello, release notes! test 测试" - }, - "1.1.5": { - "UpdateDate": 1702993420758, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 310, - "Description": "Add an Easter egg" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.6": { - "UpdateDate": 1702995326126, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 312, - "Description": "fix release notes" - } - ], - "Notes": "Welcome!" - }, - "1.1.7": { - "UpdateDate": 1703253098623, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "test: 这个算公告吗?@chenlangning" - }, - "1.1.8": { - "UpdateDate": 1703253440322, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 301, - "Description": "改变更新架构" - }, - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - }, - { - "PR": 305, - "Description": "add release notes (#303)" - }, - { - "PR": 310, - "Description": "Add an Easter egg" - }, - { - "PR": 312, - "Description": "fix release notes" - }, - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.9": { - "UpdateDate": 1703253440322, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 301, - "Description": "改变更新架构" - }, - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - }, - { - "PR": 305, - "Description": "add release notes (#303)" - }, - { - "PR": 310, - "Description": "Add an Easter egg" - }, - { - "PR": 312, - "Description": "fix release notes" - }, - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.10": { - "UpdateDate": 1703254332078, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 301, - "Description": "改变更新架构" - }, - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - }, - { - "PR": 305, - "Description": "add release notes (#303)" - }, - { - "PR": 310, - "Description": "Add an Easter egg" - }, - { - "PR": 312, - "Description": "fix release notes" - }, - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.11": { - "UpdateDate": 1703923205023, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 327, - "Description": "优化用户体验" - } - ], - "Notes": "增加bug上报和主页按钮" - }, - "1.1.12": { - "UpdateDate": 1704017187302, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 329, - "Description": "增加权限" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.13": { - "UpdateDate": 1704936560583, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 340, - "Description": "add the native link" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.14": { - "UpdateDate": 1705756153752, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 355, - "Description": "fix #332" - }, - { - "PR": 356, - "Description": "N/A" - } - ], - "Notes": "修复题解标题" - }, - "1.1.15": { - "UpdateDate": 1705807807990, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 357, - "Description": "cleanup" - } - ], - "Notes": "This release fixes a lot of things" - }, - "1.1.16": { - "UpdateDate": 1705807913261, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 327, - "Description": "优化用户体验" - }, - { - "PR": 329, - "Description": "增加权限" - }, - { - "PR": 340, - "Description": "add the native link" - }, - { - "PR": 356, - "Description": "N/A" - }, - { - "PR": 355, - "Description": "fix #332" - }, - { - "PR": 357, - "Description": "cleanup" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.17": { - "UpdateDate": 1705808495397, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 327, - "Description": "优化用户体验" - }, - { - "PR": 329, - "Description": "增加权限" - }, - { - "PR": 340, - "Description": "add the native link" - }, - { - "PR": 356, - "Description": "N/A" - }, - { - "PR": 355, - "Description": "fix #332" - }, - { - "PR": 357, - "Description": "cleanup" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.18": { - "UpdateDate": 1705813603117, - "Prerelease": false, - "UpdateContents": [], - "Notes": "No release notes were provided for this release." - }, - "1.1.19": { - "UpdateDate": 1705841193051, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 372, - "Description": "use the same peram (?to_user) as xmoj" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.20": { - "UpdateDate": 1705929267062, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 375, - "Description": "add spellcheck" - } - ], - "Notes": "This release enables spell checking for bbs and short_msg! 🎉\nI did it 5 minutes after proposing it, swx." - }, - "1.1.21": { - "UpdateDate": 1705929832424, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 377, - "Description": "fix spellcheck" - } - ], - "Notes": "Oops, sorry. I forgot to add the spellcheck some text fields. Anyway, it's fixed now. 😅" - }, - "1.1.22": { - "UpdateDate": 1705983862747, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 378, - "Description": "sleep for a sec after submitting to prevent xmoj from crashing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.23": { - "UpdateDate": 1706103530551, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 390, - "Description": "更新support链接" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.24": { - "UpdateDate": 1706245175892, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 393, - "Description": "make the upload_std interface prettier" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.25": { - "UpdateDate": 1706250229435, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 394, - "Description": "fix problemstatus" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.28": { - "UpdateDate": 1706250296931, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 372, - "Description": "use the same peram (?to_user) as xmoj" - }, - { - "PR": 375, - "Description": "add spellcheck" - }, - { - "PR": 377, - "Description": "fix spellcheck" - }, - { - "PR": 378, - "Description": "sleep for a sec after submitting to prevent xmoj from crashing" - }, - { - "PR": 390, - "Description": "更新support链接" - }, - { - "PR": 393, - "Description": "make the upload_std interface prettier" - }, - { - "PR": 394, - "Description": "fix problemstatus" - } - ], - "Notes": "Note: v1.1.26-27 is gone.\nVersion 1.1.28 of xmoj-script ships a lot of quality of life improvements and bug fixes. \n\n- Add spell checking for bbs and short_msg\n- Fix the problem status page\n- Make the upload_std interface prettier\n- Use the same parameter as xmoj for the native link\n- Sleep for a second after submitting to prevent xmoj from crashing\n- Update support link \n Please note that upload_std now uploads the user's code if there is no std.\n See you in the next release!🎆" - }, - "1.1.30": { - "UpdateDate": 1706507043236, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 403, - "Description": "make the script much faster" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.31": { - "UpdateDate": 1706507178378, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 403, - "Description": "make the script much faster" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.32": { - "UpdateDate": 1706509019854, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 406, - "Description": "more choices" - } - ], - "Notes": "Because choices..." - }, - "1.1.33": { - "UpdateDate": 1706509685600, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 407, - "Description": "superdebug mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.34": { - "UpdateDate": 1706623811152, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 411, - "Description": "修正部分用户姓名设置的错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.35": { - "UpdateDate": 1706625057380, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 412, - "Description": "修改警告" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.36": { - "UpdateDate": 1706680652574, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 413, - "Description": "fix #409" - } - ], - "Notes": "A very important fix!" - }, - "1.1.37": { - "UpdateDate": 1706680926393, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 406, - "Description": "more choices" - }, - { - "PR": 407, - "Description": "superdebug mode" - }, - { - "PR": 411, - "Description": "修正部分用户姓名设置的错误" - }, - { - "PR": 412, - "Description": "修改警告" - }, - { - "PR": 413, - "Description": "fix #409" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.38": { - "UpdateDate": 1706681565820, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 416, - "Description": "major restructuring(fixes #364)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.39": { - "UpdateDate": 1706865355252, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 425, - "Description": "Revert 增加更新链接,优化格式" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.40": { - "UpdateDate": 1706867021708, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 428, - "Description": "更新部分语言" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.41": { - "UpdateDate": 1706867272746, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 429, - "Description": "增加开发组成员,zhouyiqing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.42": { - "UpdateDate": 1707642572244, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 445, - "Description": "fix [Bug] 右上角用户名点击后无反应 " - } - ], - "Notes": "Popper.js is so stupid..." - }, - "1.1.43": { - "UpdateDate": 1707715028113, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 447, - "Description": "fix the unpkg-cdn option" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.44": { - "UpdateDate": 1707803296933, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 449, - "Description": "regression!" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.45": { - "UpdateDate": 1708070431275, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 454, - "Description": "fix #400 + //ci-no-touch" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.46": { - "UpdateDate": 1708137046736, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 445, - "Description": "fix [Bug] 右上角用户名点击后无反应 " - }, - { - "PR": 447, - "Description": "fix the unpkg-cdn option" - }, - { - "PR": 449, - "Description": "regression!" - }, - { - "PR": 454, - "Description": "fix #400 + //ci-no-touch" - } - ], - "Notes": "A lot of QoL improvements!" - }, - "1.1.48": { - "UpdateDate": 1709370871510, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 485, - "Description": "and I am so frustrated I actually make another code contribution" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.49": { - "UpdateDate": 1709371832051, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 486, - "Description": "fix showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.50": { - "UpdateDate": 1710576508444, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 485, - "Description": "and I am so frustrated I actually make another code contribution" - }, - { - "PR": 486, - "Description": "fix showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.51": { - "UpdateDate": 1710641069919, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 491, - "Description": "Update ticket email" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.52": { - "UpdateDate": 1711848297024, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 493, - "Description": "Make the dismiss button work + improve showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.53": { - "UpdateDate": 1712374147729, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 491, - "Description": "Update ticket email" - }, - { - "PR": 493, - "Description": "Make the dismiss button work + improve showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.54": { - "UpdateDate": 1712395969816, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 497, - "Description": "move the main msg content into a div with flex!!! :tada:" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.55": { - "UpdateDate": 1712411916005, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 499, - "Description": "add markdown support to short messages " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.56": { - "UpdateDate": 1713525771039, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 505, - "Description": "让 ctrl + enter 触发自动提交当年代码" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.57": { - "UpdateDate": 1713526164395, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 497, - "Description": "move the main msg content into a div with flex!!! :tada:" - }, - { - "PR": 499, - "Description": "add markdown support to short messages " - }, - { - "PR": 505, - "Description": "让 ctrl + enter 触发自动提交当年代码" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.58": { - "UpdateDate": 1713668825681, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 514, - "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.59": { - "UpdateDate": 1713676517652, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 515, - "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.60": { - "UpdateDate": 1713682768316, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 516, - "Description": "revert #514" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.61": { - "UpdateDate": 1714207526058, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 517, - "Description": "Auto Read Short message mentions" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.62": { - "UpdateDate": 1714208364065, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 519, - "Description": "Refresh Short Messages at fixed intervals (experimental)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.63": { - "UpdateDate": 1714740837007, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 522, - "Description": "chore: 删除被 @boomzero 除名的开发组成员" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.64": { - "UpdateDate": 1714819418530, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 523, - "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.65": { - "UpdateDate": 1714819512051, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 514, - "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" - }, - { - "PR": 515, - "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" - }, - { - "PR": 516, - "Description": "revert #514" - }, - { - "PR": 517, - "Description": "Auto Read Short message mentions" - }, - { - "PR": 519, - "Description": "Refresh Short Messages at fixed intervals (experimental)" - }, - { - "PR": 522, - "Description": "chore: 删除被 @boomzero 除名的开发组成员" - }, - { - "PR": 523, - "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.66": { - "UpdateDate": 1714821016028, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 526, - "Description": "[ImgBot] Optimize images" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.67": { - "UpdateDate": 1714822822741, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 526, - "Description": "[ImgBot] Optimize images" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.68": { - "UpdateDate": 1718670038883, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 544, - "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.69": { - "UpdateDate": 1719815069812, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 546, - "Description": "fix memory displays" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.70": { - "UpdateDate": 1719815850792, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 544, - "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" - }, - { - "PR": 546, - "Description": "fix memory displays" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.71": { - "UpdateDate": 1719843873067, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 550, - "Description": "Improve debug mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.72": { - "UpdateDate": 1721535836758, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 552, - "Description": "Update the api domain" - } - ], - "Notes": "Please note that users using DebugMode should update immediately.
    The domain ghpages.xmoj-bbs.tech will be updated at a later date." - }, - "1.2.0": { - "UpdateDate": 1721638608232, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 550, - "Description": "Improve debug mode" - }, - { - "PR": 552, - "Description": "Update the api domain" - } - ], - "Notes": "Please update immediately, thank you.
    This release changes the api domain from xmoj-bbs.tech to xmoj-bbs.me" - }, - "1.2.1": { - "UpdateDate": 1721656184134, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 557, - "Description": "更改获取数据体验(增加报错)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.2": { - "UpdateDate": 1721887214365, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 559, - "Description": "Add CLion" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.3": { - "UpdateDate": 1722003346568, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 563, - "Description": "fix #562" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.4": { - "UpdateDate": 1722040221117, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 565, - "Description": "短消息增加图床支持" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.5": { - "UpdateDate": 1722044501316, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 566, - "Description": "预览版域名切换" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.6": { - "UpdateDate": 1722048484899, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 557, - "Description": "更改获取数据体验(增加报错)" - }, - { - "PR": 559, - "Description": "Add CLion" - }, - { - "PR": 563, - "Description": "fix #562" - }, - { - "PR": 565, - "Description": "短消息增加图床支持" - }, - { - "PR": 566, - "Description": "预览版域名切换" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.7": { - "UpdateDate": 1722063024309, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 570, - "Description": "增加公告栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.8": { - "UpdateDate": 1722070813624, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 570, - "Description": "增加公告栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.9": { - "UpdateDate": 1722088781344, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 573, - "Description": "add username rendering for noticeboard" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.10": { - "UpdateDate": 1722425862935, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 574, - "Description": "disable tidytable" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.11": { - "UpdateDate": 1722432013017, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 575, - "Description": "Upgrade bootstrap + cdnjs" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.12": { - "UpdateDate": 1722433643071, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 576, - "Description": "fix tidytable" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.13": { - "UpdateDate": 1722435875725, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 573, - "Description": "add username rendering for noticeboard" - }, - { - "PR": 574, - "Description": "disable tidytable" - }, - { - "PR": 575, - "Description": "Upgrade bootstrap + cdnjs" - }, - { - "PR": 576, - "Description": "fix tidytable" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.14": { - "UpdateDate": 1722470253103, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 580, - "Description": "Improve popover usability" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.15": { - "UpdateDate": 1722472465539, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 581, - "Description": "fix copymd" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.16": { - "UpdateDate": 1722478374388, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 583, - "Description": "fix ACM rank" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.17": { - "UpdateDate": 1722513002990, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 594, - "Description": "Revert Extern" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.18": { - "UpdateDate": 1722513340833, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 595, - "Description": "update pic" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.19": { - "UpdateDate": 1722513622681, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 596, - "Description": "ahhhhhh typos" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.20": { - "UpdateDate": 1722558316642, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 600, - "Description": "add some document.titles" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.21": { - "UpdateDate": 1722558913797, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 601, - "Description": "more document titles" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.22": { - "UpdateDate": 1722596300758, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 608, - "Description": "美化导航栏(by zhouyiqing)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.23": { - "UpdateDate": 1722601450156, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 615, - "Description": "解决动画打断问题" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.24": { - "UpdateDate": 1722821084232, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 622, - "Description": "Extern contrib from @zhouyiqing0304" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.25": { - "UpdateDate": 1722923378941, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 629, - "Description": "Emergency fix" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.26": { - "UpdateDate": 1722934373375, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 631, - "Description": "Update More Names" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.27": { - "UpdateDate": 1722953708717, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 633, - "Description": "Add error Reporting" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.28": { - "UpdateDate": 1722992901032, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 634, - "Description": "减少歧义" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.29": { - "UpdateDate": 1722993130679, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 580, - "Description": "Improve popover usability" - }, - { - "PR": 581, - "Description": "fix copymd" - }, - { - "PR": 583, - "Description": "fix ACM rank" - }, - { - "PR": 594, - "Description": "Revert Extern" - }, - { - "PR": 595, - "Description": "update pic" - }, - { - "PR": 596, - "Description": "ahhhhhh typos" - }, - { - "PR": 600, - "Description": "add some document.titles" - }, - { - "PR": 601, - "Description": "more document titles" - }, - { - "PR": 608, - "Description": "美化导航栏(by zhouyiqing)" - }, - { - "PR": 615, - "Description": "解决动画打断问题" - }, - { - "PR": 622, - "Description": "Extern contrib from @zhouyiqing0304" - }, - { - "PR": 629, - "Description": "Emergency fix" - }, - { - "PR": 631, - "Description": "Update More Names" - }, - { - "PR": 633, - "Description": "Add error Reporting" - }, - { - "PR": 634, - "Description": "减少歧义" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.30": { - "UpdateDate": 1722995904167, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 639, - "Description": "Restore License" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.31": { - "UpdateDate": 1723001178967, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 642, - "Description": "freopen检测优化 (by zhouyiqing)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.32": { - "UpdateDate": 1723006469038, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 644, - "Description": "Better error Reporting" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.33": { - "UpdateDate": 1723007791910, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 645, - "Description": "Codemirror for freopen" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.34": { - "UpdateDate": 1723008021982, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 648, - "Description": "format freopen statement" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.35": { - "UpdateDate": 1723008839982, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 651, - "Description": "prevent RE" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.36": { - "UpdateDate": 1723018816861, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 653, - "Description": "Update a number of names" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.37": { - "UpdateDate": 1723094016287, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 655, - "Description": "Prevent RE" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.38": { - "UpdateDate": 1723094571329, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 657, - "Description": "log Success" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.39": { - "UpdateDate": 1723095200260, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 658, - "Description": "error reporting for image upload" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.40": { - "UpdateDate": 1723095733498, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 659, - "Description": "new toolbar item" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.41": { - "UpdateDate": 1723095901881, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 639, - "Description": "Restore License" - }, - { - "PR": 642, - "Description": "freopen检测优化 (by zhouyiqing)" - }, - { - "PR": 644, - "Description": "Better error Reporting" - }, - { - "PR": 645, - "Description": "Codemirror for freopen" - }, - { - "PR": 648, - "Description": "format freopen statement" - }, - { - "PR": 651, - "Description": "prevent RE" - }, - { - "PR": 653, - "Description": "Update a number of names" - }, - { - "PR": 655, - "Description": "Prevent RE" - }, - { - "PR": 657, - "Description": "log Success" - }, - { - "PR": 658, - "Description": "error reporting for image upload" - }, - { - "PR": 659, - "Description": "new toolbar item" - } - ], - "Notes": "minor release.
    Enjoy!😀" - }, - "1.2.42": { - "UpdateDate": 1723096626205, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 663, - "Description": "Do we really need this?" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.43": { - "UpdateDate": 1723097609941, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 664, - "Description": "fix reply infinite looping" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.44": { - "UpdateDate": 1723098260973, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 665, - "Description": "fix replying" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.45": { - "UpdateDate": 1723180072602, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 667, - "Description": "fix replying(again)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.46": { - "UpdateDate": 1723181112001, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 669, - "Description": "smartAlert!" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.47": { - "UpdateDate": 1723181393030, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 670, - "Description": "Fix RE" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.48": { - "UpdateDate": 1723183783191, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 671, - "Description": "允许在已结束的比赛下提交 #287" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.49": { - "UpdateDate": 1723184271477, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 673, - "Description": "Remove headers" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.50": { - "UpdateDate": 1723184583632, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 674, - "Description": "Stop!" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.51": { - "UpdateDate": 1723190703489, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 680, - "Description": " 解决强制O2的问题" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.52": { - "UpdateDate": 1723260210125, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 663, - "Description": "Do we really need this?" - }, - { - "PR": 664, - "Description": "fix reply infinite looping" - }, - { - "PR": 665, - "Description": "fix replying" - }, - { - "PR": 667, - "Description": "fix replying(again)" - }, - { - "PR": 669, - "Description": "smartAlert!" - }, - { - "PR": 670, - "Description": "Fix RE" - }, - { - "PR": 671, - "Description": "允许在已结束的比赛下提交 #287" - }, - { - "PR": 673, - "Description": "Remove headers" - }, - { - "PR": 674, - "Description": "Stop!" - }, - { - "PR": 680, - "Description": " 解决强制O2的问题" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.53": { - "UpdateDate": 1723352066327, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 690, - "Description": "移除之前忘记操作的admin" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.54": { - "UpdateDate": 1723450512410, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 694, - "Description": "更新support url" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.55": { - "UpdateDate": 1723461779306, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 698, - "Description": "Set turnstile theme and language" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.56": { - "UpdateDate": 1723956966458, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 704, - "Description": "登录界面优化" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.57": { - "UpdateDate": 1723968698208, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 712, - "Description": "更改个人中心一栏鼠标指针样式" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.58": { - "UpdateDate": 1724058379641, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 715, - "Description": "更改短消息显示" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.59": { - "UpdateDate": 1724836147641, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 718, - "Description": "Prevent caching" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.60": { - "UpdateDate": 1725005372794, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 690, - "Description": "移除之前忘记操作的admin" - }, - { - "PR": 694, - "Description": "更新support url" - }, - { - "PR": 698, - "Description": "Set turnstile theme and language" - }, - { - "PR": 704, - "Description": "登录界面优化" - }, - { - "PR": 712, - "Description": "更改个人中心一栏鼠标指针样式" - }, - { - "PR": 715, - "Description": "更改短消息显示" - }, - { - "PR": 718, - "Description": "Prevent caching" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.61": { - "UpdateDate": 1725083743111, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 725, - "Description": "Add CP Editor" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.62": { - "UpdateDate": 1726406228773, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 729, - "Description": "add chenyiming5" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.63": { - "UpdateDate": 1727743018230, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 731, - "Description": "a quick fix for mathjax" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.64": { - "UpdateDate": 1727853823983, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 735, - "Description": "更新一大批名字" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.65": { - "UpdateDate": 1727861782766, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 737, - "Description": "Add http request headers" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.66": { - "UpdateDate": 1727862388829, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 725, - "Description": "Add CP Editor" - }, - { - "PR": 729, - "Description": "add chenyiming5" - }, - { - "PR": 731, - "Description": "a quick fix for mathjax" - }, - { - "PR": 735, - "Description": "更新一大批名字" - }, - { - "PR": 737, - "Description": "Add http request headers" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.67": { - "UpdateDate": 1727873172840, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 741, - "Description": "Fix Duplicate Names (warned by Qoanda)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.68": { - "UpdateDate": 1727926979282, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 743, - "Description": "feat: 优化等待状态的显示" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.69": { - "UpdateDate": 1728563746855, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 748, - "Description": "变动cdn源" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.70": { - "UpdateDate": 1728784924115, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 741, - "Description": "Fix Duplicate Names (warned by Qoanda)" - }, - { - "PR": 743, - "Description": "feat: 优化等待状态的显示" - }, - { - "PR": 748, - "Description": "变动cdn源" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.71": { - "UpdateDate": 1732545478542, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 753, - "Description": "fix: 修复代码长度单位换算错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.72": { - "UpdateDate": 1738077477956, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 758, - "Description": "Fix #714" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.73": { - "UpdateDate": 1738925550389, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 753, - "Description": "fix: 修复代码长度单位换算错误" - }, - { - "PR": 758, - "Description": "Fix #714" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.74": { - "UpdateDate": 1738933722742, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 762, - "Description": "讨论回复跳转优化" - } - ], - "Notes": "This was so hard......" - }, - "1.2.75": { - "UpdateDate": 1738974525711, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 767, - "Description": "进入讨论后给出题目的链接 " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.3.0": { - "UpdateDate": 1738976786495, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 762, - "Description": "讨论回复跳转优化" - }, - { - "PR": 767, - "Description": "进入讨论后给出题目的链接 " - } - ], - "Notes": "If you are curious why the version number is v1.3.0, it's because we changed our versioning strategy! Click here for more details." - }, - "1.3.1": { - "UpdateDate": 1739060055956, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 774, - "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.3.2": { - "UpdateDate": 1739187728351, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 778, - "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.4.0": { - "UpdateDate": 1740147937411, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 774, - "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" - }, - { - "PR": 778, - "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.4.1": { - "UpdateDate": 1740314902240, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 784, - "Description": "fix cdn" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.0": { - "UpdateDate": 1740316085837, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 784, - "Description": "fix cdn" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.1": { - "UpdateDate": 1745307653685, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 791, - "Description": "remove ACM rankings" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.2": { - "UpdateDate": 1746264285564, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 796, - "Description": "Clear session cookie on logout for better security" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.3": { - "UpdateDate": 1746271148834, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 798, - "Description": "修复文字错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.6.0": { - "UpdateDate": 1746281721231, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 791, - "Description": "remove ACM rankings" - }, - { - "PR": 796, - "Description": "Clear session cookie on logout for better security" - }, - { - "PR": 798, - "Description": "修复文字错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.6.1": { - "UpdateDate": 1748781837958, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 802, - "Description": "Fix page flashes in dark mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.0": { - "UpdateDate": 1748873286643, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 802, - "Description": "Fix page flashes in dark mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.1": { - "UpdateDate": 1749457101103, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 806, - "Description": "Remove the statistics button from contest page" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.2": { - "UpdateDate": 1749865010938, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 808, - "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.3": { - "UpdateDate": 1750594769202, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 811, - "Description": "fix: Redirect to login page if profile element is missing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.8.0": { - "UpdateDate": 1750594823374, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 806, - "Description": "Remove the statistics button from contest page" - }, - { - "PR": 808, - "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" - }, - { - "PR": 811, - "Description": "fix: Redirect to login page if profile element is missing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.8.1": { - "UpdateDate": 1752061009119, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 817, - "Description": "fix: reinfo.php shows no test results even if test results are available " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.9.0": { - "UpdateDate": 1752132111562, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 817, - "Description": "fix: reinfo.php shows no test results even if test results are available " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.9.1": { - "UpdateDate": 1753443057292, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 821, - "Description": "修复工具栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.999990.0": { - "UpdateDate": 1753443146018, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 821, - "Description": "修复工具栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.0.0": { - "UpdateDate": 1754724044036, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 826, - "Description": "修复更新" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.0": { - "UpdateDate": 1754724455081, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 826, - "Description": "修复更新" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.1": { - "UpdateDate": 1754911555185, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 829, - "Description": "Update diff-match-patch library source URL" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.2": { - "UpdateDate": 1755589063111, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 834, - "Description": "feat: use credential management api" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.3": { - "UpdateDate": 1755596470421, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 835, - "Description": "feat: add user-selectable theme" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.2.0": { - "UpdateDate": 1755767075588, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 829, - "Description": "Update diff-match-patch library source URL" - }, - { - "PR": 834, - "Description": "feat: use credential management api" - }, - { - "PR": 835, - "Description": "feat: add user-selectable theme" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.2.1": { - "UpdateDate": 1756003502262, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 839, - "Description": "Fix formatting in feedback card text" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.2.2": { - "UpdateDate": 1757851046240, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 852, - "Description": "Fix the submit button on some pages" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.3.0": { - "UpdateDate": 1757851277491, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 839, - "Description": "Fix formatting in feedback card text" - }, - { - "PR": 852, - "Description": "Fix the submit button on some pages" - } - ], - "Notes": "随着 CI 更新, 相信以后的 release 都会有 release notes(" - }, - "2.3.1": { - "UpdateDate": 1758944205783, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 856, - "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" - } - ], - "Notes": "本版本修复了由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" - }, - "2.4.0": { - "UpdateDate": 1758956527554, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 856, - "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" - } - ], - "Notes": "请尽快升级至本版本!" - }, - "2.4.1": { - "UpdateDate": 1759412851092, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 861, - "Description": "Update CSS selector" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.4.2": { - "UpdateDate": 1759413584731, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 863, - "Description": "修复“NaN年前“" - } - ], - "Notes": "修复“NaN年前“" - }, - "2.4.3": { - "UpdateDate": 1759414259108, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 865, - "Description": "删除获取数据功能" - } - ], - "Notes": "因为它利用的bug被修复了" - }, - "2.4.4": { - "UpdateDate": 1759417362937, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 866, - "Description": "比赛题目页面里左侧栏加入题目序号列表" - } - ], - "Notes": "#860 ..." - }, - "2.4.5": { - "UpdateDate": 1759487413226, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 869, - "Description": "Update CSS selector (again...)" - } - ], - "Notes": "为什么这个破东西老是换位置" - }, - "2.4.6": { - "UpdateDate": 1759548958578, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 871, - "Description": "Add tooltip for ProblemSwitcher" - } - ], - "Notes": "现在, 你只需要将鼠标悬浮在比赛题目切换器上方, 即可查看题目名称" - }, - "2.4.7": { - "UpdateDate": 1759549826774, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 872, - "Description": "修复获取数据" - } - ], - "Notes": "funny" - }, - "2.5.0": { - "UpdateDate": 1759568103629, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 861, - "Description": "Update CSS selector" - }, - { - "PR": 863, - "Description": "修复“NaN年前“" - }, - { - "PR": 865, - "Description": "删除获取数据功能" - }, - { - "PR": 866, - "Description": "比赛题目页面里左侧栏加入题目序号列表" - }, - { - "PR": 869, - "Description": "Update CSS selector (again...)" - }, - { - "PR": 871, - "Description": "Add tooltip for ProblemSwitcher" - }, - { - "PR": 872, - "Description": "修复获取数据" - } - ], - "Notes": "XMOJ-Script 2.5.0 新增了比赛题目切换器,方便您在题目之间快速切换。此功能默认启用,您也可以在设置中禁用它。本版本还修复了一个导致时间显示为“NaN年前”的错误, 更新了一些 CSS selector,以适应 XMOJ 网站的最新变化." + "UpdateHistory": { + "1.0.200": { + "UpdateDate": 1696254577068, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 0, + "Description": "" } + ] + }, + "1.0.201": { + "UpdateDate": 1696315453970, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 26, + "Description": "增加名字" + } + ] + }, + "1.0.202": { + "UpdateDate": 1696337480177, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 26, + "Description": "增加名字" + } + ] + }, + "1.0.203": { + "UpdateDate": 1696342022363, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 57, + "Description": "add more credits" + } + ] + }, + "1.0.204": { + "UpdateDate": 1696342420616, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 58, + "Description": "把@langningchen当作吉祥物" + } + ] + }, + "1.0.205": { + "UpdateDate": 1696384470503, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 57, + "Description": "add more credits" + }, + { + "PR": 58, + "Description": "把@langningchen当作吉祥物" + } + ] + }, + "1.0.206": { + "UpdateDate": 1696382789791, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 67, + "Description": "允许用户关闭获取数据,开启学术模式选择" + } + ] + }, + "1.0.207": { + "UpdateDate": 1696392248973, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 82, + "Description": "Dev Release" + } + ] + }, + "1.0.208": { + "UpdateDate": 1696429996532, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 95, + "Description": "修复编辑用户显示" + } + ] + }, + "1.0.209": { + "UpdateDate": 1696490377673, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 98, + "Description": "修复部分功能" + } + ] + }, + "1.0.210": { + "UpdateDate": 1696510135483, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 102, + "Description": "修复高亮" + } + ] + }, + "1.0.211": { + "UpdateDate": 1696514142283, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 104, + "Description": "自动提交当年代码" + } + ] + }, + "1.0.212": { + "UpdateDate": 1696515556253, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 105, + "Description": "emergency fix" + } + ] + }, + "1.0.213": { + "UpdateDate": 1696550511282, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 108, + "Description": "修复自动提交当年代码不计分" + } + ] + }, + "1.0.214": { + "UpdateDate": 1696551077104, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 67, + "Description": "允许用户关闭获取数据,开启学术模式选择" + }, + { + "PR": 82, + "Description": "Dev Release" + }, + { + "PR": 95, + "Description": "修复编辑用户显示" + }, + { + "PR": 98, + "Description": "修复部分功能" + }, + { + "PR": 102, + "Description": "修复高亮" + }, + { + "PR": 104, + "Description": "自动提交当年代码" + }, + { + "PR": 105, + "Description": "emergency fix" + }, + { + "PR": 108, + "Description": "修复自动提交当年代码不计分" + } + ] + }, + "1.0.215": { + "UpdateDate": 1696556056381, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 113, + "Description": "更改学术模式" + } + ] + }, + "1.0.216": { + "UpdateDate": 1696562219491, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 114, + "Description": "改变自动提交当年代码样式" + } + ] + }, + "1.0.217": { + "UpdateDate": 1696566723926, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 117, + "Description": "不自动提交已AC题目" + } + ] + }, + "1.0.218": { + "UpdateDate": 1696568744173, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 119, + "Description": "预编译使用C++14" + } + ] + }, + "1.0.219": { + "UpdateDate": 1696857809857, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 128, + "Description": "Why?" + } + ] + }, + "1.0.220": { + "UpdateDate": 1696859775005, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 132, + "Description": " bump version " + } + ] + }, + "1.0.221": { + "UpdateDate": 1696859889320, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 113, + "Description": "更改学术模式" + }, + { + "PR": 114, + "Description": "改变自动提交当年代码样式" + }, + { + "PR": 117, + "Description": "不自动提交已AC题目" + }, + { + "PR": 119, + "Description": "预编译使用C++14" + }, + { + "PR": 128, + "Description": "Why?" + }, + { + "PR": 132, + "Description": " bump version " + } + ] + }, + "1.0.222": { + "UpdateDate": 1696860193859, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 135, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.223": { + "UpdateDate": 1696860366697, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 135, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.224": { + "UpdateDate": 1697249568431, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 140, + "Description": "121 feature request 抄std的给badge作弊者" + } + ] + }, + "1.0.225": { + "UpdateDate": 1697351158955, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 144, + "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" + } + ] + }, + "1.0.226": { + "UpdateDate": 1697351411842, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 145, + "Description": "Emergency Fix" + } + ] + }, + "1.0.227": { + "UpdateDate": 1697367771069, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 149, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.228": { + "UpdateDate": 1697368165342, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 151, + "Description": "增加开发组成员" + } + ] + }, + "1.0.229": { + "UpdateDate": 1697640660452, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 156, + "Description": "修复部分题目状态页没有运行编号 (#155)" + } + ] + }, + "1.0.230": { + "UpdateDate": 1697725858941, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 160, + "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" + } + ] + }, + "1.0.231": { + "UpdateDate": 1697789116509, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 163, + "Description": "让cln的badge好看一点" + } + ] + }, + "1.0.232": { + "UpdateDate": 1697789214002, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 140, + "Description": "121 feature request 抄std的给badge作弊者" + }, + { + "PR": 144, + "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" + }, + { + "PR": 145, + "Description": "Emergency Fix" + }, + { + "PR": 149, + "Description": "Update XMOJ.user.js" + }, + { + "PR": 151, + "Description": "增加开发组成员" + }, + { + "PR": 156, + "Description": "修复部分题目状态页没有运行编号 (#155)" + }, + { + "PR": 160, + "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" + }, + { + "PR": 163, + "Description": "让cln的badge好看一点" + } + ] + }, + "1.0.233": { + "UpdateDate": 1697864346624, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 169, + "Description": "为用户脚本增加icon" + } + ] + }, + "1.0.234": { + "UpdateDate": 1698975944441, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 169, + "Description": "为用户脚本增加icon" + } + ] + }, + "1.0.235": { + "UpdateDate": 1699008412737, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 190, + "Description": "使用新的图床api" + } + ] + }, + "1.0.236": { + "UpdateDate": 1699069499723, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 195, + "Description": "不需要申请啦~" + } + ] + }, + "1.0.237": { + "UpdateDate": 1699078360562, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 197, + "Description": "修复更新链接" + } + ] + }, + "1.0.238": { + "UpdateDate": 1699617792536, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 190, + "Description": "使用新的图床api" + }, + { + "PR": 195, + "Description": "不需要申请啦~" + }, + { + "PR": 197, + "Description": "修复更新链接" + } + ] + }, + "1.0.239": { + "UpdateDate": 1699691535438, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 204, + "Description": "remove this as it causes console errors" + }, + { + "PR": 205, + "Description": "修复题目标题显示 #138" + } + ] + }, + "1.0.240": { + "UpdateDate": 1699766077528, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 205, + "Description": "修复题目标题显示 #138" + }, + { + "PR": 204, + "Description": "remove this as it causes console errors" + } + ] + }, + "1.0.241": { + "UpdateDate": 1699766855912, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 220, + "Description": "Update Prerelease.yml" + } + ] + }, + "1.0.242": { + "UpdateDate": 1699766973308, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 221, + "Description": "烦死了。。。" + } + ] + }, + "1.0.243": { + "UpdateDate": 1699767075836, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 222, + "Description": "Update Prerelease.yml" + } + ] + }, + "1.0.244": { + "UpdateDate": 1699767251761, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 223, + "Description": "wtf" + } + ] + }, + "1.0.245": { + "UpdateDate": 1700289812416, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 232, + "Description": "使用Base64编码icon" + } + ] + }, + "1.0.246": { + "UpdateDate": 1700921733760, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 250, + "Description": "发送统计数据" + } + ] + }, + "1.0.247": { + "UpdateDate": 1700922013381, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 220, + "Description": "Update Prerelease.yml" + }, + { + "PR": 221, + "Description": "烦死了。。。" + }, + { + "PR": 222, + "Description": "Update Prerelease.yml" + }, + { + "PR": 223, + "Description": "wtf" + }, + { + "PR": 232, + "Description": "使用Base64编码icon" + }, + { + "PR": 250, + "Description": "发送统计数据" + } + ] + }, + "1.0.248": { + "UpdateDate": 1700981994538, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 267, + "Description": "修复提交按钮" + } + ] + }, + "1.0.249": { + "UpdateDate": 1701180984140, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 269, + "Description": "显示最后在线时间" + } + ] + }, + "1.0.250": { + "UpdateDate": 1701264987062, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 273, + "Description": "send extra information" + } + ] + }, + "1.0.251": { + "UpdateDate": 1701426631116, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 276, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.252": { + "UpdateDate": 1701435211051, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 267, + "Description": "修复提交按钮" + }, + { + "PR": 269, + "Description": "显示最后在线时间" + }, + { + "PR": 273, + "Description": "send extra information" + }, + { + "PR": 276, + "Description": "Change the include statement" + } + ] + }, + "1.0.253": { + "UpdateDate": 1701491993821, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 282, + "Description": "use https in xmoj" + } + ] + }, + "1.0.254": { + "UpdateDate": 1701511837385, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 282, + "Description": "use https in xmoj" + } + ] + }, + "1.1.0": { + "UpdateDate": 1702641659793, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + } + ] + }, + "1.1.1": { + "UpdateDate": 1702641844861, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + } + ] + }, + "1.1.2": { + "UpdateDate": 1702687185849, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 301, + "Description": "改变更新架构" + } + ] + }, + "1.1.3": { + "UpdateDate": 1702821395564, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + } + ] + }, + "1.1.4": { + "UpdateDate": 1702822514246, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 305, + "Description": "add release notes (#303)" + } + ], + "Notes": "Hello, release notes! test 测试" + }, + "1.1.5": { + "UpdateDate": 1702993420758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 310, + "Description": "Add an Easter egg" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.6": { + "UpdateDate": 1702995326126, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 312, + "Description": "fix release notes" + } + ], + "Notes": "Welcome!" + }, + "1.1.7": { + "UpdateDate": 1703253098623, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "test: 这个算公告吗?@chenlangning" + }, + "1.1.8": { + "UpdateDate": 1703253440322, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 301, + "Description": "改变更新架构" + }, + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + }, + { + "PR": 305, + "Description": "add release notes (#303)" + }, + { + "PR": 310, + "Description": "Add an Easter egg" + }, + { + "PR": 312, + "Description": "fix release notes" + }, + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.9": { + "UpdateDate": 1703253440322, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 301, + "Description": "改变更新架构" + }, + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + }, + { + "PR": 305, + "Description": "add release notes (#303)" + }, + { + "PR": 310, + "Description": "Add an Easter egg" + }, + { + "PR": 312, + "Description": "fix release notes" + }, + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.10": { + "UpdateDate": 1703254332078, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 301, + "Description": "改变更新架构" + }, + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + }, + { + "PR": 305, + "Description": "add release notes (#303)" + }, + { + "PR": 310, + "Description": "Add an Easter egg" + }, + { + "PR": 312, + "Description": "fix release notes" + }, + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.11": { + "UpdateDate": 1703923205023, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 327, + "Description": "优化用户体验" + } + ], + "Notes": "增加bug上报和主页按钮" + }, + "1.1.12": { + "UpdateDate": 1704017187302, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 329, + "Description": "增加权限" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.13": { + "UpdateDate": 1704936560583, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 340, + "Description": "add the native link" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.14": { + "UpdateDate": 1705756153752, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 355, + "Description": "fix #332" + }, + { + "PR": 356, + "Description": "N/A" + } + ], + "Notes": "修复题解标题" + }, + "1.1.15": { + "UpdateDate": 1705807807990, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 357, + "Description": "cleanup" + } + ], + "Notes": "This release fixes a lot of things" + }, + "1.1.16": { + "UpdateDate": 1705807913261, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 327, + "Description": "优化用户体验" + }, + { + "PR": 329, + "Description": "增加权限" + }, + { + "PR": 340, + "Description": "add the native link" + }, + { + "PR": 356, + "Description": "N/A" + }, + { + "PR": 355, + "Description": "fix #332" + }, + { + "PR": 357, + "Description": "cleanup" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.17": { + "UpdateDate": 1705808495397, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 327, + "Description": "优化用户体验" + }, + { + "PR": 329, + "Description": "增加权限" + }, + { + "PR": 340, + "Description": "add the native link" + }, + { + "PR": 356, + "Description": "N/A" + }, + { + "PR": 355, + "Description": "fix #332" + }, + { + "PR": 357, + "Description": "cleanup" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.18": { + "UpdateDate": 1705813603117, + "Prerelease": false, + "UpdateContents": [], + "Notes": "No release notes were provided for this release." + }, + "1.1.19": { + "UpdateDate": 1705841193051, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 372, + "Description": "use the same peram (?to_user) as xmoj" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.20": { + "UpdateDate": 1705929267062, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 375, + "Description": "add spellcheck" + } + ], + "Notes": "This release enables spell checking for bbs and short_msg! 🎉\nI did it 5 minutes after proposing it, swx." + }, + "1.1.21": { + "UpdateDate": 1705929832424, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 377, + "Description": "fix spellcheck" + } + ], + "Notes": "Oops, sorry. I forgot to add the spellcheck some text fields. Anyway, it's fixed now. 😅" + }, + "1.1.22": { + "UpdateDate": 1705983862747, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 378, + "Description": "sleep for a sec after submitting to prevent xmoj from crashing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.23": { + "UpdateDate": 1706103530551, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 390, + "Description": "更新support链接" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.24": { + "UpdateDate": 1706245175892, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 393, + "Description": "make the upload_std interface prettier" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.25": { + "UpdateDate": 1706250229435, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 394, + "Description": "fix problemstatus" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.28": { + "UpdateDate": 1706250296931, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 372, + "Description": "use the same peram (?to_user) as xmoj" + }, + { + "PR": 375, + "Description": "add spellcheck" + }, + { + "PR": 377, + "Description": "fix spellcheck" + }, + { + "PR": 378, + "Description": "sleep for a sec after submitting to prevent xmoj from crashing" + }, + { + "PR": 390, + "Description": "更新support链接" + }, + { + "PR": 393, + "Description": "make the upload_std interface prettier" + }, + { + "PR": 394, + "Description": "fix problemstatus" + } + ], + "Notes": "Note: v1.1.26-27 is gone.\nVersion 1.1.28 of xmoj-script ships a lot of quality of life improvements and bug fixes. \n\n- Add spell checking for bbs and short_msg\n- Fix the problem status page\n- Make the upload_std interface prettier\n- Use the same parameter as xmoj for the native link\n- Sleep for a second after submitting to prevent xmoj from crashing\n- Update support link \n Please note that upload_std now uploads the user's code if there is no std.\n See you in the next release!🎆" + }, + "1.1.30": { + "UpdateDate": 1706507043236, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 403, + "Description": "make the script much faster" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.31": { + "UpdateDate": 1706507178378, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 403, + "Description": "make the script much faster" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.32": { + "UpdateDate": 1706509019854, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 406, + "Description": "more choices" + } + ], + "Notes": "Because choices..." + }, + "1.1.33": { + "UpdateDate": 1706509685600, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 407, + "Description": "superdebug mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.34": { + "UpdateDate": 1706623811152, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 411, + "Description": "修正部分用户姓名设置的错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.35": { + "UpdateDate": 1706625057380, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 412, + "Description": "修改警告" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.36": { + "UpdateDate": 1706680652574, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 413, + "Description": "fix #409" + } + ], + "Notes": "A very important fix!" + }, + "1.1.37": { + "UpdateDate": 1706680926393, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 406, + "Description": "more choices" + }, + { + "PR": 407, + "Description": "superdebug mode" + }, + { + "PR": 411, + "Description": "修正部分用户姓名设置的错误" + }, + { + "PR": 412, + "Description": "修改警告" + }, + { + "PR": 413, + "Description": "fix #409" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.38": { + "UpdateDate": 1706681565820, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 416, + "Description": "major restructuring(fixes #364)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.39": { + "UpdateDate": 1706865355252, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 425, + "Description": "Revert 增加更新链接,优化格式" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.40": { + "UpdateDate": 1706867021708, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 428, + "Description": "更新部分语言" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.41": { + "UpdateDate": 1706867272746, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 429, + "Description": "增加开发组成员,zhouyiqing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.42": { + "UpdateDate": 1707642572244, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 445, + "Description": "fix [Bug] 右上角用户名点击后无反应 " + } + ], + "Notes": "Popper.js is so stupid..." + }, + "1.1.43": { + "UpdateDate": 1707715028113, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 447, + "Description": "fix the unpkg-cdn option" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.44": { + "UpdateDate": 1707803296933, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 449, + "Description": "regression!" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.45": { + "UpdateDate": 1708070431275, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 454, + "Description": "fix #400 + //ci-no-touch" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.46": { + "UpdateDate": 1708137046736, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 445, + "Description": "fix [Bug] 右上角用户名点击后无反应 " + }, + { + "PR": 447, + "Description": "fix the unpkg-cdn option" + }, + { + "PR": 449, + "Description": "regression!" + }, + { + "PR": 454, + "Description": "fix #400 + //ci-no-touch" + } + ], + "Notes": "A lot of QoL improvements!" + }, + "1.1.48": { + "UpdateDate": 1709370871510, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 485, + "Description": "and I am so frustrated I actually make another code contribution" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.49": { + "UpdateDate": 1709371832051, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 486, + "Description": "fix showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.50": { + "UpdateDate": 1710576508444, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 485, + "Description": "and I am so frustrated I actually make another code contribution" + }, + { + "PR": 486, + "Description": "fix showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.51": { + "UpdateDate": 1710641069919, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 491, + "Description": "Update ticket email" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.52": { + "UpdateDate": 1711848297024, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 493, + "Description": "Make the dismiss button work + improve showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.53": { + "UpdateDate": 1712374147729, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 491, + "Description": "Update ticket email" + }, + { + "PR": 493, + "Description": "Make the dismiss button work + improve showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.54": { + "UpdateDate": 1712395969816, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 497, + "Description": "move the main msg content into a div with flex!!! :tada:" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.55": { + "UpdateDate": 1712411916005, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 499, + "Description": "add markdown support to short messages " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.56": { + "UpdateDate": 1713525771039, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 505, + "Description": "让 ctrl + enter 触发自动提交当年代码" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.57": { + "UpdateDate": 1713526164395, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 497, + "Description": "move the main msg content into a div with flex!!! :tada:" + }, + { + "PR": 499, + "Description": "add markdown support to short messages " + }, + { + "PR": 505, + "Description": "让 ctrl + enter 触发自动提交当年代码" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.58": { + "UpdateDate": 1713668825681, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 514, + "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.59": { + "UpdateDate": 1713676517652, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 515, + "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.60": { + "UpdateDate": 1713682768316, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 516, + "Description": "revert #514" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.61": { + "UpdateDate": 1714207526058, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 517, + "Description": "Auto Read Short message mentions" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.62": { + "UpdateDate": 1714208364065, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 519, + "Description": "Refresh Short Messages at fixed intervals (experimental)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.63": { + "UpdateDate": 1714740837007, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 522, + "Description": "chore: 删除被 @boomzero 除名的开发组成员" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.64": { + "UpdateDate": 1714819418530, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 523, + "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.65": { + "UpdateDate": 1714819512051, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 514, + "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" + }, + { + "PR": 515, + "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" + }, + { + "PR": 516, + "Description": "revert #514" + }, + { + "PR": 517, + "Description": "Auto Read Short message mentions" + }, + { + "PR": 519, + "Description": "Refresh Short Messages at fixed intervals (experimental)" + }, + { + "PR": 522, + "Description": "chore: 删除被 @boomzero 除名的开发组成员" + }, + { + "PR": 523, + "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.66": { + "UpdateDate": 1714821016028, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 526, + "Description": "[ImgBot] Optimize images" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.67": { + "UpdateDate": 1714822822741, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 526, + "Description": "[ImgBot] Optimize images" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.68": { + "UpdateDate": 1718670038883, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 544, + "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.69": { + "UpdateDate": 1719815069812, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 546, + "Description": "fix memory displays" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.70": { + "UpdateDate": 1719815850792, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 544, + "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" + }, + { + "PR": 546, + "Description": "fix memory displays" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.71": { + "UpdateDate": 1719843873067, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 550, + "Description": "Improve debug mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.72": { + "UpdateDate": 1721535836758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 552, + "Description": "Update the api domain" + } + ], + "Notes": "Please note that users using DebugMode should update immediately.
    The domain ghpages.xmoj-bbs.tech will be updated at a later date." + }, + "1.2.0": { + "UpdateDate": 1721638608232, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 550, + "Description": "Improve debug mode" + }, + { + "PR": 552, + "Description": "Update the api domain" + } + ], + "Notes": "Please update immediately, thank you.
    This release changes the api domain from xmoj-bbs.tech to xmoj-bbs.me" + }, + "1.2.1": { + "UpdateDate": 1721656184134, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 557, + "Description": "更改获取数据体验(增加报错)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.2": { + "UpdateDate": 1721887214365, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 559, + "Description": "Add CLion" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.3": { + "UpdateDate": 1722003346568, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 563, + "Description": "fix #562" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.4": { + "UpdateDate": 1722040221117, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 565, + "Description": "短消息增加图床支持" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.5": { + "UpdateDate": 1722044501316, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 566, + "Description": "预览版域名切换" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.6": { + "UpdateDate": 1722048484899, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 557, + "Description": "更改获取数据体验(增加报错)" + }, + { + "PR": 559, + "Description": "Add CLion" + }, + { + "PR": 563, + "Description": "fix #562" + }, + { + "PR": 565, + "Description": "短消息增加图床支持" + }, + { + "PR": 566, + "Description": "预览版域名切换" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.7": { + "UpdateDate": 1722063024309, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 570, + "Description": "增加公告栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.8": { + "UpdateDate": 1722070813624, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 570, + "Description": "增加公告栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.9": { + "UpdateDate": 1722088781344, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 573, + "Description": "add username rendering for noticeboard" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.10": { + "UpdateDate": 1722425862935, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 574, + "Description": "disable tidytable" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.11": { + "UpdateDate": 1722432013017, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 575, + "Description": "Upgrade bootstrap + cdnjs" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.12": { + "UpdateDate": 1722433643071, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 576, + "Description": "fix tidytable" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.13": { + "UpdateDate": 1722435875725, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 573, + "Description": "add username rendering for noticeboard" + }, + { + "PR": 574, + "Description": "disable tidytable" + }, + { + "PR": 575, + "Description": "Upgrade bootstrap + cdnjs" + }, + { + "PR": 576, + "Description": "fix tidytable" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.14": { + "UpdateDate": 1722470253103, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 580, + "Description": "Improve popover usability" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.15": { + "UpdateDate": 1722472465539, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 581, + "Description": "fix copymd" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.16": { + "UpdateDate": 1722478374388, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 583, + "Description": "fix ACM rank" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.17": { + "UpdateDate": 1722513002990, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 594, + "Description": "Revert Extern" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.18": { + "UpdateDate": 1722513340833, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 595, + "Description": "update pic" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.19": { + "UpdateDate": 1722513622681, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 596, + "Description": "ahhhhhh typos" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.20": { + "UpdateDate": 1722558316642, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 600, + "Description": "add some document.titles" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.21": { + "UpdateDate": 1722558913797, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 601, + "Description": "more document titles" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.22": { + "UpdateDate": 1722596300758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 608, + "Description": "美化导航栏(by zhouyiqing)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.23": { + "UpdateDate": 1722601450156, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 615, + "Description": "解决动画打断问题" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.24": { + "UpdateDate": 1722821084232, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 622, + "Description": "Extern contrib from @zhouyiqing0304" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.25": { + "UpdateDate": 1722923378941, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 629, + "Description": "Emergency fix" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.26": { + "UpdateDate": 1722934373375, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 631, + "Description": "Update More Names" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.27": { + "UpdateDate": 1722953708717, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 633, + "Description": "Add error Reporting" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.28": { + "UpdateDate": 1722992901032, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 634, + "Description": "减少歧义" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.29": { + "UpdateDate": 1722993130679, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 580, + "Description": "Improve popover usability" + }, + { + "PR": 581, + "Description": "fix copymd" + }, + { + "PR": 583, + "Description": "fix ACM rank" + }, + { + "PR": 594, + "Description": "Revert Extern" + }, + { + "PR": 595, + "Description": "update pic" + }, + { + "PR": 596, + "Description": "ahhhhhh typos" + }, + { + "PR": 600, + "Description": "add some document.titles" + }, + { + "PR": 601, + "Description": "more document titles" + }, + { + "PR": 608, + "Description": "美化导航栏(by zhouyiqing)" + }, + { + "PR": 615, + "Description": "解决动画打断问题" + }, + { + "PR": 622, + "Description": "Extern contrib from @zhouyiqing0304" + }, + { + "PR": 629, + "Description": "Emergency fix" + }, + { + "PR": 631, + "Description": "Update More Names" + }, + { + "PR": 633, + "Description": "Add error Reporting" + }, + { + "PR": 634, + "Description": "减少歧义" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.30": { + "UpdateDate": 1722995904167, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 639, + "Description": "Restore License" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.31": { + "UpdateDate": 1723001178967, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 642, + "Description": "freopen检测优化 (by zhouyiqing)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.32": { + "UpdateDate": 1723006469038, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 644, + "Description": "Better error Reporting" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.33": { + "UpdateDate": 1723007791910, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 645, + "Description": "Codemirror for freopen" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.34": { + "UpdateDate": 1723008021982, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 648, + "Description": "format freopen statement" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.35": { + "UpdateDate": 1723008839982, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 651, + "Description": "prevent RE" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.36": { + "UpdateDate": 1723018816861, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 653, + "Description": "Update a number of names" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.37": { + "UpdateDate": 1723094016287, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 655, + "Description": "Prevent RE" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.38": { + "UpdateDate": 1723094571329, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 657, + "Description": "log Success" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.39": { + "UpdateDate": 1723095200260, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 658, + "Description": "error reporting for image upload" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.40": { + "UpdateDate": 1723095733498, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 659, + "Description": "new toolbar item" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.41": { + "UpdateDate": 1723095901881, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 639, + "Description": "Restore License" + }, + { + "PR": 642, + "Description": "freopen检测优化 (by zhouyiqing)" + }, + { + "PR": 644, + "Description": "Better error Reporting" + }, + { + "PR": 645, + "Description": "Codemirror for freopen" + }, + { + "PR": 648, + "Description": "format freopen statement" + }, + { + "PR": 651, + "Description": "prevent RE" + }, + { + "PR": 653, + "Description": "Update a number of names" + }, + { + "PR": 655, + "Description": "Prevent RE" + }, + { + "PR": 657, + "Description": "log Success" + }, + { + "PR": 658, + "Description": "error reporting for image upload" + }, + { + "PR": 659, + "Description": "new toolbar item" + } + ], + "Notes": "minor release.
    Enjoy!😀" + }, + "1.2.42": { + "UpdateDate": 1723096626205, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 663, + "Description": "Do we really need this?" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.43": { + "UpdateDate": 1723097609941, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 664, + "Description": "fix reply infinite looping" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.44": { + "UpdateDate": 1723098260973, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 665, + "Description": "fix replying" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.45": { + "UpdateDate": 1723180072602, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 667, + "Description": "fix replying(again)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.46": { + "UpdateDate": 1723181112001, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 669, + "Description": "smartAlert!" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.47": { + "UpdateDate": 1723181393030, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 670, + "Description": "Fix RE" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.48": { + "UpdateDate": 1723183783191, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 671, + "Description": "允许在已结束的比赛下提交 #287" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.49": { + "UpdateDate": 1723184271477, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 673, + "Description": "Remove headers" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.50": { + "UpdateDate": 1723184583632, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 674, + "Description": "Stop!" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.51": { + "UpdateDate": 1723190703489, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 680, + "Description": " 解决强制O2的问题" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.52": { + "UpdateDate": 1723260210125, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 663, + "Description": "Do we really need this?" + }, + { + "PR": 664, + "Description": "fix reply infinite looping" + }, + { + "PR": 665, + "Description": "fix replying" + }, + { + "PR": 667, + "Description": "fix replying(again)" + }, + { + "PR": 669, + "Description": "smartAlert!" + }, + { + "PR": 670, + "Description": "Fix RE" + }, + { + "PR": 671, + "Description": "允许在已结束的比赛下提交 #287" + }, + { + "PR": 673, + "Description": "Remove headers" + }, + { + "PR": 674, + "Description": "Stop!" + }, + { + "PR": 680, + "Description": " 解决强制O2的问题" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.53": { + "UpdateDate": 1723352066327, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 690, + "Description": "移除之前忘记操作的admin" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.54": { + "UpdateDate": 1723450512410, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 694, + "Description": "更新support url" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.55": { + "UpdateDate": 1723461779306, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 698, + "Description": "Set turnstile theme and language" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.56": { + "UpdateDate": 1723956966458, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 704, + "Description": "登录界面优化" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.57": { + "UpdateDate": 1723968698208, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 712, + "Description": "更改个人中心一栏鼠标指针样式" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.58": { + "UpdateDate": 1724058379641, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 715, + "Description": "更改短消息显示" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.59": { + "UpdateDate": 1724836147641, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 718, + "Description": "Prevent caching" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.60": { + "UpdateDate": 1725005372794, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 690, + "Description": "移除之前忘记操作的admin" + }, + { + "PR": 694, + "Description": "更新support url" + }, + { + "PR": 698, + "Description": "Set turnstile theme and language" + }, + { + "PR": 704, + "Description": "登录界面优化" + }, + { + "PR": 712, + "Description": "更改个人中心一栏鼠标指针样式" + }, + { + "PR": 715, + "Description": "更改短消息显示" + }, + { + "PR": 718, + "Description": "Prevent caching" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.61": { + "UpdateDate": 1725083743111, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 725, + "Description": "Add CP Editor" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.62": { + "UpdateDate": 1726406228773, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 729, + "Description": "add chenyiming5" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.63": { + "UpdateDate": 1727743018230, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 731, + "Description": "a quick fix for mathjax" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.64": { + "UpdateDate": 1727853823983, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 735, + "Description": "更新一大批名字" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.65": { + "UpdateDate": 1727861782766, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 737, + "Description": "Add http request headers" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.66": { + "UpdateDate": 1727862388829, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 725, + "Description": "Add CP Editor" + }, + { + "PR": 729, + "Description": "add chenyiming5" + }, + { + "PR": 731, + "Description": "a quick fix for mathjax" + }, + { + "PR": 735, + "Description": "更新一大批名字" + }, + { + "PR": 737, + "Description": "Add http request headers" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.67": { + "UpdateDate": 1727873172840, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 741, + "Description": "Fix Duplicate Names (warned by Qoanda)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.68": { + "UpdateDate": 1727926979282, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 743, + "Description": "feat: 优化等待状态的显示" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.69": { + "UpdateDate": 1728563746855, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 748, + "Description": "变动cdn源" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.70": { + "UpdateDate": 1728784924115, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 741, + "Description": "Fix Duplicate Names (warned by Qoanda)" + }, + { + "PR": 743, + "Description": "feat: 优化等待状态的显示" + }, + { + "PR": 748, + "Description": "变动cdn源" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.71": { + "UpdateDate": 1732545478542, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 753, + "Description": "fix: 修复代码长度单位换算错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.72": { + "UpdateDate": 1738077477956, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 758, + "Description": "Fix #714" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.73": { + "UpdateDate": 1738925550389, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 753, + "Description": "fix: 修复代码长度单位换算错误" + }, + { + "PR": 758, + "Description": "Fix #714" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.74": { + "UpdateDate": 1738933722742, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 762, + "Description": "讨论回复跳转优化" + } + ], + "Notes": "This was so hard......" + }, + "1.2.75": { + "UpdateDate": 1738974525711, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 767, + "Description": "进入讨论后给出题目的链接 " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.3.0": { + "UpdateDate": 1738976786495, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 762, + "Description": "讨论回复跳转优化" + }, + { + "PR": 767, + "Description": "进入讨论后给出题目的链接 " + } + ], + "Notes": "If you are curious why the version number is v1.3.0, it's because we changed our versioning strategy! Click here for more details." + }, + "1.3.1": { + "UpdateDate": 1739060055956, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 774, + "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.3.2": { + "UpdateDate": 1739187728351, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 778, + "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.4.0": { + "UpdateDate": 1740147937411, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 774, + "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" + }, + { + "PR": 778, + "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.4.1": { + "UpdateDate": 1740314902240, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 784, + "Description": "fix cdn" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.0": { + "UpdateDate": 1740316085837, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 784, + "Description": "fix cdn" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.1": { + "UpdateDate": 1745307653685, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 791, + "Description": "remove ACM rankings" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.2": { + "UpdateDate": 1746264285564, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 796, + "Description": "Clear session cookie on logout for better security" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.3": { + "UpdateDate": 1746271148834, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 798, + "Description": "修复文字错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.6.0": { + "UpdateDate": 1746281721231, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 791, + "Description": "remove ACM rankings" + }, + { + "PR": 796, + "Description": "Clear session cookie on logout for better security" + }, + { + "PR": 798, + "Description": "修复文字错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.6.1": { + "UpdateDate": 1748781837958, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 802, + "Description": "Fix page flashes in dark mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.0": { + "UpdateDate": 1748873286643, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 802, + "Description": "Fix page flashes in dark mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.1": { + "UpdateDate": 1749457101103, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 806, + "Description": "Remove the statistics button from contest page" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.2": { + "UpdateDate": 1749865010938, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 808, + "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.3": { + "UpdateDate": 1750594769202, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 811, + "Description": "fix: Redirect to login page if profile element is missing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.8.0": { + "UpdateDate": 1750594823374, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 806, + "Description": "Remove the statistics button from contest page" + }, + { + "PR": 808, + "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" + }, + { + "PR": 811, + "Description": "fix: Redirect to login page if profile element is missing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.8.1": { + "UpdateDate": 1752061009119, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 817, + "Description": "fix: reinfo.php shows no test results even if test results are available " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.9.0": { + "UpdateDate": 1752132111562, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 817, + "Description": "fix: reinfo.php shows no test results even if test results are available " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.9.1": { + "UpdateDate": 1753443057292, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 821, + "Description": "修复工具栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.999990.0": { + "UpdateDate": 1753443146018, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 821, + "Description": "修复工具栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.0.0": { + "UpdateDate": 1754724044036, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 826, + "Description": "修复更新" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.0": { + "UpdateDate": 1754724455081, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 826, + "Description": "修复更新" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.1": { + "UpdateDate": 1754911555185, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 829, + "Description": "Update diff-match-patch library source URL" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.2": { + "UpdateDate": 1755589063111, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 834, + "Description": "feat: use credential management api" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.3": { + "UpdateDate": 1755596470421, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 835, + "Description": "feat: add user-selectable theme" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.2.0": { + "UpdateDate": 1755767075588, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 829, + "Description": "Update diff-match-patch library source URL" + }, + { + "PR": 834, + "Description": "feat: use credential management api" + }, + { + "PR": 835, + "Description": "feat: add user-selectable theme" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.2.1": { + "UpdateDate": 1756003502262, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 839, + "Description": "Fix formatting in feedback card text" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.2.2": { + "UpdateDate": 1757851046240, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 852, + "Description": "Fix the submit button on some pages" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.3.0": { + "UpdateDate": 1757851277491, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 839, + "Description": "Fix formatting in feedback card text" + }, + { + "PR": 852, + "Description": "Fix the submit button on some pages" + } + ], + "Notes": "随着 CI 更新, 相信以后的 release 都会有 release notes(" + }, + "2.3.1": { + "UpdateDate": 1758944205783, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 856, + "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" + } + ], + "Notes": "本版本修复了由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" + }, + "2.4.0": { + "UpdateDate": 1758956527554, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 856, + "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" + } + ], + "Notes": "请尽快升级至本版本!" + }, + "2.4.1": { + "UpdateDate": 1759412851092, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 861, + "Description": "Update CSS selector" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.4.2": { + "UpdateDate": 1759413584731, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 863, + "Description": "修复“NaN年前“" + } + ], + "Notes": "修复“NaN年前“" + }, + "2.4.3": { + "UpdateDate": 1759414259108, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 865, + "Description": "删除获取数据功能" + } + ], + "Notes": "因为它利用的bug被修复了" + }, + "2.4.4": { + "UpdateDate": 1759417362937, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 866, + "Description": "比赛题目页面里左侧栏加入题目序号列表" + } + ], + "Notes": "#860 ..." + }, + "2.4.5": { + "UpdateDate": 1759487413226, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 869, + "Description": "Update CSS selector (again...)" + } + ], + "Notes": "为什么这个破东西老是换位置" + }, + "2.4.6": { + "UpdateDate": 1759548958578, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 871, + "Description": "Add tooltip for ProblemSwitcher" + } + ], + "Notes": "现在, 你只需要将鼠标悬浮在比赛题目切换器上方, 即可查看题目名称" + }, + "2.4.7": { + "UpdateDate": 1759549826774, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 872, + "Description": "修复获取数据" + } + ], + "Notes": "funny" + }, + "2.5.0": { + "UpdateDate": 1759568103629, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 861, + "Description": "Update CSS selector" + }, + { + "PR": 863, + "Description": "修复“NaN年前“" + }, + { + "PR": 865, + "Description": "删除获取数据功能" + }, + { + "PR": 866, + "Description": "比赛题目页面里左侧栏加入题目序号列表" + }, + { + "PR": 869, + "Description": "Update CSS selector (again...)" + }, + { + "PR": 871, + "Description": "Add tooltip for ProblemSwitcher" + }, + { + "PR": 872, + "Description": "修复获取数据" + } + ], + "Notes": "XMOJ-Script 2.5.0 新增了比赛题目切换器,方便您在题目之间快速切换。此功能默认启用,您也可以在设置中禁用它。本版本还修复了一个导致时间显示为“NaN年前”的错误, 更新了一些 CSS selector,以适应 XMOJ 网站的最新变化." + }, + "2.5.1": { + "UpdateDate": 1759830659949, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 876, + "Description": "refactor: simplify SubmitLink selection by querying all anchors and finding by text" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.5.2": { + "UpdateDate": 1759845614758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 877, + "Description": "fix: problem PID 可能为 null" + } + ], + "Notes": "No release notes were provided for this release." } + } } diff --git a/XMOJ.user.js b/XMOJ.user.js index 9382516b..70d825f4 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 2.5.0 +// @version 2.5.2 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen diff --git a/package.json b/package.json index 5a1bb28c..ba12fbcc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "2.5.0", + "version": "2.5.2", "description": "an improvement script for xmoj.tech", "type": "module", "main": "dist/XMOJ.user.js", diff --git a/src/features/remove-useless.js b/src/features/remove-useless.js index 721c17a5..fec3e571 100644 --- a/src/features/remove-useless.js +++ b/src/features/remove-useless.js @@ -34,13 +34,13 @@ export function init() { // Remove marquee elements (scrolling banners) - Line 320-322 const marquee = document.getElementsByTagName("marquee")[0]; - if (marquee !== undefined) { + if (marquee !== undefined && marquee !== null) { marquee.remove(); } // Remove footer - Line 398-402 const footer = document.getElementsByClassName("footer")[0]; - if (footer !== null) { + if (footer !== undefined && footer !== null) { footer.remove(); } @@ -50,12 +50,12 @@ export function init() { // Problem page specific removals - Line 1222-1225 if (pathname === "/problem.php") { const langEnHeader = document.querySelector("h2.lang_en"); - if (langEnHeader) { + if (langEnHeader !== null) { langEnHeader.remove(); } const centerElements = document.getElementsByTagName("center"); - if (centerElements[1]) { + if (centerElements.length > 1 && centerElements[1] !== null) { centerElements[1].remove(); } } @@ -78,7 +78,7 @@ export function init() { // Problem solution page specific removals - Line 3209-3211 if (pathname === "/problem_solution.php") { const langEnHeader = document.querySelector("h2.lang_en"); - if (langEnHeader) { + if (langEnHeader !== null) { langEnHeader.remove(); } } diff --git a/src/pages/problem.js b/src/pages/problem.js index d20ba15a..fcdfb783 100644 --- a/src/pages/problem.js +++ b/src/pages/problem.js @@ -25,10 +25,26 @@ export async function init(context) { return; } - const PID = SearchParams.get("cid") + let PID = SearchParams.get("cid") ? localStorage.getItem(`UserScript-Contest-${SearchParams.get("cid")}-Problem-${SearchParams.get("pid")}-PID`) : SearchParams.get("id"); + // Handle null PID for contest problems (fetch from page) + if (PID === null && SearchParams.get("cid")) { + await fetch(location.href) + .then((response) => response.text()) + .then((response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + const allAnchors = parsedDocument.querySelectorAll('.mt-3 > center:nth-child(1) > a'); + const SubmitLink = Array.from(allAnchors).find(a => a.textContent.trim() === '提交'); + if (SubmitLink && SubmitLink.href) { + const url = new URL(SubmitLink.href); + PID = url.searchParams.get("id"); + localStorage.setItem(`UserScript-Contest-${SearchParams.get("cid")}-Problem-${SearchParams.get("pid")}-PID`, PID); + } + }); + } + // Fix spacing if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; @@ -65,22 +81,9 @@ export async function init(context) { * Fix submit button styling and behavior */ function fixSubmitButton() { - // Try multiple selectors to find the submit link (it keeps moving position) - const selectors = [ - '.mt-3 > center:nth-child(1) > a:nth-child(12)', - '.mt-3 > center:nth-child(1) > a:nth-child(10)', - '.mt-3 > center:nth-child(1) > a:nth-child(11)', - '.mt-3 > center:nth-child(1) > a:nth-child(13)', - '.mt-3 > center:nth-child(1) > a:nth-child(9)', - '.mt-3 > center:nth-child(1) > a:nth-child(7)', - '.mt-3 > center:nth-child(1) > a:nth-child(8)', - ]; - - let submitLink = null; - for (const selector of selectors) { - submitLink = document.querySelector(selector); - if (submitLink) break; - } + // Find submit link by text content (more reliable than nth-child selectors) + const links = document.querySelectorAll('.mt-3 > center:nth-child(1) > a'); + const submitLink = Array.from(links).find(a => a.textContent.trim() === '提交'); if (!submitLink) return; From f39411db2f22ac56bbb7662a60c745a56055c73f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 00:48:33 +0000 Subject: [PATCH 15/26] fix: Remove duplicate getCredential call and fix insertBefore error Fixed two critical login page bugs: 1. InvalidStateError: A request is already pending - Removed duplicate getCredential() call in bootstrap.js (lines 2867-2876) - SavePassword feature module already handles auto-fill, no need for duplicate code - This was causing concurrent credential API requests which is not allowed 2. NotFoundError: Failed to execute 'insertBefore' - Added proper null checks and parent verification before insertBefore call - DOM structure varies between pages, need to verify mt3 is actually a child of container - Now wrapped in try-catch to prevent crashes on non-standard page structures Changes: - src/core/bootstrap.js: Removed duplicate SavePassword code (lines 2867-2876) - src/core/bootstrap.js: Added safe insertBefore with null checks (line 610) These fixes should resolve all reported login page errors. --- src/core/bootstrap.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js index 909ac434..f646505d 100644 --- a/src/core/bootstrap.js +++ b/src/core/bootstrap.js @@ -607,7 +607,16 @@ export async function main() { }); } document.body.appendChild(UpdateDiv); - document.querySelector("body > div").insertBefore(UpdateDiv, document.querySelector("body > div > div.mt-3")); + // Try to move update div before mt-3, but handle DOM structure differences + try { + const container = document.querySelector("body > div"); + const mt3 = document.querySelector("body > div > div.mt-3"); + if (container && mt3 && mt3.parentNode === container) { + container.insertBefore(UpdateDiv, mt3); + } + } catch (e) { + console.warn('[XMOJ-Script] Could not reposition update div:', e); + } } if (localStorage.getItem("UserScript-Update-LastVersion") != GM_info.script.version) { localStorage.setItem("UserScript-Update-LastVersion", GM_info.script.version); @@ -2864,16 +2873,6 @@ export async function main() { }); } }); - if (UtilityEnabled("SavePassword")) { - (async () => { - let Credential = await getCredential(); - if (Credential) { - document.querySelector("#login > div:nth-child(1) > div > input").value = Credential.id; - document.querySelector("#login > div:nth-child(2) > div > input").value = Credential.password; - LoginButton.click(); - } - })(); - } } else if (location.pathname == "/contest_video.php" || location.pathname == "/problem_video.php") { let ScriptData = document.querySelector("body > div > div.mt-3 > center > script").innerHTML; if (document.getElementById("J_prismPlayer0").innerHTML != "") { From 7121eae6c6a7570eb5b6125ba12fe3c31ef7ccbb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 01:17:59 +0000 Subject: [PATCH 16/26] fix: Move login button handler to page module to fix non-functional button The login button was not working because of a timing issue: 1. bootstrap.js attached the click handler to the login button 2. Login page module then replaced the entire form HTML on window.load 3. This destroyed the button with the click handler, leaving a non-functional button Solution: - Moved all login handling logic from bootstrap.js to src/pages/login.js - Login page module now replaces the form AND attaches the handler in the correct order - Removed duplicate login form replacement code from bootstrap.js (lines 2797-2875) This ensures the click handler is attached AFTER the form HTML is finalized. Changes: - src/pages/login.js: Added complete login handling with error messages - src/core/bootstrap.js: Removed duplicate login page handling code The login button should now work correctly on /loginpage.php --- src/core/bootstrap.js | 79 -------------------------------------- src/pages/login.js | 89 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 83 deletions(-) diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js index f646505d..c7f3676f 100644 --- a/src/core/bootstrap.js +++ b/src/core/bootstrap.js @@ -2794,85 +2794,6 @@ export async function main() { }); } } - } else if (location.pathname == "/loginpage.php") { - if (UtilityEnabled("NewBootstrap")) { - document.querySelector("#login").innerHTML = `
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    `; - } - let ErrorText = document.createElement("div"); - ErrorText.style.color = "red"; - ErrorText.style.marginBottom = "5px"; - document.querySelector("#login").appendChild(ErrorText); - let LoginButton = document.getElementsByName("submit")[0]; - LoginButton.addEventListener("click", async () => { - let Username = document.getElementsByName("user_id")[0].value; - let Password = document.getElementsByName("password")[0].value; - if (Username == "" || Password == "") { - ErrorText.innerText = "用户名或密码不能为空"; - } else { - await fetch("https://www.xmoj.tech/login.php", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: "user_id=" + encodeURIComponent(Username) + "&password=" + hex_md5(Password) - }) - .then((Response) => { - return Response.text(); - }) - .then(async (Response) => { - if (UtilityEnabled("LoginFailed")) { - if (Response.indexOf("history.go(-2);") != -1) { - if (UtilityEnabled("SavePassword")) { - await storeCredential(Username, Password); - } - let NewPage = localStorage.getItem("UserScript-LastPage"); - if (NewPage == null) { - NewPage = "https://www.xmoj.tech/index.php"; - } - location.href = NewPage; - } else { - if (UtilityEnabled("SavePassword")) { - clearCredential(); - } - Response = Response.substring(Response.indexOf("alert('") + 7); - Response = Response.substring(0, Response.indexOf("');")); - if (Response == "UserName or Password Wrong!") { - ErrorText.innerText = "用户名或密码错误!"; - } else { - ErrorText.innerText = Response; - } - } - } else { - document.innerHTML = Response; - } - }); - } - }); } else if (location.pathname == "/contest_video.php" || location.pathname == "/problem_video.php") { let ScriptData = document.querySelector("body > div > div.mt-3 > center > script").innerHTML; if (document.getElementById("J_prismPlayer0").innerHTML != "") { diff --git a/src/pages/login.js b/src/pages/login.js index 952cca77..acc5e565 100644 --- a/src/pages/login.js +++ b/src/pages/login.js @@ -1,12 +1,10 @@ /** * Login Page Module * Handles all styling and functionality for /loginpage.php - * - * Note: Login functionality is handled by LoginFailed and SavePassword features - * This module only handles page styling */ import { UtilityEnabled } from '../core/config.js'; +import { storeCredential, clearCredential } from '../utils/credentials.js'; /** * Initialize login page @@ -18,7 +16,8 @@ export async function init(context) { replaceLoginForm(); } - // Login handling and SavePassword are handled by feature modules + // Attach login button handler + attachLoginHandler(); } /** @@ -59,3 +58,85 @@ function replaceLoginForm() { console.error('[Login] Error replacing login form:', error); } } + +/** + * Attach login button click handler + */ +function attachLoginHandler() { + try { + // Add error message div + const errorText = document.createElement("div"); + errorText.style.color = "red"; + errorText.style.marginBottom = "5px"; + const loginForm = document.querySelector("#login"); + if (!loginForm) return; + loginForm.appendChild(errorText); + + // Get login button + const loginButton = document.getElementsByName("submit")[0]; + if (!loginButton) { + console.warn('[Login] Login button not found'); + return; + } + + // Attach click handler + loginButton.addEventListener("click", async () => { + const username = document.getElementsByName("user_id")[0].value; + const password = document.getElementsByName("password")[0].value; + + if (username === "" || password === "") { + errorText.innerText = "用户名或密码不能为空"; + return; + } + + try { + const response = await fetch("https://www.xmoj.tech/login.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: "user_id=" + encodeURIComponent(username) + "&password=" + hex_md5(password) + }); + + const responseText = await response.text(); + + if (UtilityEnabled("LoginFailed")) { + if (responseText.indexOf("history.go(-2);") !== -1) { + // Login successful + if (UtilityEnabled("SavePassword")) { + await storeCredential(username, password); + } + + let newPage = localStorage.getItem("UserScript-LastPage"); + if (newPage === null) { + newPage = "https://www.xmoj.tech/index.php"; + } + location.href = newPage; + } else { + // Login failed + if (UtilityEnabled("SavePassword")) { + await clearCredential(); + } + + let errorMsg = responseText.substring(responseText.indexOf("alert('") + 7); + errorMsg = errorMsg.substring(0, errorMsg.indexOf("');")); + + if (errorMsg === "UserName or Password Wrong!") { + errorText.innerText = "用户名或密码错误!"; + } else { + errorText.innerText = errorMsg; + } + } + } else { + // LoginFailed feature disabled, use default behavior + document.innerHTML = responseText; + } + } catch (error) { + console.error('[Login] Error during login:', error); + errorText.innerText = "登录请求失败,请重试"; + } + }); + } catch (error) { + console.error('[Login] Error attaching login handler:', error); + } +} From c9adfda6ad277964b33bbed09e85cd1e72804d86 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 01:41:22 +0000 Subject: [PATCH 17/26] fix: Define SearchParams in bootstrap.js to fix ReferenceError Fixed "ReferenceError: SearchParams is not defined" error that broke the Discussions page and other pages using URL parameters. Problem: - SearchParams was used extensively throughout bootstrap.js main() function - But it was never defined in bootstrap.js - It was only created in main.js for page modules (different context) Solution: - Added `const SearchParams = new URLSearchParams(location.search)` at the beginning of the main() function in bootstrap.js This variable is now available to all code within main() that needs to read URL query parameters. Changes: - src/core/bootstrap.js: Added SearchParams definition at line 174 Fixes discussions page and any other page that relies on URL parameters. --- src/core/bootstrap.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js index c7f3676f..e1412b49 100644 --- a/src/core/bootstrap.js +++ b/src/core/bootstrap.js @@ -170,6 +170,9 @@ export function replaceMarkdownImages(text, string) { // Main initialization function export async function main() { try { + // Create SearchParams for use throughout bootstrap.js + const SearchParams = new URLSearchParams(location.search); + if (location.href.startsWith('http://')) { //use https location.href = location.href.replace('http://', 'https://'); From ba8be53a3d6662873cb8a2acf17e19cba43be330 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 01:44:55 +0000 Subject: [PATCH 18/26] fix: Define missing IsAdmin and CaptchaSiteKey variables Fixed two ReferenceError issues: 1. "IsAdmin is not defined" - Added IsAdmin variable definition after CurrentUsername - Set to AdminUserList.indexOf(CurrentUsername) !== -1 - Made globally available via window.IsAdmin for page modules 2. "CaptchaSiteKey is not defined" - Added CaptchaSiteKey constant at top of bootstrap.js - Set to "0x4AAAAAAALBT58IhyDViNmv" (Turnstile site key) - Made globally available via unsafeWindow/window for Turnstile callbacks These variables were missing after the refactoring and caused errors on various pages including discussions and userinfo pages. Changes: - src/core/bootstrap.js: Added CaptchaSiteKey constant (line 15) - src/core/bootstrap.js: Added IsAdmin variable (line 198) - Both made globally accessible for compatibility Fixes userinfo page and Turnstile captcha functionality. --- src/core/bootstrap.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js index e1412b49..0c4f9049 100644 --- a/src/core/bootstrap.js +++ b/src/core/bootstrap.js @@ -11,6 +11,15 @@ import { TidyTable } from '../utils/table.js'; import { compareVersions } from '../utils/version.js'; import { clearCredential } from '../utils/credentials.js'; +// Turnstile Captcha Site Key +const CaptchaSiteKey = "0x4AAAAAAALBT58IhyDViNmv"; +// Make it globally available for Turnstile callbacks +if (typeof unsafeWindow !== 'undefined') { + unsafeWindow.CaptchaSiteKey = CaptchaSiteKey; +} else if (typeof window !== 'undefined') { + window.CaptchaSiteKey = CaptchaSiteKey; +} + // Time difference for server synchronization (initialized to 0) let diff = 0; @@ -191,6 +200,10 @@ export async function main() { CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); } + // Check if current user is admin + let IsAdmin = AdminUserList.indexOf(CurrentUsername) !== -1; + window.IsAdmin = IsAdmin; // Make available globally for page modules + // Determine server URL let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me"); From 93a505cf0aff71e87f08ca3981925ae52a944767 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Wed, 10 Dec 2025 20:14:44 +0800 Subject: [PATCH 19/26] Upgrade Bootstrap and cdnjs dependencies Updated Bootstrap and cdnjs versions in bootstrap.js to improve compatibility and security. Also includes fixes for tidytable and popover usability, adds username rendering for noticeboard, and improves ACM rank display. --- Update.json | 6432 ++++++++++++++++++------------------ src/core/bootstrap.js | 1 + src/features/discussion.js | 26 +- src/features/index-page.js | 8 +- src/pages/userinfo.js | 22 +- src/utils/user.js | 6 +- 6 files changed, 3263 insertions(+), 3232 deletions(-) diff --git a/Update.json b/Update.json index 8d281d86..043b5b37 100644 --- a/Update.json +++ b/Update.json @@ -1,3219 +1,3219 @@ { - "UpdateHistory": { - "1.0.200": { - "UpdateDate": 1696254577068, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 0, - "Description": "" + "UpdateHistory": { + "1.0.200": { + "UpdateDate": 1696254577068, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 0, + "Description": "" + } + ] + }, + "1.0.201": { + "UpdateDate": 1696315453970, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 26, + "Description": "增加名字" + } + ] + }, + "1.0.202": { + "UpdateDate": 1696337480177, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 26, + "Description": "增加名字" + } + ] + }, + "1.0.203": { + "UpdateDate": 1696342022363, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 57, + "Description": "add more credits" + } + ] + }, + "1.0.204": { + "UpdateDate": 1696342420616, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 58, + "Description": "把@langningchen当作吉祥物" + } + ] + }, + "1.0.205": { + "UpdateDate": 1696384470503, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 57, + "Description": "add more credits" + }, + { + "PR": 58, + "Description": "把@langningchen当作吉祥物" + } + ] + }, + "1.0.206": { + "UpdateDate": 1696382789791, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 67, + "Description": "允许用户关闭获取数据,开启学术模式选择" + } + ] + }, + "1.0.207": { + "UpdateDate": 1696392248973, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 82, + "Description": "Dev Release" + } + ] + }, + "1.0.208": { + "UpdateDate": 1696429996532, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 95, + "Description": "修复编辑用户显示" + } + ] + }, + "1.0.209": { + "UpdateDate": 1696490377673, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 98, + "Description": "修复部分功能" + } + ] + }, + "1.0.210": { + "UpdateDate": 1696510135483, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 102, + "Description": "修复高亮" + } + ] + }, + "1.0.211": { + "UpdateDate": 1696514142283, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 104, + "Description": "自动提交当年代码" + } + ] + }, + "1.0.212": { + "UpdateDate": 1696515556253, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 105, + "Description": "emergency fix" + } + ] + }, + "1.0.213": { + "UpdateDate": 1696550511282, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 108, + "Description": "修复自动提交当年代码不计分" + } + ] + }, + "1.0.214": { + "UpdateDate": 1696551077104, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 67, + "Description": "允许用户关闭获取数据,开启学术模式选择" + }, + { + "PR": 82, + "Description": "Dev Release" + }, + { + "PR": 95, + "Description": "修复编辑用户显示" + }, + { + "PR": 98, + "Description": "修复部分功能" + }, + { + "PR": 102, + "Description": "修复高亮" + }, + { + "PR": 104, + "Description": "自动提交当年代码" + }, + { + "PR": 105, + "Description": "emergency fix" + }, + { + "PR": 108, + "Description": "修复自动提交当年代码不计分" + } + ] + }, + "1.0.215": { + "UpdateDate": 1696556056381, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 113, + "Description": "更改学术模式" + } + ] + }, + "1.0.216": { + "UpdateDate": 1696562219491, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 114, + "Description": "改变自动提交当年代码样式" + } + ] + }, + "1.0.217": { + "UpdateDate": 1696566723926, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 117, + "Description": "不自动提交已AC题目" + } + ] + }, + "1.0.218": { + "UpdateDate": 1696568744173, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 119, + "Description": "预编译使用C++14" + } + ] + }, + "1.0.219": { + "UpdateDate": 1696857809857, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 128, + "Description": "Why?" + } + ] + }, + "1.0.220": { + "UpdateDate": 1696859775005, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 132, + "Description": " bump version " + } + ] + }, + "1.0.221": { + "UpdateDate": 1696859889320, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 113, + "Description": "更改学术模式" + }, + { + "PR": 114, + "Description": "改变自动提交当年代码样式" + }, + { + "PR": 117, + "Description": "不自动提交已AC题目" + }, + { + "PR": 119, + "Description": "预编译使用C++14" + }, + { + "PR": 128, + "Description": "Why?" + }, + { + "PR": 132, + "Description": " bump version " + } + ] + }, + "1.0.222": { + "UpdateDate": 1696860193859, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 135, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.223": { + "UpdateDate": 1696860366697, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 135, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.224": { + "UpdateDate": 1697249568431, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 140, + "Description": "121 feature request 抄std的给badge作弊者" + } + ] + }, + "1.0.225": { + "UpdateDate": 1697351158955, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 144, + "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" + } + ] + }, + "1.0.226": { + "UpdateDate": 1697351411842, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 145, + "Description": "Emergency Fix" + } + ] + }, + "1.0.227": { + "UpdateDate": 1697367771069, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 149, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.228": { + "UpdateDate": 1697368165342, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 151, + "Description": "增加开发组成员" + } + ] + }, + "1.0.229": { + "UpdateDate": 1697640660452, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 156, + "Description": "修复部分题目状态页没有运行编号 (#155)" + } + ] + }, + "1.0.230": { + "UpdateDate": 1697725858941, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 160, + "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" + } + ] + }, + "1.0.231": { + "UpdateDate": 1697789116509, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 163, + "Description": "让cln的badge好看一点" + } + ] + }, + "1.0.232": { + "UpdateDate": 1697789214002, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 140, + "Description": "121 feature request 抄std的给badge作弊者" + }, + { + "PR": 144, + "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" + }, + { + "PR": 145, + "Description": "Emergency Fix" + }, + { + "PR": 149, + "Description": "Update XMOJ.user.js" + }, + { + "PR": 151, + "Description": "增加开发组成员" + }, + { + "PR": 156, + "Description": "修复部分题目状态页没有运行编号 (#155)" + }, + { + "PR": 160, + "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" + }, + { + "PR": 163, + "Description": "让cln的badge好看一点" + } + ] + }, + "1.0.233": { + "UpdateDate": 1697864346624, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 169, + "Description": "为用户脚本增加icon" + } + ] + }, + "1.0.234": { + "UpdateDate": 1698975944441, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 169, + "Description": "为用户脚本增加icon" + } + ] + }, + "1.0.235": { + "UpdateDate": 1699008412737, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 190, + "Description": "使用新的图床api" + } + ] + }, + "1.0.236": { + "UpdateDate": 1699069499723, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 195, + "Description": "不需要申请啦~" + } + ] + }, + "1.0.237": { + "UpdateDate": 1699078360562, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 197, + "Description": "修复更新链接" + } + ] + }, + "1.0.238": { + "UpdateDate": 1699617792536, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 190, + "Description": "使用新的图床api" + }, + { + "PR": 195, + "Description": "不需要申请啦~" + }, + { + "PR": 197, + "Description": "修复更新链接" + } + ] + }, + "1.0.239": { + "UpdateDate": 1699691535438, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 204, + "Description": "remove this as it causes console errors" + }, + { + "PR": 205, + "Description": "修复题目标题显示 #138" + } + ] + }, + "1.0.240": { + "UpdateDate": 1699766077528, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 205, + "Description": "修复题目标题显示 #138" + }, + { + "PR": 204, + "Description": "remove this as it causes console errors" + } + ] + }, + "1.0.241": { + "UpdateDate": 1699766855912, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 220, + "Description": "Update Prerelease.yml" + } + ] + }, + "1.0.242": { + "UpdateDate": 1699766973308, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 221, + "Description": "烦死了。。。" + } + ] + }, + "1.0.243": { + "UpdateDate": 1699767075836, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 222, + "Description": "Update Prerelease.yml" + } + ] + }, + "1.0.244": { + "UpdateDate": 1699767251761, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 223, + "Description": "wtf" + } + ] + }, + "1.0.245": { + "UpdateDate": 1700289812416, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 232, + "Description": "使用Base64编码icon" + } + ] + }, + "1.0.246": { + "UpdateDate": 1700921733760, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 250, + "Description": "发送统计数据" + } + ] + }, + "1.0.247": { + "UpdateDate": 1700922013381, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 220, + "Description": "Update Prerelease.yml" + }, + { + "PR": 221, + "Description": "烦死了。。。" + }, + { + "PR": 222, + "Description": "Update Prerelease.yml" + }, + { + "PR": 223, + "Description": "wtf" + }, + { + "PR": 232, + "Description": "使用Base64编码icon" + }, + { + "PR": 250, + "Description": "发送统计数据" + } + ] + }, + "1.0.248": { + "UpdateDate": 1700981994538, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 267, + "Description": "修复提交按钮" + } + ] + }, + "1.0.249": { + "UpdateDate": 1701180984140, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 269, + "Description": "显示最后在线时间" + } + ] + }, + "1.0.250": { + "UpdateDate": 1701264987062, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 273, + "Description": "send extra information" + } + ] + }, + "1.0.251": { + "UpdateDate": 1701426631116, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 276, + "Description": "Update XMOJ.user.js" + } + ] + }, + "1.0.252": { + "UpdateDate": 1701435211051, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 267, + "Description": "修复提交按钮" + }, + { + "PR": 269, + "Description": "显示最后在线时间" + }, + { + "PR": 273, + "Description": "send extra information" + }, + { + "PR": 276, + "Description": "Change the include statement" + } + ] + }, + "1.0.253": { + "UpdateDate": 1701491993821, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 282, + "Description": "use https in xmoj" + } + ] + }, + "1.0.254": { + "UpdateDate": 1701511837385, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 282, + "Description": "use https in xmoj" + } + ] + }, + "1.1.0": { + "UpdateDate": 1702641659793, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + } + ] + }, + "1.1.1": { + "UpdateDate": 1702641844861, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + } + ] + }, + "1.1.2": { + "UpdateDate": 1702687185849, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 301, + "Description": "改变更新架构" + } + ] + }, + "1.1.3": { + "UpdateDate": 1702821395564, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + } + ] + }, + "1.1.4": { + "UpdateDate": 1702822514246, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 305, + "Description": "add release notes (#303)" + } + ], + "Notes": "Hello, release notes! test 测试" + }, + "1.1.5": { + "UpdateDate": 1702993420758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 310, + "Description": "Add an Easter egg" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.6": { + "UpdateDate": 1702995326126, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 312, + "Description": "fix release notes" + } + ], + "Notes": "Welcome!" + }, + "1.1.7": { + "UpdateDate": 1703253098623, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "test: 这个算公告吗?@chenlangning" + }, + "1.1.8": { + "UpdateDate": 1703253440322, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 301, + "Description": "改变更新架构" + }, + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + }, + { + "PR": 305, + "Description": "add release notes (#303)" + }, + { + "PR": 310, + "Description": "Add an Easter egg" + }, + { + "PR": 312, + "Description": "fix release notes" + }, + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.9": { + "UpdateDate": 1703253440322, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 301, + "Description": "改变更新架构" + }, + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + }, + { + "PR": 305, + "Description": "add release notes (#303)" + }, + { + "PR": 310, + "Description": "Add an Easter egg" + }, + { + "PR": 312, + "Description": "fix release notes" + }, + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.10": { + "UpdateDate": 1703254332078, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 300, + "Description": "Add Docs" + }, + { + "PR": 301, + "Description": "改变更新架构" + }, + { + "PR": 302, + "Description": "Update allowed tags in PurifyHTML function to allow the tag" + }, + { + "PR": 305, + "Description": "add release notes (#303)" + }, + { + "PR": 310, + "Description": "Add an Easter egg" + }, + { + "PR": 312, + "Description": "fix release notes" + }, + { + "PR": 315, + "Description": "修复无法在某些页面检查登录状态" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.11": { + "UpdateDate": 1703923205023, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 327, + "Description": "优化用户体验" + } + ], + "Notes": "增加bug上报和主页按钮" + }, + "1.1.12": { + "UpdateDate": 1704017187302, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 329, + "Description": "增加权限" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.13": { + "UpdateDate": 1704936560583, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 340, + "Description": "add the native link" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.14": { + "UpdateDate": 1705756153752, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 355, + "Description": "fix #332" + }, + { + "PR": 356, + "Description": "N/A" + } + ], + "Notes": "修复题解标题" + }, + "1.1.15": { + "UpdateDate": 1705807807990, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 357, + "Description": "cleanup" + } + ], + "Notes": "This release fixes a lot of things" + }, + "1.1.16": { + "UpdateDate": 1705807913261, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 327, + "Description": "优化用户体验" + }, + { + "PR": 329, + "Description": "增加权限" + }, + { + "PR": 340, + "Description": "add the native link" + }, + { + "PR": 356, + "Description": "N/A" + }, + { + "PR": 355, + "Description": "fix #332" + }, + { + "PR": 357, + "Description": "cleanup" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.17": { + "UpdateDate": 1705808495397, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 327, + "Description": "优化用户体验" + }, + { + "PR": 329, + "Description": "增加权限" + }, + { + "PR": 340, + "Description": "add the native link" + }, + { + "PR": 356, + "Description": "N/A" + }, + { + "PR": 355, + "Description": "fix #332" + }, + { + "PR": 357, + "Description": "cleanup" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.18": { + "UpdateDate": 1705813603117, + "Prerelease": false, + "UpdateContents": [], + "Notes": "No release notes were provided for this release." + }, + "1.1.19": { + "UpdateDate": 1705841193051, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 372, + "Description": "use the same peram (?to_user) as xmoj" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.20": { + "UpdateDate": 1705929267062, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 375, + "Description": "add spellcheck" + } + ], + "Notes": "This release enables spell checking for bbs and short_msg! 🎉\nI did it 5 minutes after proposing it, swx." + }, + "1.1.21": { + "UpdateDate": 1705929832424, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 377, + "Description": "fix spellcheck" + } + ], + "Notes": "Oops, sorry. I forgot to add the spellcheck some text fields. Anyway, it's fixed now. 😅" + }, + "1.1.22": { + "UpdateDate": 1705983862747, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 378, + "Description": "sleep for a sec after submitting to prevent xmoj from crashing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.23": { + "UpdateDate": 1706103530551, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 390, + "Description": "更新support链接" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.24": { + "UpdateDate": 1706245175892, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 393, + "Description": "make the upload_std interface prettier" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.25": { + "UpdateDate": 1706250229435, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 394, + "Description": "fix problemstatus" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.28": { + "UpdateDate": 1706250296931, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 372, + "Description": "use the same peram (?to_user) as xmoj" + }, + { + "PR": 375, + "Description": "add spellcheck" + }, + { + "PR": 377, + "Description": "fix spellcheck" + }, + { + "PR": 378, + "Description": "sleep for a sec after submitting to prevent xmoj from crashing" + }, + { + "PR": 390, + "Description": "更新support链接" + }, + { + "PR": 393, + "Description": "make the upload_std interface prettier" + }, + { + "PR": 394, + "Description": "fix problemstatus" + } + ], + "Notes": "Note: v1.1.26-27 is gone.\nVersion 1.1.28 of xmoj-script ships a lot of quality of life improvements and bug fixes. \n\n- Add spell checking for bbs and short_msg\n- Fix the problem status page\n- Make the upload_std interface prettier\n- Use the same parameter as xmoj for the native link\n- Sleep for a second after submitting to prevent xmoj from crashing\n- Update support link \n Please note that upload_std now uploads the user's code if there is no std.\n See you in the next release!🎆" + }, + "1.1.30": { + "UpdateDate": 1706507043236, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 403, + "Description": "make the script much faster" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.31": { + "UpdateDate": 1706507178378, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 403, + "Description": "make the script much faster" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.32": { + "UpdateDate": 1706509019854, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 406, + "Description": "more choices" + } + ], + "Notes": "Because choices..." + }, + "1.1.33": { + "UpdateDate": 1706509685600, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 407, + "Description": "superdebug mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.34": { + "UpdateDate": 1706623811152, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 411, + "Description": "修正部分用户姓名设置的错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.35": { + "UpdateDate": 1706625057380, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 412, + "Description": "修改警告" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.36": { + "UpdateDate": 1706680652574, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 413, + "Description": "fix #409" + } + ], + "Notes": "A very important fix!" + }, + "1.1.37": { + "UpdateDate": 1706680926393, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 406, + "Description": "more choices" + }, + { + "PR": 407, + "Description": "superdebug mode" + }, + { + "PR": 411, + "Description": "修正部分用户姓名设置的错误" + }, + { + "PR": 412, + "Description": "修改警告" + }, + { + "PR": 413, + "Description": "fix #409" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.38": { + "UpdateDate": 1706681565820, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 416, + "Description": "major restructuring(fixes #364)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.39": { + "UpdateDate": 1706865355252, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 425, + "Description": "Revert 增加更新链接,优化格式" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.40": { + "UpdateDate": 1706867021708, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 428, + "Description": "更新部分语言" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.41": { + "UpdateDate": 1706867272746, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 429, + "Description": "增加开发组成员,zhouyiqing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.42": { + "UpdateDate": 1707642572244, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 445, + "Description": "fix [Bug] 右上角用户名点击后无反应 " + } + ], + "Notes": "Popper.js is so stupid..." + }, + "1.1.43": { + "UpdateDate": 1707715028113, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 447, + "Description": "fix the unpkg-cdn option" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.44": { + "UpdateDate": 1707803296933, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 449, + "Description": "regression!" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.45": { + "UpdateDate": 1708070431275, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 454, + "Description": "fix #400 + //ci-no-touch" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.46": { + "UpdateDate": 1708137046736, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 445, + "Description": "fix [Bug] 右上角用户名点击后无反应 " + }, + { + "PR": 447, + "Description": "fix the unpkg-cdn option" + }, + { + "PR": 449, + "Description": "regression!" + }, + { + "PR": 454, + "Description": "fix #400 + //ci-no-touch" + } + ], + "Notes": "A lot of QoL improvements!" + }, + "1.1.48": { + "UpdateDate": 1709370871510, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 485, + "Description": "and I am so frustrated I actually make another code contribution" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.49": { + "UpdateDate": 1709371832051, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 486, + "Description": "fix showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.50": { + "UpdateDate": 1710576508444, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 485, + "Description": "and I am so frustrated I actually make another code contribution" + }, + { + "PR": 486, + "Description": "fix showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.51": { + "UpdateDate": 1710641069919, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 491, + "Description": "Update ticket email" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.52": { + "UpdateDate": 1711848297024, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 493, + "Description": "Make the dismiss button work + improve showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.53": { + "UpdateDate": 1712374147729, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 491, + "Description": "Update ticket email" + }, + { + "PR": 493, + "Description": "Make the dismiss button work + improve showsource" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.54": { + "UpdateDate": 1712395969816, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 497, + "Description": "move the main msg content into a div with flex!!! :tada:" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.55": { + "UpdateDate": 1712411916005, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 499, + "Description": "add markdown support to short messages " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.56": { + "UpdateDate": 1713525771039, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 505, + "Description": "让 ctrl + enter 触发自动提交当年代码" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.57": { + "UpdateDate": 1713526164395, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 497, + "Description": "move the main msg content into a div with flex!!! :tada:" + }, + { + "PR": 499, + "Description": "add markdown support to short messages " + }, + { + "PR": 505, + "Description": "让 ctrl + enter 触发自动提交当年代码" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.58": { + "UpdateDate": 1713668825681, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 514, + "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.59": { + "UpdateDate": 1713676517652, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 515, + "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.60": { + "UpdateDate": 1713682768316, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 516, + "Description": "revert #514" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.61": { + "UpdateDate": 1714207526058, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 517, + "Description": "Auto Read Short message mentions" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.62": { + "UpdateDate": 1714208364065, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 519, + "Description": "Refresh Short Messages at fixed intervals (experimental)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.63": { + "UpdateDate": 1714740837007, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 522, + "Description": "chore: 删除被 @boomzero 除名的开发组成员" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.64": { + "UpdateDate": 1714819418530, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 523, + "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.65": { + "UpdateDate": 1714819512051, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 514, + "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" + }, + { + "PR": 515, + "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" + }, + { + "PR": 516, + "Description": "revert #514" + }, + { + "PR": 517, + "Description": "Auto Read Short message mentions" + }, + { + "PR": 519, + "Description": "Refresh Short Messages at fixed intervals (experimental)" + }, + { + "PR": 522, + "Description": "chore: 删除被 @boomzero 除名的开发组成员" + }, + { + "PR": 523, + "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.66": { + "UpdateDate": 1714821016028, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 526, + "Description": "[ImgBot] Optimize images" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.67": { + "UpdateDate": 1714822822741, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 526, + "Description": "[ImgBot] Optimize images" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.68": { + "UpdateDate": 1718670038883, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 544, + "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.69": { + "UpdateDate": 1719815069812, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 546, + "Description": "fix memory displays" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.70": { + "UpdateDate": 1719815850792, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 544, + "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" + }, + { + "PR": 546, + "Description": "fix memory displays" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.71": { + "UpdateDate": 1719843873067, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 550, + "Description": "Improve debug mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.1.72": { + "UpdateDate": 1721535836758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 552, + "Description": "Update the api domain" + } + ], + "Notes": "Please note that users using DebugMode should update immediately.
    The domain ghpages.xmoj-bbs.tech will be updated at a later date." + }, + "1.2.0": { + "UpdateDate": 1721638608232, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 550, + "Description": "Improve debug mode" + }, + { + "PR": 552, + "Description": "Update the api domain" + } + ], + "Notes": "Please update immediately, thank you.
    This release changes the api domain from xmoj-bbs.tech to xmoj-bbs.me" + }, + "1.2.1": { + "UpdateDate": 1721656184134, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 557, + "Description": "更改获取数据体验(增加报错)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.2": { + "UpdateDate": 1721887214365, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 559, + "Description": "Add CLion" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.3": { + "UpdateDate": 1722003346568, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 563, + "Description": "fix #562" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.4": { + "UpdateDate": 1722040221117, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 565, + "Description": "短消息增加图床支持" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.5": { + "UpdateDate": 1722044501316, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 566, + "Description": "预览版域名切换" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.6": { + "UpdateDate": 1722048484899, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 557, + "Description": "更改获取数据体验(增加报错)" + }, + { + "PR": 559, + "Description": "Add CLion" + }, + { + "PR": 563, + "Description": "fix #562" + }, + { + "PR": 565, + "Description": "短消息增加图床支持" + }, + { + "PR": 566, + "Description": "预览版域名切换" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.7": { + "UpdateDate": 1722063024309, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 570, + "Description": "增加公告栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.8": { + "UpdateDate": 1722070813624, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 570, + "Description": "增加公告栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.9": { + "UpdateDate": 1722088781344, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 573, + "Description": "add username rendering for noticeboard" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.10": { + "UpdateDate": 1722425862935, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 574, + "Description": "disable tidytable" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.11": { + "UpdateDate": 1722432013017, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 575, + "Description": "Upgrade bootstrap + cdnjs" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.12": { + "UpdateDate": 1722433643071, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 576, + "Description": "fix tidytable" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.13": { + "UpdateDate": 1722435875725, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 573, + "Description": "add username rendering for noticeboard" + }, + { + "PR": 574, + "Description": "disable tidytable" + }, + { + "PR": 575, + "Description": "Upgrade bootstrap + cdnjs" + }, + { + "PR": 576, + "Description": "fix tidytable" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.14": { + "UpdateDate": 1722470253103, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 580, + "Description": "Improve popover usability" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.15": { + "UpdateDate": 1722472465539, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 581, + "Description": "fix copymd" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.16": { + "UpdateDate": 1722478374388, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 583, + "Description": "fix ACM rank" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.17": { + "UpdateDate": 1722513002990, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 594, + "Description": "Revert Extern" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.18": { + "UpdateDate": 1722513340833, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 595, + "Description": "update pic" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.19": { + "UpdateDate": 1722513622681, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 596, + "Description": "ahhhhhh typos" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.20": { + "UpdateDate": 1722558316642, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 600, + "Description": "add some document.titles" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.21": { + "UpdateDate": 1722558913797, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 601, + "Description": "more document titles" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.22": { + "UpdateDate": 1722596300758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 608, + "Description": "美化导航栏(by zhouyiqing)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.23": { + "UpdateDate": 1722601450156, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 615, + "Description": "解决动画打断问题" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.24": { + "UpdateDate": 1722821084232, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 622, + "Description": "Extern contrib from @zhouyiqing0304" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.25": { + "UpdateDate": 1722923378941, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 629, + "Description": "Emergency fix" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.26": { + "UpdateDate": 1722934373375, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 631, + "Description": "Update More Names" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.27": { + "UpdateDate": 1722953708717, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 633, + "Description": "Add error Reporting" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.28": { + "UpdateDate": 1722992901032, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 634, + "Description": "减少歧义" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.29": { + "UpdateDate": 1722993130679, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 580, + "Description": "Improve popover usability" + }, + { + "PR": 581, + "Description": "fix copymd" + }, + { + "PR": 583, + "Description": "fix ACM rank" + }, + { + "PR": 594, + "Description": "Revert Extern" + }, + { + "PR": 595, + "Description": "update pic" + }, + { + "PR": 596, + "Description": "ahhhhhh typos" + }, + { + "PR": 600, + "Description": "add some document.titles" + }, + { + "PR": 601, + "Description": "more document titles" + }, + { + "PR": 608, + "Description": "美化导航栏(by zhouyiqing)" + }, + { + "PR": 615, + "Description": "解决动画打断问题" + }, + { + "PR": 622, + "Description": "Extern contrib from @zhouyiqing0304" + }, + { + "PR": 629, + "Description": "Emergency fix" + }, + { + "PR": 631, + "Description": "Update More Names" + }, + { + "PR": 633, + "Description": "Add error Reporting" + }, + { + "PR": 634, + "Description": "减少歧义" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.30": { + "UpdateDate": 1722995904167, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 639, + "Description": "Restore License" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.31": { + "UpdateDate": 1723001178967, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 642, + "Description": "freopen检测优化 (by zhouyiqing)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.32": { + "UpdateDate": 1723006469038, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 644, + "Description": "Better error Reporting" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.33": { + "UpdateDate": 1723007791910, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 645, + "Description": "Codemirror for freopen" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.34": { + "UpdateDate": 1723008021982, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 648, + "Description": "format freopen statement" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.35": { + "UpdateDate": 1723008839982, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 651, + "Description": "prevent RE" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.36": { + "UpdateDate": 1723018816861, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 653, + "Description": "Update a number of names" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.37": { + "UpdateDate": 1723094016287, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 655, + "Description": "Prevent RE" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.38": { + "UpdateDate": 1723094571329, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 657, + "Description": "log Success" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.39": { + "UpdateDate": 1723095200260, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 658, + "Description": "error reporting for image upload" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.40": { + "UpdateDate": 1723095733498, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 659, + "Description": "new toolbar item" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.41": { + "UpdateDate": 1723095901881, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 639, + "Description": "Restore License" + }, + { + "PR": 642, + "Description": "freopen检测优化 (by zhouyiqing)" + }, + { + "PR": 644, + "Description": "Better error Reporting" + }, + { + "PR": 645, + "Description": "Codemirror for freopen" + }, + { + "PR": 648, + "Description": "format freopen statement" + }, + { + "PR": 651, + "Description": "prevent RE" + }, + { + "PR": 653, + "Description": "Update a number of names" + }, + { + "PR": 655, + "Description": "Prevent RE" + }, + { + "PR": 657, + "Description": "log Success" + }, + { + "PR": 658, + "Description": "error reporting for image upload" + }, + { + "PR": 659, + "Description": "new toolbar item" + } + ], + "Notes": "minor release.
    Enjoy!😀" + }, + "1.2.42": { + "UpdateDate": 1723096626205, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 663, + "Description": "Do we really need this?" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.43": { + "UpdateDate": 1723097609941, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 664, + "Description": "fix reply infinite looping" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.44": { + "UpdateDate": 1723098260973, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 665, + "Description": "fix replying" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.45": { + "UpdateDate": 1723180072602, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 667, + "Description": "fix replying(again)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.46": { + "UpdateDate": 1723181112001, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 669, + "Description": "smartAlert!" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.47": { + "UpdateDate": 1723181393030, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 670, + "Description": "Fix RE" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.48": { + "UpdateDate": 1723183783191, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 671, + "Description": "允许在已结束的比赛下提交 #287" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.49": { + "UpdateDate": 1723184271477, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 673, + "Description": "Remove headers" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.50": { + "UpdateDate": 1723184583632, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 674, + "Description": "Stop!" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.51": { + "UpdateDate": 1723190703489, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 680, + "Description": " 解决强制O2的问题" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.52": { + "UpdateDate": 1723260210125, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 663, + "Description": "Do we really need this?" + }, + { + "PR": 664, + "Description": "fix reply infinite looping" + }, + { + "PR": 665, + "Description": "fix replying" + }, + { + "PR": 667, + "Description": "fix replying(again)" + }, + { + "PR": 669, + "Description": "smartAlert!" + }, + { + "PR": 670, + "Description": "Fix RE" + }, + { + "PR": 671, + "Description": "允许在已结束的比赛下提交 #287" + }, + { + "PR": 673, + "Description": "Remove headers" + }, + { + "PR": 674, + "Description": "Stop!" + }, + { + "PR": 680, + "Description": " 解决强制O2的问题" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.53": { + "UpdateDate": 1723352066327, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 690, + "Description": "移除之前忘记操作的admin" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.54": { + "UpdateDate": 1723450512410, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 694, + "Description": "更新support url" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.55": { + "UpdateDate": 1723461779306, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 698, + "Description": "Set turnstile theme and language" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.56": { + "UpdateDate": 1723956966458, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 704, + "Description": "登录界面优化" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.57": { + "UpdateDate": 1723968698208, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 712, + "Description": "更改个人中心一栏鼠标指针样式" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.58": { + "UpdateDate": 1724058379641, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 715, + "Description": "更改短消息显示" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.59": { + "UpdateDate": 1724836147641, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 718, + "Description": "Prevent caching" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.60": { + "UpdateDate": 1725005372794, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 690, + "Description": "移除之前忘记操作的admin" + }, + { + "PR": 694, + "Description": "更新support url" + }, + { + "PR": 698, + "Description": "Set turnstile theme and language" + }, + { + "PR": 704, + "Description": "登录界面优化" + }, + { + "PR": 712, + "Description": "更改个人中心一栏鼠标指针样式" + }, + { + "PR": 715, + "Description": "更改短消息显示" + }, + { + "PR": 718, + "Description": "Prevent caching" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.61": { + "UpdateDate": 1725083743111, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 725, + "Description": "Add CP Editor" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.62": { + "UpdateDate": 1726406228773, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 729, + "Description": "add chenyiming5" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.63": { + "UpdateDate": 1727743018230, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 731, + "Description": "a quick fix for mathjax" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.64": { + "UpdateDate": 1727853823983, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 735, + "Description": "更新一大批名字" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.65": { + "UpdateDate": 1727861782766, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 737, + "Description": "Add http request headers" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.66": { + "UpdateDate": 1727862388829, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 725, + "Description": "Add CP Editor" + }, + { + "PR": 729, + "Description": "add chenyiming5" + }, + { + "PR": 731, + "Description": "a quick fix for mathjax" + }, + { + "PR": 735, + "Description": "更新一大批名字" + }, + { + "PR": 737, + "Description": "Add http request headers" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.67": { + "UpdateDate": 1727873172840, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 741, + "Description": "Fix Duplicate Names (warned by Qoanda)" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.68": { + "UpdateDate": 1727926979282, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 743, + "Description": "feat: 优化等待状态的显示" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.69": { + "UpdateDate": 1728563746855, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 748, + "Description": "变动cdn源" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.70": { + "UpdateDate": 1728784924115, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 741, + "Description": "Fix Duplicate Names (warned by Qoanda)" + }, + { + "PR": 743, + "Description": "feat: 优化等待状态的显示" + }, + { + "PR": 748, + "Description": "变动cdn源" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.71": { + "UpdateDate": 1732545478542, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 753, + "Description": "fix: 修复代码长度单位换算错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.72": { + "UpdateDate": 1738077477956, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 758, + "Description": "Fix #714" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.73": { + "UpdateDate": 1738925550389, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 753, + "Description": "fix: 修复代码长度单位换算错误" + }, + { + "PR": 758, + "Description": "Fix #714" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.2.74": { + "UpdateDate": 1738933722742, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 762, + "Description": "讨论回复跳转优化" + } + ], + "Notes": "This was so hard......" + }, + "1.2.75": { + "UpdateDate": 1738974525711, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 767, + "Description": "进入讨论后给出题目的链接 " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.3.0": { + "UpdateDate": 1738976786495, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 762, + "Description": "讨论回复跳转优化" + }, + { + "PR": 767, + "Description": "进入讨论后给出题目的链接 " + } + ], + "Notes": "If you are curious why the version number is v1.3.0, it's because we changed our versioning strategy! Click here for more details." + }, + "1.3.1": { + "UpdateDate": 1739060055956, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 774, + "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.3.2": { + "UpdateDate": 1739187728351, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 778, + "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.4.0": { + "UpdateDate": 1740147937411, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 774, + "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" + }, + { + "PR": 778, + "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.4.1": { + "UpdateDate": 1740314902240, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 784, + "Description": "fix cdn" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.0": { + "UpdateDate": 1740316085837, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 784, + "Description": "fix cdn" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.1": { + "UpdateDate": 1745307653685, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 791, + "Description": "remove ACM rankings" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.2": { + "UpdateDate": 1746264285564, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 796, + "Description": "Clear session cookie on logout for better security" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.5.3": { + "UpdateDate": 1746271148834, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 798, + "Description": "修复文字错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.6.0": { + "UpdateDate": 1746281721231, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 791, + "Description": "remove ACM rankings" + }, + { + "PR": 796, + "Description": "Clear session cookie on logout for better security" + }, + { + "PR": 798, + "Description": "修复文字错误" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.6.1": { + "UpdateDate": 1748781837958, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 802, + "Description": "Fix page flashes in dark mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.0": { + "UpdateDate": 1748873286643, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 802, + "Description": "Fix page flashes in dark mode" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.1": { + "UpdateDate": 1749457101103, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 806, + "Description": "Remove the statistics button from contest page" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.2": { + "UpdateDate": 1749865010938, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 808, + "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.7.3": { + "UpdateDate": 1750594769202, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 811, + "Description": "fix: Redirect to login page if profile element is missing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.8.0": { + "UpdateDate": 1750594823374, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 806, + "Description": "Remove the statistics button from contest page" + }, + { + "PR": 808, + "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" + }, + { + "PR": 811, + "Description": "fix: Redirect to login page if profile element is missing" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.8.1": { + "UpdateDate": 1752061009119, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 817, + "Description": "fix: reinfo.php shows no test results even if test results are available " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.9.0": { + "UpdateDate": 1752132111562, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 817, + "Description": "fix: reinfo.php shows no test results even if test results are available " + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.9.1": { + "UpdateDate": 1753443057292, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 821, + "Description": "修复工具栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "1.10.0": { + "UpdateDate": 1753443146018, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 821, + "Description": "修复工具栏" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.0.0": { + "UpdateDate": 1754724044036, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 826, + "Description": "修复更新" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.0": { + "UpdateDate": 1754724455081, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 826, + "Description": "修复更新" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.1": { + "UpdateDate": 1754911555185, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 829, + "Description": "Update diff-match-patch library source URL" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.2": { + "UpdateDate": 1755589063111, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 834, + "Description": "feat: use credential management api" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.1.3": { + "UpdateDate": 1755596470421, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 835, + "Description": "feat: add user-selectable theme" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.2.0": { + "UpdateDate": 1755767075588, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 829, + "Description": "Update diff-match-patch library source URL" + }, + { + "PR": 834, + "Description": "feat: use credential management api" + }, + { + "PR": 835, + "Description": "feat: add user-selectable theme" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.2.1": { + "UpdateDate": 1756003502262, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 839, + "Description": "Fix formatting in feedback card text" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.2.2": { + "UpdateDate": 1757851046240, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 852, + "Description": "Fix the submit button on some pages" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.3.0": { + "UpdateDate": 1757851277491, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 839, + "Description": "Fix formatting in feedback card text" + }, + { + "PR": 852, + "Description": "Fix the submit button on some pages" + } + ], + "Notes": "随着 CI 更新, 相信以后的 release 都会有 release notes(" + }, + "2.3.1": { + "UpdateDate": 1758944205783, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 856, + "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" + } + ], + "Notes": "本版本修复了由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" + }, + "2.4.0": { + "UpdateDate": 1758956527554, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 856, + "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" + } + ], + "Notes": "请尽快升级至本版本!" + }, + "2.4.1": { + "UpdateDate": 1759412851092, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 861, + "Description": "Update CSS selector" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.4.2": { + "UpdateDate": 1759413584731, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 863, + "Description": "修复“NaN年前“" + } + ], + "Notes": "修复“NaN年前“" + }, + "2.4.3": { + "UpdateDate": 1759414259108, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 865, + "Description": "删除获取数据功能" + } + ], + "Notes": "因为它利用的bug被修复了" + }, + "2.4.4": { + "UpdateDate": 1759417362937, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 866, + "Description": "比赛题目页面里左侧栏加入题目序号列表" + } + ], + "Notes": "#860 ..." + }, + "2.4.5": { + "UpdateDate": 1759487413226, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 869, + "Description": "Update CSS selector (again...)" + } + ], + "Notes": "为什么这个破东西老是换位置" + }, + "2.4.6": { + "UpdateDate": 1759548958578, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 871, + "Description": "Add tooltip for ProblemSwitcher" + } + ], + "Notes": "现在, 你只需要将鼠标悬浮在比赛题目切换器上方, 即可查看题目名称" + }, + "2.4.7": { + "UpdateDate": 1759549826774, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 872, + "Description": "修复获取数据" + } + ], + "Notes": "funny" + }, + "2.5.0": { + "UpdateDate": 1759568103629, + "Prerelease": false, + "UpdateContents": [ + { + "PR": 861, + "Description": "Update CSS selector" + }, + { + "PR": 863, + "Description": "修复“NaN年前“" + }, + { + "PR": 865, + "Description": "删除获取数据功能" + }, + { + "PR": 866, + "Description": "比赛题目页面里左侧栏加入题目序号列表" + }, + { + "PR": 869, + "Description": "Update CSS selector (again...)" + }, + { + "PR": 871, + "Description": "Add tooltip for ProblemSwitcher" + }, + { + "PR": 872, + "Description": "修复获取数据" + } + ], + "Notes": "XMOJ-Script 2.5.0 新增了比赛题目切换器,方便您在题目之间快速切换。此功能默认启用,您也可以在设置中禁用它。本版本还修复了一个导致时间显示为“NaN年前”的错误, 更新了一些 CSS selector,以适应 XMOJ 网站的最新变化." + }, + "2.5.1": { + "UpdateDate": 1759830659949, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 876, + "Description": "refactor: simplify SubmitLink selection by querying all anchors and finding by text" + } + ], + "Notes": "No release notes were provided for this release." + }, + "2.5.2": { + "UpdateDate": 1759845614758, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 877, + "Description": "fix: problem PID 可能为 null" + } + ], + "Notes": "No release notes were provided for this release." } - ] - }, - "1.0.201": { - "UpdateDate": 1696315453970, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 26, - "Description": "增加名字" - } - ] - }, - "1.0.202": { - "UpdateDate": 1696337480177, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 26, - "Description": "增加名字" - } - ] - }, - "1.0.203": { - "UpdateDate": 1696342022363, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 57, - "Description": "add more credits" - } - ] - }, - "1.0.204": { - "UpdateDate": 1696342420616, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 58, - "Description": "把@langningchen当作吉祥物" - } - ] - }, - "1.0.205": { - "UpdateDate": 1696384470503, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 57, - "Description": "add more credits" - }, - { - "PR": 58, - "Description": "把@langningchen当作吉祥物" - } - ] - }, - "1.0.206": { - "UpdateDate": 1696382789791, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 67, - "Description": "允许用户关闭获取数据,开启学术模式选择" - } - ] - }, - "1.0.207": { - "UpdateDate": 1696392248973, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 82, - "Description": "Dev Release" - } - ] - }, - "1.0.208": { - "UpdateDate": 1696429996532, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 95, - "Description": "修复编辑用户显示" - } - ] - }, - "1.0.209": { - "UpdateDate": 1696490377673, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 98, - "Description": "修复部分功能" - } - ] - }, - "1.0.210": { - "UpdateDate": 1696510135483, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 102, - "Description": "修复高亮" - } - ] - }, - "1.0.211": { - "UpdateDate": 1696514142283, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 104, - "Description": "自动提交当年代码" - } - ] - }, - "1.0.212": { - "UpdateDate": 1696515556253, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 105, - "Description": "emergency fix" - } - ] - }, - "1.0.213": { - "UpdateDate": 1696550511282, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 108, - "Description": "修复自动提交当年代码不计分" - } - ] - }, - "1.0.214": { - "UpdateDate": 1696551077104, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 67, - "Description": "允许用户关闭获取数据,开启学术模式选择" - }, - { - "PR": 82, - "Description": "Dev Release" - }, - { - "PR": 95, - "Description": "修复编辑用户显示" - }, - { - "PR": 98, - "Description": "修复部分功能" - }, - { - "PR": 102, - "Description": "修复高亮" - }, - { - "PR": 104, - "Description": "自动提交当年代码" - }, - { - "PR": 105, - "Description": "emergency fix" - }, - { - "PR": 108, - "Description": "修复自动提交当年代码不计分" - } - ] - }, - "1.0.215": { - "UpdateDate": 1696556056381, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 113, - "Description": "更改学术模式" - } - ] - }, - "1.0.216": { - "UpdateDate": 1696562219491, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 114, - "Description": "改变自动提交当年代码样式" - } - ] - }, - "1.0.217": { - "UpdateDate": 1696566723926, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 117, - "Description": "不自动提交已AC题目" - } - ] - }, - "1.0.218": { - "UpdateDate": 1696568744173, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 119, - "Description": "预编译使用C++14" - } - ] - }, - "1.0.219": { - "UpdateDate": 1696857809857, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 128, - "Description": "Why?" - } - ] - }, - "1.0.220": { - "UpdateDate": 1696859775005, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 132, - "Description": " bump version " - } - ] - }, - "1.0.221": { - "UpdateDate": 1696859889320, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 113, - "Description": "更改学术模式" - }, - { - "PR": 114, - "Description": "改变自动提交当年代码样式" - }, - { - "PR": 117, - "Description": "不自动提交已AC题目" - }, - { - "PR": 119, - "Description": "预编译使用C++14" - }, - { - "PR": 128, - "Description": "Why?" - }, - { - "PR": 132, - "Description": " bump version " - } - ] - }, - "1.0.222": { - "UpdateDate": 1696860193859, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 135, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.223": { - "UpdateDate": 1696860366697, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 135, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.224": { - "UpdateDate": 1697249568431, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 140, - "Description": "121 feature request 抄std的给badge作弊者" - } - ] - }, - "1.0.225": { - "UpdateDate": 1697351158955, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 144, - "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" - } - ] - }, - "1.0.226": { - "UpdateDate": 1697351411842, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 145, - "Description": "Emergency Fix" - } - ] - }, - "1.0.227": { - "UpdateDate": 1697367771069, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 149, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.228": { - "UpdateDate": 1697368165342, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 151, - "Description": "增加开发组成员" - } - ] - }, - "1.0.229": { - "UpdateDate": 1697640660452, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 156, - "Description": "修复部分题目状态页没有运行编号 (#155)" - } - ] - }, - "1.0.230": { - "UpdateDate": 1697725858941, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 160, - "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" - } - ] - }, - "1.0.231": { - "UpdateDate": 1697789116509, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 163, - "Description": "让cln的badge好看一点" - } - ] - }, - "1.0.232": { - "UpdateDate": 1697789214002, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 140, - "Description": "121 feature request 抄std的给badge作弊者" - }, - { - "PR": 144, - "Description": "禁止用户修改badge,只允许用户向管理组提出修改请求" - }, - { - "PR": 145, - "Description": "Emergency Fix" - }, - { - "PR": 149, - "Description": "Update XMOJ.user.js" - }, - { - "PR": 151, - "Description": "增加开发组成员" - }, - { - "PR": 156, - "Description": "修复部分题目状态页没有运行编号 (#155)" - }, - { - "PR": 160, - "Description": "1.修改了大量中文 2.写明提醒内容 3.update 关键字词替换" - }, - { - "PR": 163, - "Description": "让cln的badge好看一点" - } - ] - }, - "1.0.233": { - "UpdateDate": 1697864346624, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 169, - "Description": "为用户脚本增加icon" - } - ] - }, - "1.0.234": { - "UpdateDate": 1698975944441, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 169, - "Description": "为用户脚本增加icon" - } - ] - }, - "1.0.235": { - "UpdateDate": 1699008412737, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 190, - "Description": "使用新的图床api" - } - ] - }, - "1.0.236": { - "UpdateDate": 1699069499723, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 195, - "Description": "不需要申请啦~" - } - ] - }, - "1.0.237": { - "UpdateDate": 1699078360562, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 197, - "Description": "修复更新链接" - } - ] - }, - "1.0.238": { - "UpdateDate": 1699617792536, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 190, - "Description": "使用新的图床api" - }, - { - "PR": 195, - "Description": "不需要申请啦~" - }, - { - "PR": 197, - "Description": "修复更新链接" - } - ] - }, - "1.0.239": { - "UpdateDate": 1699691535438, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 204, - "Description": "remove this as it causes console errors" - }, - { - "PR": 205, - "Description": "修复题目标题显示 #138" - } - ] - }, - "1.0.240": { - "UpdateDate": 1699766077528, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 205, - "Description": "修复题目标题显示 #138" - }, - { - "PR": 204, - "Description": "remove this as it causes console errors" - } - ] - }, - "1.0.241": { - "UpdateDate": 1699766855912, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 220, - "Description": "Update Prerelease.yml" - } - ] - }, - "1.0.242": { - "UpdateDate": 1699766973308, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 221, - "Description": "烦死了。。。" - } - ] - }, - "1.0.243": { - "UpdateDate": 1699767075836, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 222, - "Description": "Update Prerelease.yml" - } - ] - }, - "1.0.244": { - "UpdateDate": 1699767251761, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 223, - "Description": "wtf" - } - ] - }, - "1.0.245": { - "UpdateDate": 1700289812416, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 232, - "Description": "使用Base64编码icon" - } - ] - }, - "1.0.246": { - "UpdateDate": 1700921733760, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 250, - "Description": "发送统计数据" - } - ] - }, - "1.0.247": { - "UpdateDate": 1700922013381, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 220, - "Description": "Update Prerelease.yml" - }, - { - "PR": 221, - "Description": "烦死了。。。" - }, - { - "PR": 222, - "Description": "Update Prerelease.yml" - }, - { - "PR": 223, - "Description": "wtf" - }, - { - "PR": 232, - "Description": "使用Base64编码icon" - }, - { - "PR": 250, - "Description": "发送统计数据" - } - ] - }, - "1.0.248": { - "UpdateDate": 1700981994538, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 267, - "Description": "修复提交按钮" - } - ] - }, - "1.0.249": { - "UpdateDate": 1701180984140, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 269, - "Description": "显示最后在线时间" - } - ] - }, - "1.0.250": { - "UpdateDate": 1701264987062, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 273, - "Description": "send extra information" - } - ] - }, - "1.0.251": { - "UpdateDate": 1701426631116, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 276, - "Description": "Update XMOJ.user.js" - } - ] - }, - "1.0.252": { - "UpdateDate": 1701435211051, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 267, - "Description": "修复提交按钮" - }, - { - "PR": 269, - "Description": "显示最后在线时间" - }, - { - "PR": 273, - "Description": "send extra information" - }, - { - "PR": 276, - "Description": "Change the include statement" - } - ] - }, - "1.0.253": { - "UpdateDate": 1701491993821, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 282, - "Description": "use https in xmoj" - } - ] - }, - "1.0.254": { - "UpdateDate": 1701511837385, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 282, - "Description": "use https in xmoj" - } - ] - }, - "1.1.0": { - "UpdateDate": 1702641659793, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - } - ] - }, - "1.1.1": { - "UpdateDate": 1702641844861, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - } - ] - }, - "1.1.2": { - "UpdateDate": 1702687185849, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 301, - "Description": "改变更新架构" - } - ] - }, - "1.1.3": { - "UpdateDate": 1702821395564, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - } - ] - }, - "1.1.4": { - "UpdateDate": 1702822514246, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 305, - "Description": "add release notes (#303)" - } - ], - "Notes": "Hello, release notes! test 测试" - }, - "1.1.5": { - "UpdateDate": 1702993420758, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 310, - "Description": "Add an Easter egg" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.6": { - "UpdateDate": 1702995326126, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 312, - "Description": "fix release notes" - } - ], - "Notes": "Welcome!" - }, - "1.1.7": { - "UpdateDate": 1703253098623, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "test: 这个算公告吗?@chenlangning" - }, - "1.1.8": { - "UpdateDate": 1703253440322, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 301, - "Description": "改变更新架构" - }, - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - }, - { - "PR": 305, - "Description": "add release notes (#303)" - }, - { - "PR": 310, - "Description": "Add an Easter egg" - }, - { - "PR": 312, - "Description": "fix release notes" - }, - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.9": { - "UpdateDate": 1703253440322, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 301, - "Description": "改变更新架构" - }, - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - }, - { - "PR": 305, - "Description": "add release notes (#303)" - }, - { - "PR": 310, - "Description": "Add an Easter egg" - }, - { - "PR": 312, - "Description": "fix release notes" - }, - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.10": { - "UpdateDate": 1703254332078, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 300, - "Description": "Add Docs" - }, - { - "PR": 301, - "Description": "改变更新架构" - }, - { - "PR": 302, - "Description": "Update allowed tags in PurifyHTML function to allow the tag" - }, - { - "PR": 305, - "Description": "add release notes (#303)" - }, - { - "PR": 310, - "Description": "Add an Easter egg" - }, - { - "PR": 312, - "Description": "fix release notes" - }, - { - "PR": 315, - "Description": "修复无法在某些页面检查登录状态" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.11": { - "UpdateDate": 1703923205023, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 327, - "Description": "优化用户体验" - } - ], - "Notes": "增加bug上报和主页按钮" - }, - "1.1.12": { - "UpdateDate": 1704017187302, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 329, - "Description": "增加权限" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.13": { - "UpdateDate": 1704936560583, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 340, - "Description": "add the native link" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.14": { - "UpdateDate": 1705756153752, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 355, - "Description": "fix #332" - }, - { - "PR": 356, - "Description": "N/A" - } - ], - "Notes": "修复题解标题" - }, - "1.1.15": { - "UpdateDate": 1705807807990, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 357, - "Description": "cleanup" - } - ], - "Notes": "This release fixes a lot of things" - }, - "1.1.16": { - "UpdateDate": 1705807913261, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 327, - "Description": "优化用户体验" - }, - { - "PR": 329, - "Description": "增加权限" - }, - { - "PR": 340, - "Description": "add the native link" - }, - { - "PR": 356, - "Description": "N/A" - }, - { - "PR": 355, - "Description": "fix #332" - }, - { - "PR": 357, - "Description": "cleanup" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.17": { - "UpdateDate": 1705808495397, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 327, - "Description": "优化用户体验" - }, - { - "PR": 329, - "Description": "增加权限" - }, - { - "PR": 340, - "Description": "add the native link" - }, - { - "PR": 356, - "Description": "N/A" - }, - { - "PR": 355, - "Description": "fix #332" - }, - { - "PR": 357, - "Description": "cleanup" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.18": { - "UpdateDate": 1705813603117, - "Prerelease": false, - "UpdateContents": [], - "Notes": "No release notes were provided for this release." - }, - "1.1.19": { - "UpdateDate": 1705841193051, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 372, - "Description": "use the same peram (?to_user) as xmoj" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.20": { - "UpdateDate": 1705929267062, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 375, - "Description": "add spellcheck" - } - ], - "Notes": "This release enables spell checking for bbs and short_msg! 🎉\nI did it 5 minutes after proposing it, swx." - }, - "1.1.21": { - "UpdateDate": 1705929832424, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 377, - "Description": "fix spellcheck" - } - ], - "Notes": "Oops, sorry. I forgot to add the spellcheck some text fields. Anyway, it's fixed now. 😅" - }, - "1.1.22": { - "UpdateDate": 1705983862747, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 378, - "Description": "sleep for a sec after submitting to prevent xmoj from crashing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.23": { - "UpdateDate": 1706103530551, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 390, - "Description": "更新support链接" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.24": { - "UpdateDate": 1706245175892, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 393, - "Description": "make the upload_std interface prettier" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.25": { - "UpdateDate": 1706250229435, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 394, - "Description": "fix problemstatus" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.28": { - "UpdateDate": 1706250296931, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 372, - "Description": "use the same peram (?to_user) as xmoj" - }, - { - "PR": 375, - "Description": "add spellcheck" - }, - { - "PR": 377, - "Description": "fix spellcheck" - }, - { - "PR": 378, - "Description": "sleep for a sec after submitting to prevent xmoj from crashing" - }, - { - "PR": 390, - "Description": "更新support链接" - }, - { - "PR": 393, - "Description": "make the upload_std interface prettier" - }, - { - "PR": 394, - "Description": "fix problemstatus" - } - ], - "Notes": "Note: v1.1.26-27 is gone.\nVersion 1.1.28 of xmoj-script ships a lot of quality of life improvements and bug fixes. \n\n- Add spell checking for bbs and short_msg\n- Fix the problem status page\n- Make the upload_std interface prettier\n- Use the same parameter as xmoj for the native link\n- Sleep for a second after submitting to prevent xmoj from crashing\n- Update support link \n Please note that upload_std now uploads the user's code if there is no std.\n See you in the next release!🎆" - }, - "1.1.30": { - "UpdateDate": 1706507043236, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 403, - "Description": "make the script much faster" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.31": { - "UpdateDate": 1706507178378, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 403, - "Description": "make the script much faster" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.32": { - "UpdateDate": 1706509019854, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 406, - "Description": "more choices" - } - ], - "Notes": "Because choices..." - }, - "1.1.33": { - "UpdateDate": 1706509685600, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 407, - "Description": "superdebug mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.34": { - "UpdateDate": 1706623811152, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 411, - "Description": "修正部分用户姓名设置的错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.35": { - "UpdateDate": 1706625057380, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 412, - "Description": "修改警告" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.36": { - "UpdateDate": 1706680652574, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 413, - "Description": "fix #409" - } - ], - "Notes": "A very important fix!" - }, - "1.1.37": { - "UpdateDate": 1706680926393, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 406, - "Description": "more choices" - }, - { - "PR": 407, - "Description": "superdebug mode" - }, - { - "PR": 411, - "Description": "修正部分用户姓名设置的错误" - }, - { - "PR": 412, - "Description": "修改警告" - }, - { - "PR": 413, - "Description": "fix #409" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.38": { - "UpdateDate": 1706681565820, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 416, - "Description": "major restructuring(fixes #364)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.39": { - "UpdateDate": 1706865355252, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 425, - "Description": "Revert 增加更新链接,优化格式" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.40": { - "UpdateDate": 1706867021708, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 428, - "Description": "更新部分语言" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.41": { - "UpdateDate": 1706867272746, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 429, - "Description": "增加开发组成员,zhouyiqing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.42": { - "UpdateDate": 1707642572244, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 445, - "Description": "fix [Bug] 右上角用户名点击后无反应 " - } - ], - "Notes": "Popper.js is so stupid..." - }, - "1.1.43": { - "UpdateDate": 1707715028113, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 447, - "Description": "fix the unpkg-cdn option" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.44": { - "UpdateDate": 1707803296933, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 449, - "Description": "regression!" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.45": { - "UpdateDate": 1708070431275, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 454, - "Description": "fix #400 + //ci-no-touch" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.46": { - "UpdateDate": 1708137046736, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 445, - "Description": "fix [Bug] 右上角用户名点击后无反应 " - }, - { - "PR": 447, - "Description": "fix the unpkg-cdn option" - }, - { - "PR": 449, - "Description": "regression!" - }, - { - "PR": 454, - "Description": "fix #400 + //ci-no-touch" - } - ], - "Notes": "A lot of QoL improvements!" - }, - "1.1.48": { - "UpdateDate": 1709370871510, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 485, - "Description": "and I am so frustrated I actually make another code contribution" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.49": { - "UpdateDate": 1709371832051, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 486, - "Description": "fix showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.50": { - "UpdateDate": 1710576508444, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 485, - "Description": "and I am so frustrated I actually make another code contribution" - }, - { - "PR": 486, - "Description": "fix showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.51": { - "UpdateDate": 1710641069919, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 491, - "Description": "Update ticket email" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.52": { - "UpdateDate": 1711848297024, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 493, - "Description": "Make the dismiss button work + improve showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.53": { - "UpdateDate": 1712374147729, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 491, - "Description": "Update ticket email" - }, - { - "PR": 493, - "Description": "Make the dismiss button work + improve showsource" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.54": { - "UpdateDate": 1712395969816, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 497, - "Description": "move the main msg content into a div with flex!!! :tada:" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.55": { - "UpdateDate": 1712411916005, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 499, - "Description": "add markdown support to short messages " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.56": { - "UpdateDate": 1713525771039, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 505, - "Description": "让 ctrl + enter 触发自动提交当年代码" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.57": { - "UpdateDate": 1713526164395, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 497, - "Description": "move the main msg content into a div with flex!!! :tada:" - }, - { - "PR": 499, - "Description": "add markdown support to short messages " - }, - { - "PR": 505, - "Description": "让 ctrl + enter 触发自动提交当年代码" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.58": { - "UpdateDate": 1713668825681, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 514, - "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.59": { - "UpdateDate": 1713676517652, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 515, - "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.60": { - "UpdateDate": 1713682768316, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 516, - "Description": "revert #514" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.61": { - "UpdateDate": 1714207526058, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 517, - "Description": "Auto Read Short message mentions" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.62": { - "UpdateDate": 1714208364065, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 519, - "Description": "Refresh Short Messages at fixed intervals (experimental)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.63": { - "UpdateDate": 1714740837007, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 522, - "Description": "chore: 删除被 @boomzero 除名的开发组成员" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.64": { - "UpdateDate": 1714819418530, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 523, - "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.65": { - "UpdateDate": 1714819512051, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 514, - "Description": "支持跳转到讨论编号 修改讨论区用户界面 在讨论区Page过大时自动跳转 在讨论区最后一页自动删除" - }, - { - "PR": 515, - "Description": "允许管理在用户页管理badge并在修改badge后清除缓存" - }, - { - "PR": 516, - "Description": "revert #514" - }, - { - "PR": 517, - "Description": "Auto Read Short message mentions" - }, - { - "PR": 519, - "Description": "Refresh Short Messages at fixed intervals (experimental)" - }, - { - "PR": 522, - "Description": "chore: 删除被 @boomzero 除名的开发组成员" - }, - { - "PR": 523, - "Description": "Revert Refresh Short Messages at fixed intervals (experimental)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.66": { - "UpdateDate": 1714821016028, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 526, - "Description": "[ImgBot] Optimize images" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.67": { - "UpdateDate": 1714822822741, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 526, - "Description": "[ImgBot] Optimize images" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.68": { - "UpdateDate": 1718670038883, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 544, - "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.69": { - "UpdateDate": 1719815069812, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 546, - "Description": "fix memory displays" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.70": { - "UpdateDate": 1719815850792, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 544, - "Description": "fix https://www.xmoj.tech/open_contest_sign_up.php" - }, - { - "PR": 546, - "Description": "fix memory displays" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.71": { - "UpdateDate": 1719843873067, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 550, - "Description": "Improve debug mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.1.72": { - "UpdateDate": 1721535836758, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 552, - "Description": "Update the api domain" - } - ], - "Notes": "Please note that users using DebugMode should update immediately.
    The domain ghpages.xmoj-bbs.tech will be updated at a later date." - }, - "1.2.0": { - "UpdateDate": 1721638608232, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 550, - "Description": "Improve debug mode" - }, - { - "PR": 552, - "Description": "Update the api domain" - } - ], - "Notes": "Please update immediately, thank you.
    This release changes the api domain from xmoj-bbs.tech to xmoj-bbs.me" - }, - "1.2.1": { - "UpdateDate": 1721656184134, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 557, - "Description": "更改获取数据体验(增加报错)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.2": { - "UpdateDate": 1721887214365, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 559, - "Description": "Add CLion" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.3": { - "UpdateDate": 1722003346568, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 563, - "Description": "fix #562" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.4": { - "UpdateDate": 1722040221117, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 565, - "Description": "短消息增加图床支持" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.5": { - "UpdateDate": 1722044501316, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 566, - "Description": "预览版域名切换" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.6": { - "UpdateDate": 1722048484899, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 557, - "Description": "更改获取数据体验(增加报错)" - }, - { - "PR": 559, - "Description": "Add CLion" - }, - { - "PR": 563, - "Description": "fix #562" - }, - { - "PR": 565, - "Description": "短消息增加图床支持" - }, - { - "PR": 566, - "Description": "预览版域名切换" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.7": { - "UpdateDate": 1722063024309, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 570, - "Description": "增加公告栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.8": { - "UpdateDate": 1722070813624, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 570, - "Description": "增加公告栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.9": { - "UpdateDate": 1722088781344, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 573, - "Description": "add username rendering for noticeboard" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.10": { - "UpdateDate": 1722425862935, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 574, - "Description": "disable tidytable" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.11": { - "UpdateDate": 1722432013017, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 575, - "Description": "Upgrade bootstrap + cdnjs" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.12": { - "UpdateDate": 1722433643071, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 576, - "Description": "fix tidytable" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.13": { - "UpdateDate": 1722435875725, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 573, - "Description": "add username rendering for noticeboard" - }, - { - "PR": 574, - "Description": "disable tidytable" - }, - { - "PR": 575, - "Description": "Upgrade bootstrap + cdnjs" - }, - { - "PR": 576, - "Description": "fix tidytable" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.14": { - "UpdateDate": 1722470253103, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 580, - "Description": "Improve popover usability" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.15": { - "UpdateDate": 1722472465539, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 581, - "Description": "fix copymd" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.16": { - "UpdateDate": 1722478374388, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 583, - "Description": "fix ACM rank" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.17": { - "UpdateDate": 1722513002990, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 594, - "Description": "Revert Extern" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.18": { - "UpdateDate": 1722513340833, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 595, - "Description": "update pic" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.19": { - "UpdateDate": 1722513622681, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 596, - "Description": "ahhhhhh typos" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.20": { - "UpdateDate": 1722558316642, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 600, - "Description": "add some document.titles" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.21": { - "UpdateDate": 1722558913797, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 601, - "Description": "more document titles" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.22": { - "UpdateDate": 1722596300758, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 608, - "Description": "美化导航栏(by zhouyiqing)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.23": { - "UpdateDate": 1722601450156, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 615, - "Description": "解决动画打断问题" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.24": { - "UpdateDate": 1722821084232, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 622, - "Description": "Extern contrib from @zhouyiqing0304" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.25": { - "UpdateDate": 1722923378941, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 629, - "Description": "Emergency fix" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.26": { - "UpdateDate": 1722934373375, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 631, - "Description": "Update More Names" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.27": { - "UpdateDate": 1722953708717, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 633, - "Description": "Add error Reporting" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.28": { - "UpdateDate": 1722992901032, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 634, - "Description": "减少歧义" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.29": { - "UpdateDate": 1722993130679, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 580, - "Description": "Improve popover usability" - }, - { - "PR": 581, - "Description": "fix copymd" - }, - { - "PR": 583, - "Description": "fix ACM rank" - }, - { - "PR": 594, - "Description": "Revert Extern" - }, - { - "PR": 595, - "Description": "update pic" - }, - { - "PR": 596, - "Description": "ahhhhhh typos" - }, - { - "PR": 600, - "Description": "add some document.titles" - }, - { - "PR": 601, - "Description": "more document titles" - }, - { - "PR": 608, - "Description": "美化导航栏(by zhouyiqing)" - }, - { - "PR": 615, - "Description": "解决动画打断问题" - }, - { - "PR": 622, - "Description": "Extern contrib from @zhouyiqing0304" - }, - { - "PR": 629, - "Description": "Emergency fix" - }, - { - "PR": 631, - "Description": "Update More Names" - }, - { - "PR": 633, - "Description": "Add error Reporting" - }, - { - "PR": 634, - "Description": "减少歧义" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.30": { - "UpdateDate": 1722995904167, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 639, - "Description": "Restore License" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.31": { - "UpdateDate": 1723001178967, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 642, - "Description": "freopen检测优化 (by zhouyiqing)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.32": { - "UpdateDate": 1723006469038, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 644, - "Description": "Better error Reporting" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.33": { - "UpdateDate": 1723007791910, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 645, - "Description": "Codemirror for freopen" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.34": { - "UpdateDate": 1723008021982, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 648, - "Description": "format freopen statement" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.35": { - "UpdateDate": 1723008839982, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 651, - "Description": "prevent RE" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.36": { - "UpdateDate": 1723018816861, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 653, - "Description": "Update a number of names" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.37": { - "UpdateDate": 1723094016287, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 655, - "Description": "Prevent RE" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.38": { - "UpdateDate": 1723094571329, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 657, - "Description": "log Success" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.39": { - "UpdateDate": 1723095200260, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 658, - "Description": "error reporting for image upload" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.40": { - "UpdateDate": 1723095733498, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 659, - "Description": "new toolbar item" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.41": { - "UpdateDate": 1723095901881, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 639, - "Description": "Restore License" - }, - { - "PR": 642, - "Description": "freopen检测优化 (by zhouyiqing)" - }, - { - "PR": 644, - "Description": "Better error Reporting" - }, - { - "PR": 645, - "Description": "Codemirror for freopen" - }, - { - "PR": 648, - "Description": "format freopen statement" - }, - { - "PR": 651, - "Description": "prevent RE" - }, - { - "PR": 653, - "Description": "Update a number of names" - }, - { - "PR": 655, - "Description": "Prevent RE" - }, - { - "PR": 657, - "Description": "log Success" - }, - { - "PR": 658, - "Description": "error reporting for image upload" - }, - { - "PR": 659, - "Description": "new toolbar item" - } - ], - "Notes": "minor release.
    Enjoy!😀" - }, - "1.2.42": { - "UpdateDate": 1723096626205, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 663, - "Description": "Do we really need this?" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.43": { - "UpdateDate": 1723097609941, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 664, - "Description": "fix reply infinite looping" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.44": { - "UpdateDate": 1723098260973, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 665, - "Description": "fix replying" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.45": { - "UpdateDate": 1723180072602, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 667, - "Description": "fix replying(again)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.46": { - "UpdateDate": 1723181112001, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 669, - "Description": "smartAlert!" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.47": { - "UpdateDate": 1723181393030, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 670, - "Description": "Fix RE" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.48": { - "UpdateDate": 1723183783191, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 671, - "Description": "允许在已结束的比赛下提交 #287" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.49": { - "UpdateDate": 1723184271477, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 673, - "Description": "Remove headers" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.50": { - "UpdateDate": 1723184583632, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 674, - "Description": "Stop!" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.51": { - "UpdateDate": 1723190703489, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 680, - "Description": " 解决强制O2的问题" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.52": { - "UpdateDate": 1723260210125, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 663, - "Description": "Do we really need this?" - }, - { - "PR": 664, - "Description": "fix reply infinite looping" - }, - { - "PR": 665, - "Description": "fix replying" - }, - { - "PR": 667, - "Description": "fix replying(again)" - }, - { - "PR": 669, - "Description": "smartAlert!" - }, - { - "PR": 670, - "Description": "Fix RE" - }, - { - "PR": 671, - "Description": "允许在已结束的比赛下提交 #287" - }, - { - "PR": 673, - "Description": "Remove headers" - }, - { - "PR": 674, - "Description": "Stop!" - }, - { - "PR": 680, - "Description": " 解决强制O2的问题" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.53": { - "UpdateDate": 1723352066327, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 690, - "Description": "移除之前忘记操作的admin" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.54": { - "UpdateDate": 1723450512410, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 694, - "Description": "更新support url" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.55": { - "UpdateDate": 1723461779306, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 698, - "Description": "Set turnstile theme and language" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.56": { - "UpdateDate": 1723956966458, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 704, - "Description": "登录界面优化" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.57": { - "UpdateDate": 1723968698208, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 712, - "Description": "更改个人中心一栏鼠标指针样式" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.58": { - "UpdateDate": 1724058379641, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 715, - "Description": "更改短消息显示" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.59": { - "UpdateDate": 1724836147641, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 718, - "Description": "Prevent caching" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.60": { - "UpdateDate": 1725005372794, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 690, - "Description": "移除之前忘记操作的admin" - }, - { - "PR": 694, - "Description": "更新support url" - }, - { - "PR": 698, - "Description": "Set turnstile theme and language" - }, - { - "PR": 704, - "Description": "登录界面优化" - }, - { - "PR": 712, - "Description": "更改个人中心一栏鼠标指针样式" - }, - { - "PR": 715, - "Description": "更改短消息显示" - }, - { - "PR": 718, - "Description": "Prevent caching" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.61": { - "UpdateDate": 1725083743111, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 725, - "Description": "Add CP Editor" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.62": { - "UpdateDate": 1726406228773, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 729, - "Description": "add chenyiming5" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.63": { - "UpdateDate": 1727743018230, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 731, - "Description": "a quick fix for mathjax" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.64": { - "UpdateDate": 1727853823983, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 735, - "Description": "更新一大批名字" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.65": { - "UpdateDate": 1727861782766, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 737, - "Description": "Add http request headers" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.66": { - "UpdateDate": 1727862388829, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 725, - "Description": "Add CP Editor" - }, - { - "PR": 729, - "Description": "add chenyiming5" - }, - { - "PR": 731, - "Description": "a quick fix for mathjax" - }, - { - "PR": 735, - "Description": "更新一大批名字" - }, - { - "PR": 737, - "Description": "Add http request headers" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.67": { - "UpdateDate": 1727873172840, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 741, - "Description": "Fix Duplicate Names (warned by Qoanda)" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.68": { - "UpdateDate": 1727926979282, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 743, - "Description": "feat: 优化等待状态的显示" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.69": { - "UpdateDate": 1728563746855, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 748, - "Description": "变动cdn源" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.70": { - "UpdateDate": 1728784924115, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 741, - "Description": "Fix Duplicate Names (warned by Qoanda)" - }, - { - "PR": 743, - "Description": "feat: 优化等待状态的显示" - }, - { - "PR": 748, - "Description": "变动cdn源" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.71": { - "UpdateDate": 1732545478542, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 753, - "Description": "fix: 修复代码长度单位换算错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.72": { - "UpdateDate": 1738077477956, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 758, - "Description": "Fix #714" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.73": { - "UpdateDate": 1738925550389, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 753, - "Description": "fix: 修复代码长度单位换算错误" - }, - { - "PR": 758, - "Description": "Fix #714" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.2.74": { - "UpdateDate": 1738933722742, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 762, - "Description": "讨论回复跳转优化" - } - ], - "Notes": "This was so hard......" - }, - "1.2.75": { - "UpdateDate": 1738974525711, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 767, - "Description": "进入讨论后给出题目的链接 " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.3.0": { - "UpdateDate": 1738976786495, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 762, - "Description": "讨论回复跳转优化" - }, - { - "PR": 767, - "Description": "进入讨论后给出题目的链接 " - } - ], - "Notes": "If you are curious why the version number is v1.3.0, it's because we changed our versioning strategy! Click here for more details." - }, - "1.3.1": { - "UpdateDate": 1739060055956, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 774, - "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.3.2": { - "UpdateDate": 1739187728351, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 778, - "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.4.0": { - "UpdateDate": 1740147937411, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 774, - "Description": "Fix code scanning alert - DOM text reinterpreted as HTML" - }, - { - "PR": 778, - "Description": "Change how we load addonscript (transition from GitHub pages to Cloudflare KV) + Fix *that* console error" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.4.1": { - "UpdateDate": 1740314902240, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 784, - "Description": "fix cdn" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.0": { - "UpdateDate": 1740316085837, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 784, - "Description": "fix cdn" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.1": { - "UpdateDate": 1745307653685, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 791, - "Description": "remove ACM rankings" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.2": { - "UpdateDate": 1746264285564, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 796, - "Description": "Clear session cookie on logout for better security" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.5.3": { - "UpdateDate": 1746271148834, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 798, - "Description": "修复文字错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.6.0": { - "UpdateDate": 1746281721231, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 791, - "Description": "remove ACM rankings" - }, - { - "PR": 796, - "Description": "Clear session cookie on logout for better security" - }, - { - "PR": 798, - "Description": "修复文字错误" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.6.1": { - "UpdateDate": 1748781837958, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 802, - "Description": "Fix page flashes in dark mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.0": { - "UpdateDate": 1748873286643, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 802, - "Description": "Fix page flashes in dark mode" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.1": { - "UpdateDate": 1749457101103, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 806, - "Description": "Remove the statistics button from contest page" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.2": { - "UpdateDate": 1749865010938, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 808, - "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.7.3": { - "UpdateDate": 1750594769202, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 811, - "Description": "fix: Redirect to login page if profile element is missing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.8.0": { - "UpdateDate": 1750594823374, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 806, - "Description": "Remove the statistics button from contest page" - }, - { - "PR": 808, - "Description": "feat: Enhance keyboard shortcuts for better cross-platform support" - }, - { - "PR": 811, - "Description": "fix: Redirect to login page if profile element is missing" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.8.1": { - "UpdateDate": 1752061009119, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 817, - "Description": "fix: reinfo.php shows no test results even if test results are available " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.9.0": { - "UpdateDate": 1752132111562, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 817, - "Description": "fix: reinfo.php shows no test results even if test results are available " - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.9.1": { - "UpdateDate": 1753443057292, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 821, - "Description": "修复工具栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "1.999990.0": { - "UpdateDate": 1753443146018, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 821, - "Description": "修复工具栏" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.0.0": { - "UpdateDate": 1754724044036, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 826, - "Description": "修复更新" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.0": { - "UpdateDate": 1754724455081, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 826, - "Description": "修复更新" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.1": { - "UpdateDate": 1754911555185, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 829, - "Description": "Update diff-match-patch library source URL" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.2": { - "UpdateDate": 1755589063111, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 834, - "Description": "feat: use credential management api" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.1.3": { - "UpdateDate": 1755596470421, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 835, - "Description": "feat: add user-selectable theme" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.2.0": { - "UpdateDate": 1755767075588, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 829, - "Description": "Update diff-match-patch library source URL" - }, - { - "PR": 834, - "Description": "feat: use credential management api" - }, - { - "PR": 835, - "Description": "feat: add user-selectable theme" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.2.1": { - "UpdateDate": 1756003502262, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 839, - "Description": "Fix formatting in feedback card text" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.2.2": { - "UpdateDate": 1757851046240, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 852, - "Description": "Fix the submit button on some pages" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.3.0": { - "UpdateDate": 1757851277491, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 839, - "Description": "Fix formatting in feedback card text" - }, - { - "PR": 852, - "Description": "Fix the submit button on some pages" - } - ], - "Notes": "随着 CI 更新, 相信以后的 release 都会有 release notes(" - }, - "2.3.1": { - "UpdateDate": 1758944205783, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 856, - "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" - } - ], - "Notes": "本版本修复了由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" - }, - "2.4.0": { - "UpdateDate": 1758956527554, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 856, - "Description": "修复由于 PHPSESSID 启用了 httpOnly 导致的后台不可用" - } - ], - "Notes": "请尽快升级至本版本!" - }, - "2.4.1": { - "UpdateDate": 1759412851092, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 861, - "Description": "Update CSS selector" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.4.2": { - "UpdateDate": 1759413584731, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 863, - "Description": "修复“NaN年前“" - } - ], - "Notes": "修复“NaN年前“" - }, - "2.4.3": { - "UpdateDate": 1759414259108, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 865, - "Description": "删除获取数据功能" - } - ], - "Notes": "因为它利用的bug被修复了" - }, - "2.4.4": { - "UpdateDate": 1759417362937, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 866, - "Description": "比赛题目页面里左侧栏加入题目序号列表" - } - ], - "Notes": "#860 ..." - }, - "2.4.5": { - "UpdateDate": 1759487413226, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 869, - "Description": "Update CSS selector (again...)" - } - ], - "Notes": "为什么这个破东西老是换位置" - }, - "2.4.6": { - "UpdateDate": 1759548958578, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 871, - "Description": "Add tooltip for ProblemSwitcher" - } - ], - "Notes": "现在, 你只需要将鼠标悬浮在比赛题目切换器上方, 即可查看题目名称" - }, - "2.4.7": { - "UpdateDate": 1759549826774, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 872, - "Description": "修复获取数据" - } - ], - "Notes": "funny" - }, - "2.5.0": { - "UpdateDate": 1759568103629, - "Prerelease": false, - "UpdateContents": [ - { - "PR": 861, - "Description": "Update CSS selector" - }, - { - "PR": 863, - "Description": "修复“NaN年前“" - }, - { - "PR": 865, - "Description": "删除获取数据功能" - }, - { - "PR": 866, - "Description": "比赛题目页面里左侧栏加入题目序号列表" - }, - { - "PR": 869, - "Description": "Update CSS selector (again...)" - }, - { - "PR": 871, - "Description": "Add tooltip for ProblemSwitcher" - }, - { - "PR": 872, - "Description": "修复获取数据" - } - ], - "Notes": "XMOJ-Script 2.5.0 新增了比赛题目切换器,方便您在题目之间快速切换。此功能默认启用,您也可以在设置中禁用它。本版本还修复了一个导致时间显示为“NaN年前”的错误, 更新了一些 CSS selector,以适应 XMOJ 网站的最新变化." - }, - "2.5.1": { - "UpdateDate": 1759830659949, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 876, - "Description": "refactor: simplify SubmitLink selection by querying all anchors and finding by text" - } - ], - "Notes": "No release notes were provided for this release." - }, - "2.5.2": { - "UpdateDate": 1759845614758, - "Prerelease": true, - "UpdateContents": [ - { - "PR": 877, - "Description": "fix: problem PID 可能为 null" - } - ], - "Notes": "No release notes were provided for this release." } - } -} +} \ No newline at end of file diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js index 0c4f9049..2fa777c7 100644 --- a/src/core/bootstrap.js +++ b/src/core/bootstrap.js @@ -2120,6 +2120,7 @@ export async function main() { ), ); } + let rPID; rPID = contestProblems[new URL(location.href).searchParams.get("pid")]; if (UtilityEnabled("DebugMode")) { console.log("Contest Problems:", contestProblems); diff --git a/src/features/discussion.js b/src/features/discussion.js index 1e7ad8d0..d2dc1b0d 100644 --- a/src/features/discussion.js +++ b/src/features/discussion.js @@ -205,7 +205,8 @@ export function init(context) { TitleLink.classList.add("link-secondary"); TitleLink.innerHTML = "🔒 "; } - TitleLink.innerHTML += Posts[i].Title; + // Avoid re-parsing by appending a text node + TitleLink.appendChild(document.createTextNode(Posts[i].Title)); let AuthorCell = document.createElement("td"); Row.appendChild(AuthorCell); GetUsernameHTML(AuthorCell, Posts[i].UserID); @@ -704,9 +705,26 @@ export function init(context) { ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); if (Replies[i].EditTime != null) { if (Replies[i].EditPerson == Replies[i].UserID) { - ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; + { + const span = document.createElement('span'); + span.className = 'text-muted'; + span.style.fontSize = '12px'; + span.appendChild(document.createTextNode(`最后编辑于${GetRelativeTime(Replies[i].EditTime)}`)); + ReplyContentElement.appendChild(span); + } } else { - ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; + { + const outer = document.createElement('span'); + outer.className = 'text-muted'; + outer.style.fontSize = '12px'; + outer.appendChild(document.createTextNode('最后被')); + const userSpan = document.createElement('span'); + userSpan.className = 'Usernames'; + userSpan.appendChild(document.createTextNode(Replies[i].EditPerson)); + outer.appendChild(userSpan); + outer.appendChild(document.createTextNode(`编辑于${GetRelativeTime(Replies[i].EditTime)}`)); + ReplyContentElement.appendChild(outer); + } } } let ContentEditElement = document.createElement("div"); @@ -794,7 +812,7 @@ export function init(context) { let LockUsernameSpan = document.createElement("span"); LockElement.appendChild(LockUsernameSpan); GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); - LockElement.innerHTML += " 锁定"; + LockElement.appendChild(document.createTextNode(" 锁定")); LockElement.classList.add("mb-5"); } diff --git a/src/features/index-page.js b/src/features/index-page.js index a92c12cf..79042108 100644 --- a/src/features/index-page.js +++ b/src/features/index-page.js @@ -1,9 +1,9 @@ // Index page handler -import { UtilityEnabled } from '../utils/settings.js'; +import { UtilityEnabled } from '../core/config.js'; import { RequestAPI } from '../utils/api.js'; -import { TidyTable } from '../utils/dom.js'; +import { TidyTable } from '../utils/table.js'; import { RenderMathJax } from '../utils/mathjax.js'; -import { GetUsernameHTML } from '../utils/username.js'; +import { GetUsernameHTML } from '../utils/user.js'; /** * Handle index page (/index.php or /) @@ -142,7 +142,7 @@ export async function handleIndexPage(context) { }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { "ID": "ReplaceXM", "Type": "F", - "Name": "将网站中所有"小明"和"我"关键字替换为"高老师",所有"小红"替换为"徐师娘",所有"小粉"替换为"彩虹",所有"下海"、"海上"替换为"上海" (此功能默认关闭)" + "Name": "将网站中所有\"小明\"和\"我\"关键字替换为\"高老师\",所有\"小红\"替换为\"徐师娘\",所有\"小粉\"替换为\"彩虹\",所有\"下海\"、\"海上\"替换为\"上海\" (此功能默认关闭)" }] }, { "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" diff --git a/src/pages/userinfo.js b/src/pages/userinfo.js index b6c3edc2..ab342067 100644 --- a/src/pages/userinfo.js +++ b/src/pages/userinfo.js @@ -180,11 +180,15 @@ async function createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUs const userInfoElement = document.createElement("div"); userInfoElement.classList.add("col-auto"); userInfoElement.style.lineHeight = "40px"; - userInfoElement.innerHTML += `用户名:${userId}
    `; - userInfoElement.innerHTML += `昵称:${userNick}
    `; + // Safer text insertion without re-parsing HTML + userInfoElement.appendChild(document.createTextNode(`用户名:${userId}`)); + userInfoElement.appendChild(document.createElement('br')); + userInfoElement.appendChild(document.createTextNode(`昵称:${userNick}`)); + userInfoElement.appendChild(document.createElement('br')); if (UtilityEnabled("Rating")) { - userInfoElement.innerHTML += `评分:${userInfo?.Rating || 'N/A'}
    `; + userInfoElement.appendChild(document.createTextNode(`评分:${userInfo?.Rating || 'N/A'}`)); + userInfoElement.appendChild(document.createElement('br')); } // Last online time (async) @@ -217,10 +221,18 @@ async function createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUs const rightDiv = document.createElement("div"); rightDiv.className = "col-md-7"; row.appendChild(rightDiv); - rightDiv.innerHTML = "
    已解决题目
    "; + // Heading + const heading = document.createElement('h5'); + heading.appendChild(document.createTextNode('已解决题目')); + rightDiv.appendChild(heading); for (const problemId of acProblems) { - rightDiv.innerHTML += `${problemId} `; + const link = document.createElement('a'); + link.href = `https://www.xmoj.tech/problem.php?id=${problemId}`; + link.target = '_blank'; + link.appendChild(document.createTextNode(String(problemId))); + rightDiv.appendChild(link); + rightDiv.appendChild(document.createTextNode(' ')); } // Replace page content diff --git a/src/utils/user.js b/src/utils/user.js index 997d4865..451ae493 100644 --- a/src/utils/user.js +++ b/src/utils/user.js @@ -123,7 +123,7 @@ export let GetUsernameHTML = async (Element, Username, Simple = false, Href = "h let HTMLData = ""; if (!Simple) { HTMLData += ``; + HTMLData += `">`; if (!Simple) { if (AdminUserList.includes(Username)) { HTMLData += `脚本管理员`; } let BadgeInfo = await GetUserBadge(Username); - if (BadgeInfo.Content != "") { + if (BadgeInfo.Content !== "") { HTMLData += `${BadgeInfo.Content}`; } } From d76a4efb318a2505b043e746691230e194e575de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 12:16:49 +0000 Subject: [PATCH 20/26] 2.5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba12fbcc..a8ccbe08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "2.5.2", + "version": "2.5.3", "description": "an improvement script for xmoj.tech", "type": "module", "main": "dist/XMOJ.user.js", From 1c2b7a6c0b62ec4b7ad72eb33282dce699a27159 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 12:16:54 +0000 Subject: [PATCH 21/26] Update version info to 2.5.3 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 043b5b37..323ef4e6 100644 --- a/Update.json +++ b/Update.json @@ -3214,6 +3214,17 @@ } ], "Notes": "No release notes were provided for this release." + }, + "2.5.3": { + "UpdateDate": 1765369009839, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 882, + "Description": "refactor userscript structure " + } + ], + "Notes": "No release notes were provided for this release." } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 70d825f4..7a2a8e1d 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 2.5.2 +// @version 2.5.3 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From 815ccb46b14f0a3fb917dfa90cf248632cfb73fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Dec 2025 13:46:50 +0000 Subject: [PATCH 22/26] Update time and description of 2.5.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 323ef4e6..a6546ac8 100644 --- a/Update.json +++ b/Update.json @@ -3216,7 +3216,7 @@ "Notes": "No release notes were provided for this release." }, "2.5.3": { - "UpdateDate": 1765369009839, + "UpdateDate": 1765633604936, "Prerelease": true, "UpdateContents": [ { From 12973ef97f207aad979752574aafe679ed034b34 Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Sat, 13 Dec 2025 21:51:13 +0800 Subject: [PATCH 23/26] Fix Copilot review issues: use strict equality, remove unused imports and variables --- src/main.js | 2 +- src/pages/contest.js | 2 -- src/pages/submit.js | 1 - src/utils/user.js | 2 +- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main.js b/src/main.js index be3fc11a..0edf848f 100644 --- a/src/main.js +++ b/src/main.js @@ -20,7 +20,7 @@ import { TidyTable } from './utils/table.js'; import { GetUserInfo, GetUserBadge, GetUsernameHTML } from './utils/user.js'; // Core application imports -import { initTheme, NavbarStyler, replaceMarkdownImages, main } from './core/bootstrap.js'; +import { initTheme, main } from './core/bootstrap.js'; import { registerMenuCommands } from './core/menu.js'; // Feature modules imports diff --git a/src/pages/contest.js b/src/pages/contest.js index 8c3ae56b..7c5185c6 100644 --- a/src/pages/contest.js +++ b/src/pages/contest.js @@ -3,8 +3,6 @@ * Handles all styling and functionality for /contest.php */ -import { UtilityEnabled } from '../core/config.js'; - /** * Initialize contest page * @param {Object} context - Page context with utilities diff --git a/src/pages/submit.js b/src/pages/submit.js index f7c11abd..1a53005e 100644 --- a/src/pages/submit.js +++ b/src/pages/submit.js @@ -17,7 +17,6 @@ export async function init(context) { if (problemId) { document.title = `提交代码: 题目${Number(problemId)}`; } else if (contestId) { - const problemLetter = String.fromCharCode(65 + parseInt(SearchParams.get("pid"))); document.title = `提交代码: 比赛${Number(contestId)}`; } diff --git a/src/utils/user.js b/src/utils/user.js index 451ae493..4418d367 100644 --- a/src/utils/user.js +++ b/src/utils/user.js @@ -32,7 +32,7 @@ export 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); - if (Email == "") { + if (Email === "") { EmailHash = undefined; } else { localStorage.setItem("UserScript-User-" + Username + "-EmailHash", EmailHash); From f1b09a238ce103f1a222276aa05c3dcd48accbc3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Dec 2025 13:51:52 +0000 Subject: [PATCH 24/26] Update time and description of 2.5.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index a6546ac8..bdd1037c 100644 --- a/Update.json +++ b/Update.json @@ -3216,7 +3216,7 @@ "Notes": "No release notes were provided for this release." }, "2.5.3": { - "UpdateDate": 1765633604936, + "UpdateDate": 1765633907152, "Prerelease": true, "UpdateContents": [ { From ca3c754da1c65f6484bee1c1447eb9848a9a5e43 Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Sat, 13 Dec 2025 21:56:59 +0800 Subject: [PATCH 25/26] Fix: use textContent instead of innerHTML in contest rank cells to avoid HTML reinterpretation --- src/pages/contestrank.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/contestrank.js b/src/pages/contestrank.js index f4d8fe01..3421b76e 100644 --- a/src/pages/contestrank.js +++ b/src/pages/contestrank.js @@ -206,7 +206,8 @@ function styleProblemCell(cell) { } } - cell.innerHTML = innerText; + // Set cell text safely without re-parsing HTML + cell.textContent = innerText; cell.style.backgroundColor = backgroundColor; cell.style.color = UtilityEnabled("DarkMode") ? "white" : "black"; } From 841badc4c007cdcfb29f096baabad9f6047ab861 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 Dec 2025 00:51:53 +0000 Subject: [PATCH 26/26] Update time and description of 2.5.3 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index bdd1037c..fd2fe77e 100644 --- a/Update.json +++ b/Update.json @@ -3216,7 +3216,7 @@ "Notes": "No release notes were provided for this release." }, "2.5.3": { - "UpdateDate": 1765633907152, + "UpdateDate": 1766796708973, "Prerelease": true, "UpdateContents": [ {