From 68fd2da36b6c36938b20205ba5dfd835adeb6d1e Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Thu, 19 Mar 2026 22:11:33 +0800 Subject: [PATCH 01/22] Fully written except turnstile --- .github/workflows/code-check.yml | 30 + .github/workflows/release-build.yml | 78 + .gitignore | 1 + README.md | 73 +- XMOJ.user.js | 6128 +++++++++++++++++++++++++++ eslint.config.js | 23 + package.json | 59 + src/main.js | 622 +++ src/preload.js | 796 ++++ src/settings.html | 202 + src/settings.js | 50 + src/storage.js | 99 + src/updater.js | 79 + 13 files changed, 8238 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/code-check.yml create mode 100644 .github/workflows/release-build.yml create mode 100644 .gitignore create mode 100644 XMOJ.user.js create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 src/main.js create mode 100644 src/preload.js create mode 100644 src/settings.html create mode 100644 src/settings.js create mode 100644 src/storage.js create mode 100644 src/updater.js diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml new file mode 100644 index 0000000..4d45191 --- /dev/null +++ b/.github/workflows/code-check.yml @@ -0,0 +1,30 @@ +name: Code Check + +on: + push: + branches: + - "**" + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run lint and syntax check + run: npm run check diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 0000000..6728175 --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,78 @@ +name: Release Build + +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.name }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: Windows + os: windows-latest + script: npm run pack:win + - name: macOS + os: macos-latest + script: npm run pack:mac + - name: Linux + os: ubuntu-latest + script: npm run pack:linux + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libarchive-tools rpm + + - name: Install dependencies + run: npm ci + + - name: Run code check + run: npm run check + + - name: Build package + run: ${{ matrix.script }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ELXMOJ-${{ matrix.name }} + path: dist/** + if-no-files-found: error + + publish: + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + path: release-assets + merge-multiple: true + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: release-assets/** + generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index aaa65f7..d3da6a9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ -# Electro-XMOJ -XMOJ exported to electron! +# ELXMOJ (Electron) + +在 Electron 中访问 `https://www.xmoj.tech`,自动加载 `XMOJ.user.js`,并提供启动自检、脚本更新与设置持久化。 + +## 功能 + +- 启动后打开 `www.xmoj.tech` +- 自动注入仓库内 `XMOJ.user.js`(首次运行复制到用户数据目录) +- 每次启动可检查脚本更新 +- 正式版更新源:`https://xmoj-bbs.me/XMOJ.user.js` +- 预览版更新源:`https://dev.xmoj-bbs.me/XMOJ.user.js` +- 发现新版本时弹窗提示用户是否更新 +- 设置持久化(通道、启动检查、自动注入) +- 提供启动自检和手动自检 + +## 运行 + +```bash +npm install +npm start +``` + +## 自检模式 + +```bash +npm run self-check +``` + +## 代码检查 + +```bash +npm run check +``` + +包含: + +- `eslint` 静态检查(`src`) +- Node 语法检查(`node --check`) + +## 全平台打包 + +```bash +npm run pack:win +npm run pack:mac +npm run pack:linux +``` + +打包产物默认输出到 `dist/`。 + +## GitHub Actions + +- 代码检查工作流:`.github/workflows/code-check.yml` + - 在 `push` / `pull_request` 触发 + - 执行 `npm ci` + `npm run check` +- 发布构建工作流:`.github/workflows/release-build.yml` + - 在手动触发或 `v*` tag 触发 + - Windows/macOS/Linux 矩阵并行打包 + - `v*` tag 时自动创建 GitHub Release 并上传产物 + +应用菜单 `ELXMOJ` 中也可以执行: + +- 设置 +- 执行自检 +- 检查脚本更新 + +## 持久化位置 + +应用会把运行数据写到 Electron 的 `userData` 目录,包括: + +- `settings.json` +- `XMOJ.user.js`(托管脚本文件) diff --git a/XMOJ.user.js b/XMOJ.user.js new file mode 100644 index 0000000..7d2b366 --- /dev/null +++ b/XMOJ.user.js @@ -0,0 +1,6128 @@ +// ==UserScript== +// @name XMOJ +// @version 3.3.5 +// @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== + +/** + * 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 CaptchaSiteKey = "0x4AAAAAAALBT58IhyDViNmv"; +const AdminUserList = ["zhuchenrui2", "shanwenxiao", "chenlangning", "admin"]; + +let escapeHTML = (str) => { + return str.replace(/[&<>"']/g, function (match) { + const escape = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return escape[match]; + }); +}; + +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."); + } + } +} +let SmartAlert = (Message) => { + if (localStorage.getItem("UserScript-Alert") !== Message) { + alert(Message); + } + localStorage.setItem("UserScript-Alert", Message); +} +/** + * 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."); + } + } +}; + +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 +} + +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 (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); + } +}; +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. + */ +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. + */ +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."); + } + } +}; +/** + * 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 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 = (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."); + } + } +}; +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."); + } + } +}; +/** + * 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 = (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."); + } + } +}; +/** + * 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."); + } + } +}; +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")) { + 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."); + } + } +}; +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); + } + } +}; +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; +}; +let clearCredential = async () => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + await navigator.credentials.preventSilentAccess(); + } catch (e) { + console.error(e); + } + } +}; +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); + }); + } + 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."); + } + } +}; + +unsafeWindow.GetContestProblemList = async function(RefreshList) { + try { + const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + SearchParams.get("cid")); + const res = await contestReq.text(); + if (contestReq.status === 200 && res.indexOf("比赛尚未开始或私有,不能查看题目。") === -1) { + const parser = new DOMParser(); + const dom = parser.parseFromString(res, "text/html"); + const rows = (dom.querySelector("#problemset > tbody")).rows; + let problemList = []; + for (let i = 0; i < rows.length; i++) { + problemList.push({ + "title": rows[i].children[2].innerText, + "url": rows[i].children[2].children[0].href + }); + } + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList", JSON.stringify(problemList)); + if (RefreshList) location.reload(); + } + } catch (e) { + console.error(e); + } +} + +// WebSocket Notification System +let NotificationSocket = null; +let NotificationSocketReconnectAttempts = 0; +let NotificationSocketReconnectDelay = 1000; +let NotificationSocketPingInterval = null; +let NotificationSocketReconnectTimer = null; + +function GetPHPSESSID() { + 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]; + break; + } + } + return Session; +} + +function ConnectNotificationSocket() { + try { + // Clear any pending reconnection timer to prevent duplicate connections + if (NotificationSocketReconnectTimer) { + clearTimeout(NotificationSocketReconnectTimer); + NotificationSocketReconnectTimer = null; + } + + let Session = GetPHPSESSID(); + if (Session === "") { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: PHPSESSID not available, skipping connection"); + } + return; + } + + let wsUrl = (UtilityEnabled("SuperDebug") ? "ws://127.0.0.1:8787" : "wss://api.xmoj-bbs.me") + "/ws/notifications?SessionID=" + Session; + + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connecting to", wsUrl); + } + + NotificationSocket = new WebSocket(wsUrl); + + NotificationSocket.onopen = () => { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connected successfully"); + } + NotificationSocketReconnectAttempts = 0; + NotificationSocketReconnectDelay = 1000; + + // Start ping keepalive + if (NotificationSocketPingInterval) { + clearInterval(NotificationSocketPingInterval); + } + NotificationSocketPingInterval = setInterval(() => { + if (NotificationSocket && NotificationSocket.readyState === WebSocket.OPEN) { + NotificationSocket.send(JSON.stringify({ type: 'ping' })); + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Sent ping"); + } + } else { + clearInterval(NotificationSocketPingInterval); + } + }, 30000); + }; + + NotificationSocket.onmessage = (event) => { + HandleNotificationMessage(event); + }; + + NotificationSocket.onerror = (error) => { + if (UtilityEnabled("DebugMode")) { + console.error("WebSocket: Error", error); + } + }; + + NotificationSocket.onclose = (event) => { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Connection closed", event.code, event.reason); + } + if (NotificationSocketPingInterval) { + clearInterval(NotificationSocketPingInterval); + } + ReconnectNotificationSocket(); + }; + } catch (e) { + console.error("WebSocket: Failed to connect", e); + ReconnectNotificationSocket(); + } +} + +function ReconnectNotificationSocket() { + const delay = Math.min(NotificationSocketReconnectDelay * Math.pow(2, NotificationSocketReconnectAttempts), 30000); + NotificationSocketReconnectAttempts++; + + if (UtilityEnabled("DebugMode")) { + console.log(`WebSocket: Reconnecting in ${delay}ms (attempt ${NotificationSocketReconnectAttempts})`); + } + + NotificationSocketReconnectTimer = setTimeout(() => { + ConnectNotificationSocket(); + }, delay); +} + +function HandleNotificationMessage(event) { + try { + const notification = JSON.parse(event.data); + + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Received message", notification); + } + + if (notification.type === 'connected') { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Server confirmed connection at timestamp", notification.timestamp); + } + } else if (notification.type === 'bbs_mention') { + if (UtilityEnabled("BBSPopup")) { + CreateAndShowBBSMentionToast(notification.data); + } + } else if (notification.type === 'mail_mention') { + if (UtilityEnabled("MessagePopup")) { + CreateAndShowMailMentionToast(notification.data); + } + } else if (notification.type === 'pong') { + if (UtilityEnabled("DebugMode")) { + console.log("WebSocket: Received pong"); + } + } + } catch (e) { + console.error("WebSocket: Failed to handle message", e); + } +} + +function CreateAndShowBBSMentionToast(mention) { + let ToastContainer = document.querySelector(".toast-container"); + if (!ToastContainer) return; + + 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(mention.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 = "讨论" + escapeHTML(mention.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(mention.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=" + mention.PostID + '&page=' + mention.PageNumber, "_blank"); + RequestAPI("ReadBBSMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); +} + +function CreateAndShowMailMentionToast(mention) { + let ToastContainer = document.querySelector(".toast-container"); + if (!ToastContainer) return; + + 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(mention.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"); + let ToastUser = document.createElement("span"); + GetUsernameHTML(ToastUser, mention.FromUserID); + ToastBody.appendChild(ToastUser); + ToastBody.appendChild(document.createTextNode(" 给你发了一封短消息")); + 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.setAttribute("data-bs-dismiss", "toast"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadMailMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + 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=" + mention.FromUserID, "_blank"); + RequestAPI("ReadMailMention", { + "MentionID": Number(mention.MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); +} + +function PollNotifications() { + // Clear toast container once before fetching to prevent race condition + if (UtilityEnabled("BBSPopup") || UtilityEnabled("MessagePopup")) { + let ToastContainer = document.querySelector(".toast-container"); + if (ToastContainer) { + ToastContainer.innerHTML = ""; + } + } + if (UtilityEnabled("BBSPopup")) { + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + CreateAndShowBBSMentionToast(MentionList[i]); + } + } + }); + } + if (UtilityEnabled("MessagePopup")) { + RequestAPI("GetMailMentionList", {}, (Response) => { + if (Response.Success) { + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + CreateAndShowMailMentionToast(MentionList[i]); + } + } + }); + } +} + +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(); + } +}); + +//otherwise CurrentUsername might be undefined +if (UtilityEnabled("AutoLogin") && document.querySelector("body > a:nth-child(1)") != null && document.querySelector("body > a:nth-child(1)").innerText == "请登录后继续操作") { + localStorage.setItem("UserScript-LastPage", location.pathname + location.search); + location.href = "https://www.xmoj.tech/loginpage.php"; +} + +let SearchParams = new URLSearchParams(location.search); +let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me") +if (document.querySelector("#profile") === null) { + location.href = "https://www.xmoj.tech/loginpage.php"; +} +let CurrentUsername = document.querySelector("#profile").innerText; +CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); +let IsAdmin = AdminUserList.indexOf(CurrentUsername) !== -1; + +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); + } +}; +initTheme(); + + +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'); + if (UtilityEnabled("MonochromeUI")) { + Object.assign(n.style, { + position: 'fixed', + borderRadius: '0', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', + margin: '0', + maxWidth: '100%', + backgroundColor: 'rgba(255, 255, 255, 0)', + opacity: '0.75', + zIndex: '1000' + }); + } else { + 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 = UtilityEnabled("MonochromeUI") ? ` + #blur-overlay { + display: none !important; + } + ` : ` + #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."); + } + } + } +} + +function replaceMarkdownImages(text, string) { + return text.replace(/!\[.*?\]\(.*?\)/g, string); +} + +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; + } + 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"); + } + if (UtilityEnabled("MonochromeUI")) { + let fontLink = document.createElement("link"); + fontLink.rel = "stylesheet"; + fontLink.href = "https://fonts.loli.net/css2?family=Playfair+Display:wght@400;700&family=Source+Serif+4:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"; + document.head.appendChild(fontLink); + let earlyStyle = document.createElement("style"); + earlyStyle.textContent = ` + :root { + --mono-black: #000; --mono-white: #fff; + --mono-gray-100: #f5f5f5; --mono-gray-300: #d4d4d4; + --mono-font-heading: 'Playfair Display', Georgia, serif; + --mono-font-body: 'Source Serif 4', 'Source Serif Pro', Georgia, serif; + } + [data-bs-theme='dark'] { + --mono-black: #e5e5e5; --mono-white: #1a1a1a; + --mono-gray-100: #222; --mono-gray-300: #404040; + } + * { border-radius: 0 !important; box-shadow: none !important; } + body { font-family: var(--mono-font-body) !important; background-color: var(--mono-white) !important; color: var(--mono-black) !important; } + h1,h2,h3,h4,h5,h6 { font-family: var(--mono-font-heading) !important; } + .table thead th { background-color: var(--mono-black) !important; color: var(--mono-white) !important; } + .card { border: 2px solid var(--mono-black) !important; } + .card-header { background-color: var(--mono-black) !important; color: var(--mono-white) !important; } + `; + document.head.appendChild(earlyStyle); + } + 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); + if (UtilityEnabled("MonochromeUI")) { + Style.innerHTML = ` + /* Fonts loaded via to avoid layout shift */ + + :root { + --mono-black: #000; + --mono-white: #fff; + --mono-gray-100: #f5f5f5; + --mono-gray-200: #e5e5e5; + --mono-gray-300: #d4d4d4; + --mono-gray-400: #a3a3a3; + --mono-gray-500: #737373; + --mono-border: 2px solid var(--mono-black); + --mono-border-thin: 1px solid var(--mono-gray-300); + --mono-font-heading: 'Playfair Display', Georgia, serif; + --mono-font-body: 'Source Serif 4', 'Source Serif Pro', Georgia, serif; + --mono-font-mono: 'JetBrains Mono', 'Consolas', monospace; + --mono-transition: 100ms ease; + } + + [data-bs-theme='dark'] { + --mono-black: #e5e5e5; + --mono-white: #1a1a1a; + --mono-gray-100: #222; + --mono-gray-200: #2a2a2a; + --mono-gray-300: #404040; + --mono-gray-400: #737373; + --mono-gray-500: #a3a3a3; + } + + * { + border-radius: 0 !important; + box-shadow: none !important; + } + + body { + font-family: var(--mono-font-body) !important; + color: var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + + h1, h2, h3, h4, h5, h6 { + font-family: var(--mono-font-heading) !important; + font-weight: 700 !important; + } + + code, pre, .CodeMirror, kbd, samp { + font-family: var(--mono-font-mono) !important; + } + + a { + color: var(--mono-black) !important; + text-decoration: none !important; + transition: var(--mono-transition) !important; + } + .container a:not(.nav-link):not(.btn):not(.dropdown-item):not(.list-group-item):not(.page-link) { + border-bottom: 1px solid var(--mono-gray-400) !important; + padding-bottom: 1px !important; + } + .container a:not(.nav-link):not(.btn):not(.dropdown-item):not(.list-group-item):not(.page-link):hover { + border-bottom-color: var(--mono-black) !important; + } + + blockquote { + border-left: 4px solid var(--mono-black) !important; + padding: 0.5em 1em; + } + + /* Navbar */ + .navbar, nav.navbar { + border-bottom: 4px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + opacity: 1 !important; + } + .navbar .nav-link { + color: var(--mono-black) !important; + text-decoration: none !important; + font-family: var(--mono-font-body) !important; + text-transform: uppercase !important; + letter-spacing: 0.05em !important; + font-size: 0.85rem !important; + } + .navbar .nav-link:hover, .navbar .nav-link.active { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + + /* Buttons */ + .btn { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + text-transform: uppercase !important; + letter-spacing: 0.1em !important; + font-family: var(--mono-font-body) !important; + font-weight: 600 !important; + transition: var(--mono-transition) !important; + } + .btn:hover, .btn:focus, .btn:active, .btn.active { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + border-color: var(--mono-black) !important; + } + .btn-primary { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + .btn-primary:hover { + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .btn-secondary { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .btn-secondary:hover { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + .btn-success { + background-color: var(--mono-white) !important; + border: 2px solid #52c41a !important; + color: #52c41a !important; + } + .btn-danger { + background-color: var(--mono-white) !important; + border: 2px solid #fe4c61 !important; + color: #fe4c61 !important; + } + .btn-warning { + background-color: var(--mono-white) !important; + border: 2px solid #ffa900 !important; + color: #ffa900 !important; + } + .btn-info { + background-color: var(--mono-white) !important; + border: 2px solid #0dcaf0 !important; + color: #0dcaf0 !important; + } + + /* Cards */ + .card { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + .card-header { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + border-bottom: none !important; + font-family: var(--mono-font-heading) !important; + } + .card-header * { + color: var(--mono-white) !important; + } + .card-body { + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .card-footer { + border-top: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + } + + /* Modals */ + .modal-content { + border: 4px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + .modal-header { + border-bottom: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + } + .modal-footer { + border-top: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + } + .modal-title { + font-family: var(--mono-font-heading) !important; + } + + /* Toasts */ + .toast { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + } + .toast-header { + background-color: var(--mono-gray-100) !important; + color: var(--mono-black) !important; + border-bottom: 1px solid var(--mono-gray-300) !important; + } + + /* Tables */ + .table { + border-color: var(--mono-gray-300) !important; + } + thead th, th.header, th.headerSortUp, th.headerSortDown { + background-color: var(--mono-black) !important; + background-image: none !important; + color: var(--mono-white) !important; + border-bottom: none !important; + font-family: var(--mono-font-heading) !important; + text-transform: uppercase !important; + letter-spacing: 0.05em !important; + font-size: 0.85rem !important; + } + td, th { + border-color: var(--mono-gray-300) !important; + text-align: center !important; + } + .table-striped > tbody > tr:nth-of-type(odd) > * { + background-color: var(--mono-gray-100) !important; + } + table { + margin-top: 16px !important; + } + + /* List groups */ + .list-group-item { + border: none !important; + border-bottom: 1px solid var(--mono-gray-300) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .list-group-item-success { + border-left: 4px solid #52c41a !important; + } + .list-group-item-warning { + border-left: 4px solid #ffa900 !important; + } + .list-group-item-danger { + border-left: 4px solid #fe4c61 !important; + } + + /* Dropdowns */ + .dropdown-menu { + border: 2px solid var(--mono-black) !important; + padding: 0 !important; + background-color: var(--mono-white) !important; + } + .dropdown-item { + border-bottom: 1px solid var(--mono-gray-200) !important; + color: var(--mono-black) !important; + transition: var(--mono-transition) !important; + text-decoration: none !important; + } + .dropdown-item:last-child { + border-bottom: none !important; + } + .dropdown-item:hover, .dropdown-item:focus { + background-color: var(--mono-black) !important; + color: var(--mono-white) !important; + } + + /* Forms */ + .form-control, .form-select { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + font-family: var(--mono-font-body) !important; + } + .form-control:focus, .form-select:focus { + outline: 2px solid var(--mono-black) !important; + outline-offset: 2px !important; + border-color: var(--mono-black) !important; + } + + /* Alerts */ + .alert { + border: 2px solid var(--mono-black) !important; + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + .alert-primary { + border-left: 8px solid var(--mono-black) !important; + } + + /* Status indicators */ + .status_y { + background-color: #52c41a !important; + color: #fff !important; + border-color: #52c41a !important; + } + .status_n { + background-color: #fe4c61 !important; + color: #fff !important; + border-color: #fe4c61 !important; + } + .status_w { + background-color: #ffa900 !important; + color: #fff !important; + border-color: #ffa900 !important; + } + + .test-case:hover { + border: 2px solid var(--mono-black) !important; + } + + .software_list { + width: unset !important; + } + .software_item { + margin: 5px 10px !important; + background-color: var(--mono-gray-100) !important; + border: 1px solid var(--mono-gray-300) !important; + } + .software_item img { + width: 50px !important; + height: 50px !important; + object-fit: contain !important; + } + .item-txt { + color: var(--mono-black) !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(--mono-black); + color: var(--mono-white); + width: 100%; + font-family: var(--mono-font-heading); + } + .cnt-row-head * { + color: var(--mono-white) !important; + } + .cnt-row-body { + padding: 1em; + border: 2px solid var(--mono-black); + border-top: none; + } + + /* Scrollbar */ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: var(--mono-white); + } + ::-webkit-scrollbar-thumb { + background: var(--mono-black); + } + + /* Copy button in inverted headers */ + .cnt-row-head .copy-btn, .card-header .copy-btn { + border-color: var(--mono-white) !important; + color: var(--mono-white) !important; + background-color: transparent !important; + } + .cnt-row-head .copy-btn:hover, .card-header .copy-btn:hover { + background-color: var(--mono-white) !important; + color: var(--mono-black) !important; + } + + /* Problem switcher responsive */ + @media (max-width: 768px) { + .problem-switcher-container { + display: none !important; + } + } + .refreshList { + cursor: pointer; + } + + /* Contain images */ + img { + max-width: 100% !important; + height: auto !important; + } + + /* Hide blur overlay */ + #blur-overlay { display: none !important; }`; + } else { + 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; + } + .refreshList { + cursor: pointer; + color: #6c757d; + text-decoration: none; + }`; + } + if (UtilityEnabled("AddAnimation")) { + Style.innerHTML += `.status, .test-case { + transition: ${UtilityEnabled("MonochromeUI") ? "100ms ease" : "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")) { + 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 = "⏳"; + } + } + + 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 = UtilityEnabled("MonochromeUI") ? 'transform 100ms ease, opacity 100ms ease' : '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 * (UtilityEnabled("MonochromeUI") ? 20 : 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'; + }, UtilityEnabled("MonochromeUI") ? 80 : 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); + 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 + ") " + escapeHTML(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(); + } + }); + RequestAPI("GetAddOnScript", {}, (Response) => { + if (Response.Success) { + eval(Response.Data["Script"]); + } else { + console.warn("Fetch AddOnScript failed: " + Response.Message); + } + }); + let ToastContainer = document.createElement("div"); + ToastContainer.classList.add("toast-container", "position-fixed", "bottom-0", "end-0", "p-3"); + document.body.appendChild(ToastContainer); + // Initialize WebSocket notification system + if (CurrentUsername && (UtilityEnabled("BBSPopup") || UtilityEnabled("MessagePopup"))) { + ConnectNotificationSocket(); + } + + // Fallback polling when WebSocket is not connected + addEventListener("focus", () => { + if (!NotificationSocket || NotificationSocket.readyState !== WebSocket.OPEN) { + PollNotifications(); + } + }); + + // Periodic fallback polling every 60 seconds when WebSocket is down + setInterval(() => { + if (!NotificationSocket || NotificationSocket.readyState !== WebSocket.OPEN) { + PollNotifications(); + } + }, 60000); + + // Handle tab visibility changes - reconnect if connection dropped + document.addEventListener('visibilitychange', () => { + if (!document.hidden && NotificationSocket && + NotificationSocket.readyState !== WebSocket.OPEN && + NotificationSocket.readyState !== WebSocket.CONNECTING) { + ConnectNotificationSocket(); + } + }); + 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": "MonochromeUI", "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": "ImageEnlarger", "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") { + let transZhEn = document.getElementById("lang_cn_to_en"); + let transEnZh = document.getElementById("lang_en_to_cn"); + if (transZhEn !== null) transZhEn.remove(); + if (transEnZh !== null) transEnZh.remove(); + + await RenderMathJax(); + if (SearchParams.get("cid") != null && UtilityEnabled("ProblemSwitcher")) { + let pid = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + if (!pid) { + 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; + for (let i = 0; i < rows.length; i++) { + let problemIdText = rows[i].children[1].innerText; // Get the text content + let match = problemIdText.match(/\d+/); // Extract the number + if (match) { + let extractedPid = match[0]; + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + i + "-PID", extractedPid); + } + } + pid = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + } + } + if (pid) { + document.getElementsByTagName("h2")[0].innerHTML += " (" + pid + ")"; + } + let ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList"); + if (ContestProblemList == null) { + await unsafeWindow.GetContestProblemList(false); + ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList"); + } + + let problemSwitcher = document.createElement("div"); + problemSwitcher.classList.add("problem-switcher-container"); + 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 = UtilityEnabled("MonochromeUI") ? "#000" : "rgba(0, 0, 0, 0.8)"; + } else { + problemSwitcher.style.backgroundColor = UtilityEnabled("MonochromeUI") ? "#FFF" : "rgba(255, 255, 255, 0.8)"; + } + problemSwitcher.style.padding = "10px"; + problemSwitcher.style.borderRadius = UtilityEnabled("MonochromeUI") ? "0" : "0 10px 10px 0"; + if (UtilityEnabled("MonochromeUI")) problemSwitcher.style.borderRight = "4px solid"; + problemSwitcher.style.display = "flex"; + problemSwitcher.style.flexDirection = "column"; + + let problemList = JSON.parse(ContestProblemList); + problemSwitcher.innerHTML += `刷新`; + 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"; + } + //修复提交按钮 + const links = document.querySelectorAll('.mt-3 > center:nth-child(1) > a'); + const SubmitLink = Array.from(links).find(a => a.textContent.trim() === '提交'); + 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; + document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); + 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); + }; + var 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")) { + var checkNum = function(str) { + var patrn = /^[0-9]{1,20}$/; + var ans = true; + if (!patrn.exec(str)) ans = false; + return ans; + } + + const params = new URL(location.href).searchParams; + let CurrentProblemId = checkNum(params.get("problem_id")) ? Number(params.get("problem_id")) : ""; + let CurrentLanguageParam = params.get("language"); + let CurrentLanguage = checkNum(CurrentLanguageParam) && -1 <= CurrentLanguageParam && CurrentLanguageParam <= 2 ? Number(CurrentLanguageParam) : "-1"; + let CurrentJresultParam = params.get("jresult"); + let CurrentJresult = checkNum(CurrentJresultParam) && -1 <= CurrentJresultParam && CurrentJresultParam <= 11 ? Number(CurrentJresultParam) : "-1"; + + document.querySelector("#simform").outerHTML = `
+ +
+ + +
+
+ + +
+ + +
+
+ +
`; + + var selectElement = document.getElementById('problem_id'); + selectElement.value = CurrentProblemId; + selectElement = document.getElementById('language'); + selectElement.value = CurrentLanguage; + selectElement = document.getElementById('jresult'); + selectElement.value = CurrentJresult; + } + + 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 = []; + 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 = null; + let StdListReady = 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 += UtilityEnabled("MonochromeUI") ? "" : ""; + 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(async (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 += UtilityEnabled("MonochromeUI") ? "" : ""; + } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { + await StdListReady; + if (!StdList) { /* skip upload if list fetch failed */ } + else { + if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; + let Std = StdList.find((Element) => { + return Element == Number(PID); + }); + if (Std != undefined) { + TempHTML += UtilityEnabled("MonochromeUI") ? "[STD]" : "✅"; + } else { + RequestAPI("UploadStd", { + "ProblemID": Number(PID), + }, (Result) => { + if (Result.Success) { + CurrentRow.cells[2].innerHTML += UtilityEnabled("MonochromeUI") ? "[OK]" : "🆗"; + } else { + CurrentRow.cells[2].innerHTML += UtilityEnabled("MonochromeUI") ? "[ERR]" : "⚠️"; + } + }); + } + } + } + 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排名)"; + let HeaderCells = document.querySelectorAll("#rank > thead > tr > *"); + HeaderCells[0].innerText = "排名"; + HeaderCells[1].innerText = "用户"; + HeaderCells[2].innerText = "昵称"; + HeaderCells[3].innerText = "AC数"; + HeaderCells[4].innerText = "得分"; + if (UtilityEnabled("MonochromeUI")) { + for (let j = 0; j < HeaderCells.length; j++) { + HeaderCells[j].removeAttribute("bgcolor"); + HeaderCells[j].style.setProperty("background-color", "black", "important"); + HeaderCells[j].style.setProperty("color", "white", "important"); + let Links = HeaderCells[j].querySelectorAll("a"); + for (let k = 0; k < Links.length; k++) { + Links[k].style.setProperty("color", "white", "important"); + } + } + } + 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++) { + if (UtilityEnabled("MonochromeUI")) { + Temp[i].style.backgroundColor = ""; + } + 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; + if (UtilityEnabled("MonochromeUI")) { + for (let j = 0; j < 5 && j < Temp[i].cells.length; j++) { + Temp[i].cells[j].style.backgroundColor = ""; + Temp[i].cells[j].style.color = ""; + } + } + 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 = "rgba(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(); + } + let HeaderCells = document.querySelectorAll("#rank > thead > tr > *"); + HeaderCells[0].innerText = "排名"; + HeaderCells[1].innerText = "用户"; + HeaderCells[2].innerText = "昵称"; + HeaderCells[3].innerText = "AC数"; + HeaderCells[4].innerText = "得分"; + if (UtilityEnabled("MonochromeUI")) { + for (let j = 0; j < HeaderCells.length; j++) { + HeaderCells[j].removeAttribute("bgcolor"); + HeaderCells[j].style.setProperty("background-color", "black", "important"); + HeaderCells[j].style.setProperty("color", "white", "important"); + let Links = HeaderCells[j].querySelectorAll("a"); + for (let k = 0; k < Links.length; k++) { + Links[k].style.setProperty("color", "white", "important"); + } + } + } + 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++) { + if (UtilityEnabled("MonochromeUI")) { + Temp[i].style.backgroundColor = ""; + } + 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; + if (UtilityEnabled("MonochromeUI")) { + for (let j = 0; j < 5 && j < Temp[i].cells.length; j++) { + Temp[i].cells[j].style.backgroundColor = ""; + Temp[i].cells[j].style.color = ""; + } + } + 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; + Temp[i].cells[j].style.color = (UtilityEnabled("DarkMode") ? "white" : "black"); + } + } + 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 = UtilityEnabled("MonochromeUI") ? "2px solid var(--mono-black)" : "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 + ") " + escapeHTML(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 lineBreakPos = CurrentCode.search(/[\r\n]/); + if (lineBreakPos === -1) continue; + let headerLine = CurrentCode.slice(0, lineBreakPos); + let digitMatch = headerLine.match(/\d+/); + if (!digitMatch) continue; + let CurrentQuestionID = digitMatch[0]; + let bodyStart = lineBreakPos + 1; + if (CurrentCode[lineBreakPos] === '\r' && CurrentCode[lineBreakPos + 1] === '\n') { + bodyStart = lineBreakPos + 2; + } + CurrentCode = CurrentCode.slice(bodyStart); + 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 += "用户名:" + escapeHTML(UserID) + "
"; + UserInfoElement.innerHTML += "昵称:" + escapeHTML(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 += escapeHTML(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 = escapeHTML(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 = escapeHTML(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 += `最后被${escapeHTML(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); + } + } + } + } + } + } + + // Image Enlargement Feature + if (UtilityEnabled("ImageEnlarger")) { + try { + // Add CSS styles for the enlarger + let EnlargerStyle = document.createElement("style"); + EnlargerStyle.textContent = ` + .xmoj-image-preview { + cursor: pointer; + } + + .xmoj-image-preview:hover { + opacity: 0.8; + transition: opacity 0.2s ease; + } + + + .xmoj-image-modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + } + + .xmoj-image-modal.show { + display: flex; + flex-direction: column; + } + + .xmoj-image-modal-content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; + } + + .xmoj-image-modal-image { + max-width: 100%; + max-height: 100%; + object-fit: contain; + } + + .xmoj-image-modal-toolbar { + display: flex; + justify-content: center; + gap: 10px; + padding: 15px; + background-color: rgba(0, 0, 0, 0.5); + flex-wrap: wrap; + } + + .xmoj-image-modal-toolbar button { + padding: 8px 16px; + background-color: #0d6efd; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s ease; + } + + .xmoj-image-modal-toolbar button:hover { + background-color: #0b5ed7; + } + + .xmoj-image-modal-toolbar button:active { + background-color: #0a58ca; + } + + .xmoj-image-modal-close { + position: absolute; + top: 20px; + right: 30px; + color: white; + background: none; + border: none; + padding: 0; + line-height: 1; + font-size: 40px; + font-weight: bold; + cursor: pointer; + transition: color 0.2s ease; + z-index: 1; + } + + .xmoj-image-modal-close:hover { + color: #ccc; + } + + .xmoj-image-modal-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: white; + border: none; + padding: 20px 12px; + cursor: pointer; + font-size: 28px; + transition: background-color 0.2s ease; + user-select: none; + -webkit-user-select: none; + } + + .xmoj-image-modal-nav:hover { + background: rgba(0, 0, 0, 0.8); + } + + .xmoj-image-modal-nav:disabled { + opacity: 0.3; + cursor: default; + } + + .xmoj-image-modal-nav-prev { + left: 0; + border-radius: 0 4px 4px 0; + } + + .xmoj-image-modal-nav-next { + right: 0; + border-radius: 4px 0 0 4px; + } + `; + document.head.appendChild(EnlargerStyle); + + // Create modal element + let ImageModal = document.createElement("div"); + ImageModal.className = "xmoj-image-modal"; + ImageModal.id = "xmoj-image-modal"; + + let CloseButton = document.createElement("button"); + CloseButton.className = "xmoj-image-modal-close"; + CloseButton.type = "button"; + CloseButton.setAttribute("aria-label", "关闭图片"); + CloseButton.title = "关闭图片"; + CloseButton.innerHTML = "×"; + ImageModal.appendChild(CloseButton); + + let ModalContent = document.createElement("div"); + ModalContent.className = "xmoj-image-modal-content"; + + let PrevBtn = document.createElement("button"); + PrevBtn.className = "xmoj-image-modal-nav xmoj-image-modal-nav-prev"; + PrevBtn.type = "button"; + PrevBtn.setAttribute("aria-label", "上一张"); + PrevBtn.innerHTML = "❮"; + ModalContent.appendChild(PrevBtn); + + let NextBtn = document.createElement("button"); + NextBtn.className = "xmoj-image-modal-nav xmoj-image-modal-nav-next"; + NextBtn.type = "button"; + NextBtn.setAttribute("aria-label", "下一张"); + NextBtn.innerHTML = "❯"; + ModalContent.appendChild(NextBtn); + + let ModalImage = document.createElement("img"); + ModalImage.className = "xmoj-image-modal-image"; + ModalContent.appendChild(ModalImage); + ImageModal.appendChild(ModalContent); + + let Toolbar = document.createElement("div"); + Toolbar.className = "xmoj-image-modal-toolbar"; + + let ZoomInBtn = document.createElement("button"); + ZoomInBtn.innerHTML = "放大 (+)"; + ZoomInBtn.type = "button"; + Toolbar.appendChild(ZoomInBtn); + + let ZoomOutBtn = document.createElement("button"); + ZoomOutBtn.innerHTML = "缩小 (-)"; + ZoomOutBtn.type = "button"; + Toolbar.appendChild(ZoomOutBtn); + + let ResetZoomBtn = document.createElement("button"); + ResetZoomBtn.innerHTML = "重置大小"; + ResetZoomBtn.type = "button"; + Toolbar.appendChild(ResetZoomBtn); + + let SaveBtn = document.createElement("button"); + SaveBtn.innerHTML = "保存图片"; + SaveBtn.type = "button"; + Toolbar.appendChild(SaveBtn); + + ImageModal.appendChild(Toolbar); + document.body.appendChild(ImageModal); + + // Zoom level and navigation state + let CurrentZoom = 1; + const ZoomStep = 0.1; + const MinZoom = 0.1; + const MaxZoom = 5; + let ImageList = []; + let CurrentImageIndex = -1; + let PanX = 0; + let PanY = 0; + let IsDragging = false; + let DragStartX = 0; + let DragStartY = 0; + let DragStartPanX = 0; + let DragStartPanY = 0; + let IsTouchPanning = false; + let TouchStartX = 0; + let TouchStartY = 0; + let TouchPanStartPanX = 0; + let TouchPanStartPanY = 0; + + // Function to update image transform (zoom + pan) + let UpdateImageSize = () => { + ModalImage.style.transform = `translate(${PanX}px, ${PanY}px) scale(${CurrentZoom})`; + ModalImage.style.transition = IsDragging ? "none" : "transform 0.2s ease"; + let CursorStyle = CurrentZoom > 1 ? "grab" : ""; + ModalImage.style.cursor = CursorStyle; + ModalContent.style.cursor = CursorStyle; + }; + + // Function to update prev/next button state + let UpdateNavButtons = () => { + let HasMultiple = ImageList.length > 1; + PrevBtn.style.display = HasMultiple ? "" : "none"; + NextBtn.style.display = HasMultiple ? "" : "none"; + PrevBtn.disabled = CurrentImageIndex <= 0; + NextBtn.disabled = CurrentImageIndex >= ImageList.length - 1; + }; + + // Function to navigate to a specific image by index + let NavigateTo = (index) => { + if (index < 0 || index >= ImageList.length) return; + CurrentImageIndex = index; + CurrentZoom = 1; + PanX = 0; + PanY = 0; + ModalImage.src = ImageList[CurrentImageIndex]; + UpdateNavButtons(); + UpdateImageSize(); + }; + + // Function to open modal + let OpenImageModal = (imgElement) => { + let PreviewImages = [...document.querySelectorAll("img.xmoj-image-preview")]; + ImageList = PreviewImages.map(img => img.currentSrc || img.src).filter(src => src); + CurrentImageIndex = PreviewImages.indexOf(imgElement); + if (CurrentImageIndex === -1) { + ImageList = [(imgElement.currentSrc || imgElement.src)]; + CurrentImageIndex = 0; + } + CurrentZoom = 1; + PanX = 0; + PanY = 0; + ModalImage.src = ImageList[CurrentImageIndex]; + ImageModal.classList.add("show"); + UpdateNavButtons(); + UpdateImageSize(); + }; + + // Function to close modal + let CloseImageModal = () => { + ImageModal.classList.remove("show"); + }; + + // Close button click + CloseButton.addEventListener("click", CloseImageModal); + + // Close when clicking outside the image + ImageModal.addEventListener("click", (e) => { + if (e.target === ImageModal || e.target === ModalContent) { + CloseImageModal(); + } + }); + + // Keyboard shortcuts + document.addEventListener("keydown", (e) => { + if (ImageModal.classList.contains("show")) { + if (e.key === "Escape") { + CloseImageModal(); + } else if (e.key === "+") { + ZoomInBtn.click(); + } else if (e.key === "-") { + ZoomOutBtn.click(); + } else if (e.key === "ArrowLeft") { + NavigateTo(CurrentImageIndex - 1); + } else if (e.key === "ArrowRight") { + NavigateTo(CurrentImageIndex + 1); + } + } + }); + + // Touch events: pan when zoomed, swipe to navigate when at zoom level 1 + ModalContent.addEventListener("touchstart", (e) => { + if (e.touches.length !== 1) return; + TouchStartX = e.touches[0].clientX; + TouchStartY = e.touches[0].clientY; + if (CurrentZoom > 1) { + IsTouchPanning = true; + TouchPanStartPanX = PanX; + TouchPanStartPanY = PanY; + } else { + IsTouchPanning = false; + } + }, { passive: true }); + + ModalContent.addEventListener("touchmove", (e) => { + if (!IsTouchPanning || e.touches.length !== 1) return; + PanX = TouchPanStartPanX + (e.touches[0].clientX - TouchStartX); + PanY = TouchPanStartPanY + (e.touches[0].clientY - TouchStartY); + UpdateImageSize(); + e.preventDefault(); + }, { passive: false }); + + ModalContent.addEventListener("touchend", (e) => { + if (IsTouchPanning) { + IsTouchPanning = false; + return; + } + let TouchEndX = e.changedTouches[0].clientX; + let TouchEndY = e.changedTouches[0].clientY; + let DeltaX = TouchEndX - TouchStartX; + let DeltaY = TouchEndY - TouchStartY; + const SwipeThreshold = 50; + if (Math.abs(DeltaX) > SwipeThreshold && Math.abs(DeltaX) > Math.abs(DeltaY)) { + if (DeltaX < 0) { + NavigateTo(CurrentImageIndex + 1); + } else { + NavigateTo(CurrentImageIndex - 1); + } + } + }, { passive: true }); + + // Mouse drag to pan when zoomed + ModalContent.addEventListener("mousedown", (e) => { + if (CurrentZoom <= 1) return; + if (e.target.tagName.toUpperCase() === "BUTTON") return; + IsDragging = true; + DragStartX = e.clientX; + DragStartY = e.clientY; + DragStartPanX = PanX; + DragStartPanY = PanY; + ModalImage.style.cursor = "grabbing"; + ModalContent.style.cursor = "grabbing"; + e.preventDefault(); + }); + + document.addEventListener("mousemove", (e) => { + if (!IsDragging) return; + PanX = DragStartPanX + (e.clientX - DragStartX); + PanY = DragStartPanY + (e.clientY - DragStartY); + UpdateImageSize(); + }); + + document.addEventListener("mouseup", () => { + if (IsDragging) { + IsDragging = false; + let CursorStyle = CurrentZoom > 1 ? "grab" : ""; + ModalImage.style.cursor = CursorStyle; + ModalContent.style.cursor = CursorStyle; + } + }); + + // Mouse wheel to zoom in/out + ModalContent.addEventListener("wheel", (e) => { + e.preventDefault(); + let ZoomDelta = e.deltaY > 0 ? -ZoomStep : ZoomStep; + CurrentZoom = Math.max(MinZoom, Math.min(MaxZoom, CurrentZoom + ZoomDelta)); + UpdateImageSize(); + }, { passive: false }); + + // Navigation button clicks + PrevBtn.addEventListener("click", (e) => { + e.stopPropagation(); + NavigateTo(CurrentImageIndex - 1); + }); + + NextBtn.addEventListener("click", (e) => { + e.stopPropagation(); + NavigateTo(CurrentImageIndex + 1); + }); + + // Zoom controls + ZoomInBtn.addEventListener("click", () => { + CurrentZoom = Math.min(CurrentZoom + ZoomStep, MaxZoom); + UpdateImageSize(); + }); + + ZoomOutBtn.addEventListener("click", () => { + CurrentZoom = Math.max(CurrentZoom - ZoomStep, MinZoom); + UpdateImageSize(); + }); + + ResetZoomBtn.addEventListener("click", () => { + CurrentZoom = 1; + PanX = 0; + PanY = 0; + UpdateImageSize(); + }); + + // Save/Download image: fetch via GM_xmlhttpRequest to bypass CORS, then use blob URL for reliable download + SaveBtn.addEventListener("click", () => { + let src = ModalImage.src; + let urlPath = src.split("?")[0]; + let filename = urlPath.split("/").pop() || "image.png"; + GM_xmlhttpRequest({ + method: "GET", + url: src, + responseType: "blob", + onload: (resp) => { + let BlobUrl = URL.createObjectURL(resp.response); + let Link = document.createElement("a"); + Link.href = BlobUrl; + Link.download = filename; + document.body.appendChild(Link); + Link.click(); + document.body.removeChild(Link); + setTimeout(() => URL.revokeObjectURL(BlobUrl), 100); + }, + onerror: () => { + let Link = document.createElement("a"); + Link.href = src; + Link.download = filename; + Link.target = "_blank"; + document.body.appendChild(Link); + Link.click(); + document.body.removeChild(Link); + } + }); + }); + + // Apply to all images on the page + let ApplyEnlargerToImage = (img) => { + const effectiveSrc = img.currentSrc || img.src; + if (!img.classList.contains("xmoj-image-preview") && + !img.closest(".xmoj-image-modal") && + effectiveSrc && + !effectiveSrc.includes("gravatar") && + !effectiveSrc.includes("cravatar")) { + + img.classList.add("xmoj-image-preview"); + if (!img.title) { + img.title = "点击放大"; + } + img.addEventListener("click", (e) => { + e.stopPropagation(); + OpenImageModal(img); + }); + } + }; + + let ApplyEnlargerToImages = () => { + document.querySelectorAll("img").forEach(ApplyEnlargerToImage); + }; + + // Apply to existing images + ApplyEnlargerToImages(); + + // Apply to dynamically added images + let Observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType !== Node.ELEMENT_NODE) return; + if (node.tagName === "IMG") { + ApplyEnlargerToImage(node); + } else { + node.querySelectorAll("img").forEach(ApplyEnlargerToImage); + } + }); + }); + }); + + Observer.observe(document.body, { + childList: true, + subtree: true + }); + + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + 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."); + } + } +} + +main().then(r => { + console.log("XMOJ-Script loaded successfully!"); +}); \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..c9d0b50 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +const js = require("@eslint/js"); +const globals = require("globals"); + +module.exports = [ + { + ignores: ["node_modules/**", "dist/**"] + }, + js.configs.recommended, + { + files: ["src/**/*.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "script", + globals: { + ...globals.browser, + ...globals.node + } + }, + rules: { + "no-console": "off" + } + } +]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ac9998b --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "electro-xmoj", + "version": "1.0.0", + "description": "ELXMOJ Electron launcher with userscript updater", + "main": "src/main.js", + "license": "GPL-3.0-or-later", + "scripts": { + "start": "electron .", + "self-check": "electron . --self-check", + "lint": "eslint src --ext .js", + "check:syntax": "node --check src/main.js && node --check src/preload.js && node --check src/settings.js && node --check src/storage.js && node --check src/updater.js", + "check": "npm run lint && npm run check:syntax", + "pack:win": "electron-builder --win nsis portable --publish never", + "pack:mac": "electron-builder --mac zip --publish never", + "pack:linux": "electron-builder --linux AppImage tar.gz --publish never" + }, + "devDependencies": { + "@eslint/js": "^9.23.0", + "electron": "^37.2.0", + "electron-builder": "^26.0.12", + "eslint": "^9.23.0", + "globals": "^16.0.0" + }, + "build": { + "appId": "me.xmoj.elxmoj", + "productName": "ELXMOJ", + "directories": { + "output": "dist" + }, + "files": [ + "src/**/*", + "XMOJ.user.js", + "package.json", + "README.md", + "LICENSE" + ], + "asar": true, + "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", + "win": { + "target": [ + "nsis", + "portable" + ] + }, + "mac": { + "target": [ + "zip" + ], + "category": "public.app-category.developer-tools" + }, + "linux": { + "target": [ + "AppImage", + "tar.gz" + ], + "category": "Development" + } + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..ca5414d --- /dev/null +++ b/src/main.js @@ -0,0 +1,622 @@ +const path = require("node:path"); +const { app, BrowserWindow, dialog, ipcMain, Menu, net, session } = require("electron"); + +const { + loadSettings, + saveSettings, + readManagedScript, + writeManagedScript, + ensureManagedScript +} = require("./storage"); +const { + getChannelUrl, + downloadText, + extractVersion, + extractName, + extractRequires, + isNewerVersion +} = require("./updater"); + +let mainWindow = null; +let settingsWindow = null; +let settingsCache = null; +let lastCheckResult = null; + +const LOCAL_SCRIPT_PATH = path.join(__dirname, "..", "XMOJ.user.js"); +const XMOJ_HOME = "https://www.xmoj.tech"; + +async function getPhpSessionIdFromCookieStore() { + try { + const cookies = await session.defaultSession.cookies.get({ + url: XMOJ_HOME, + name: "PHPSESSID" + }); + return cookies[0]?.value || ""; + } catch { + return ""; + } +} + +function isXmojScriptApiRequest(url) { + return /^https:\/\/api\.xmoj-bbs\.(me|tech)\//i.test(String(url || "")); +} + +async function patchApiAuthPayloadIfNeeded({ url, method, headers, body }) { + if (!isXmojScriptApiRequest(url) || String(method || "").toUpperCase() !== "POST") { + return body; + } + + if (typeof body !== "string" || !body.trim()) { + return body; + } + + let parsed; + try { + parsed = JSON.parse(body); + } catch { + return body; + } + + if (!parsed || typeof parsed !== "object") { + return body; + } + + const realSessionId = await getPhpSessionIdFromCookieStore(); + if (!realSessionId) { + return body; + } + + if (!parsed.Authentication || typeof parsed.Authentication !== "object") { + parsed.Authentication = {}; + } + + parsed.Authentication.SessionID = realSessionId; + + const userHeader = headers?.["XMOJ-UserID"] ?? headers?.["xmoj-userid"]; + if (!parsed.Authentication.Username && userHeader) { + parsed.Authentication.Username = String(userHeader); + } + + return JSON.stringify(parsed); +} + +function getScriptBootstrapOptions() { + return { + getInitialScriptContent: async () => downloadText(getChannelUrl("stable")) + }; +} + +function createMainMenu() { + const openConsole = (mode = "bottom") => { + const target = BrowserWindow.getFocusedWindow() || mainWindow; + if (!target || target.isDestroyed()) return; + target.webContents.openDevTools({ mode }); + }; + + const template = [ + { + label: "ELXMOJ", + submenu: [ + { + label: "打开主页", + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.loadURL(XMOJ_HOME); + } + } + }, + { type: "separator" }, + { + label: "设置", + click: () => openSettingsWindow() + }, + { + label: "执行自检", + click: () => runSelfCheck(true) + }, + { + label: "检查脚本更新", + click: () => checkForScriptUpdate({ showNoUpdateDialog: true }) + }, + { type: "separator" }, + { + label: "查看控制台", + accelerator: "F12", + click: () => openConsole("bottom") + }, + { + label: "分离控制台窗口", + accelerator: "Ctrl+Shift+I", + click: () => openConsole("detach") + }, + { + label: "关闭控制台", + click: () => { + const target = BrowserWindow.getFocusedWindow() || mainWindow; + if (target && !target.isDestroyed()) { + target.webContents.closeDevTools(); + } + } + }, + { type: "separator" }, + { role: "forceReload", label: "强制刷新" }, + { role: "reload", label: "刷新" }, + { role: "quit", label: "退出" } + ] + }, + { + label: "编辑", + submenu: [ + { role: "undo", label: "撤销" }, + { role: "redo", label: "重做" }, + { type: "separator" }, + { role: "cut", label: "剪切" }, + { role: "copy", label: "复制" }, + { role: "paste", label: "粘贴" }, + { role: "selectAll", label: "全选" } + ] + }, + { + label: "查看", + submenu: [ + { role: "zoomIn", label: "放大" }, + { role: "zoomOut", label: "缩小" }, + { role: "resetZoom", label: "重置缩放" }, + { type: "separator" }, + { role: "togglefullscreen", label: "全屏" }, + { + label: "查看控制台", + accelerator: "F12", + click: () => openConsole("bottom") + }, + { + label: "切换开发者工具", + role: "toggleDevTools" + } + ] + }, + { + label: "窗口", + submenu: [ + { role: "minimize", label: "最小化" }, + { role: "close", label: "关闭窗口" } + ] + }, + { + label: "帮助", + submenu: [ + { + label: "关于 ELXMOJ", + click: async () => { + await dialog.showMessageBox(mainWindow || undefined, { + type: "info", + title: "关于 ELXMOJ", + message: "ELXMOJ", + detail: "Electron 封装的 XMOJ 增强启动器。\n支持 userscript 自动注入、更新检查和自检。" + }); + } + } + ] + } + ]; + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} + +async function getSettings() { + if (!settingsCache) { + settingsCache = await loadSettings(app); + } + return settingsCache; +} + +async function setSettings(nextSettings) { + settingsCache = nextSettings; + await saveSettings(app, settingsCache); +} + +function createMainWindow() { + mainWindow = new BrowserWindow({ + width: 1400, + height: 900, + minWidth: 1100, + minHeight: 680, + title: "ELXMOJ", + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + sandbox: false + } + }); + + mainWindow.loadURL(XMOJ_HOME); + mainWindow.on("closed", () => { + mainWindow = null; + }); +} + +function openSettingsWindow() { + if (settingsWindow && !settingsWindow.isDestroyed()) { + settingsWindow.focus(); + return; + } + + settingsWindow = new BrowserWindow({ + width: 520, + height: 520, + resizable: false, + minimizable: false, + maximizable: false, + autoHideMenuBar: true, + title: "ELXMOJ 设置", + parent: mainWindow || undefined, + modal: Boolean(mainWindow), + webPreferences: { + preload: path.join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + sandbox: false + } + }); + + settingsWindow.loadFile(path.join(__dirname, "settings.html")); + settingsWindow.on("closed", () => { + settingsWindow = null; + }); +} + +function buildSelfCheckReport({ + hasManagedScript, + localVersion, + hasVersionMeta, + urlReachable, + injectionReady, + channel +}) { + const lines = [ + "ELXMOJ 自检结果", + "", + `脚本文件: ${hasManagedScript ? "OK" : "失败"}`, + `脚本版本元数据(@version): ${hasVersionMeta ? `OK (${localVersion})` : "缺失"}`, + `更新源可访问: ${urlReachable ? "OK" : "失败"}`, + `注入状态: ${injectionReady ? "已就绪" : "未就绪"}`, + `更新通道: ${channel === "preview" ? "预览版" : "正式版"}` + ]; + return lines.join("\n"); +} + +async function checkUpdateEndpoint(channel) { + try { + const text = await downloadText(getChannelUrl(channel)); + return { ok: true, text }; + } catch (error) { + return { ok: false, error: String(error?.message || error) }; + } +} + +async function runSelfCheck(showDialog = false) { + const settings = await getSettings(); + await ensureManagedScript(app, LOCAL_SCRIPT_PATH, getScriptBootstrapOptions()); + const localScript = await readManagedScript(app, LOCAL_SCRIPT_PATH, getScriptBootstrapOptions()); + const localVersion = extractVersion(localScript); + const endpoint = await checkUpdateEndpoint(settings.channel); + + const report = buildSelfCheckReport({ + hasManagedScript: Boolean(localScript && localScript.length > 0), + localVersion, + hasVersionMeta: Boolean(localVersion), + urlReachable: endpoint.ok, + injectionReady: true, + channel: settings.channel + }); + + lastCheckResult = { + timestamp: Date.now(), + report, + endpointError: endpoint.ok ? "" : endpoint.error + }; + + if (showDialog && mainWindow) { + await dialog.showMessageBox(mainWindow, { + type: endpoint.ok ? "info" : "warning", + title: "ELXMOJ 自检", + message: report, + detail: endpoint.ok ? "" : `更新源访问失败:\n${endpoint.error}` + }); + } + + return lastCheckResult; +} + +async function checkForScriptUpdate({ showNoUpdateDialog = false } = {}) { + const settings = await getSettings(); + await ensureManagedScript(app, LOCAL_SCRIPT_PATH, getScriptBootstrapOptions()); + + const localScript = await readManagedScript(app, LOCAL_SCRIPT_PATH, getScriptBootstrapOptions()); + const currentVersion = extractVersion(localScript) || "0.0.0"; + + let remoteScript; + try { + remoteScript = await downloadText(getChannelUrl(settings.channel)); + } catch (error) { + if (showNoUpdateDialog && mainWindow) { + await dialog.showMessageBox(mainWindow, { + type: "warning", + title: "更新检查失败", + message: "无法连接脚本更新源。", + detail: String(error?.message || error) + }); + } + return { updated: false, reason: "download_failed" }; + } + + const remoteVersion = extractVersion(remoteScript) || "0.0.0"; + const remoteName = extractName(remoteScript); + const shouldUpdate = isNewerVersion(currentVersion, remoteVersion); + + if (!shouldUpdate) { + if (showNoUpdateDialog && mainWindow) { + await dialog.showMessageBox(mainWindow, { + type: "info", + title: "已是最新", + message: `${remoteName} 当前已是最新版本 (${currentVersion})` + }); + } + return { updated: false, reason: "already_latest", currentVersion, remoteVersion }; + } + + if (settings.skipVersionPrompt === remoteVersion) { + return { updated: false, reason: "user_skipped", currentVersion, remoteVersion }; + } + + if (!mainWindow) { + return { updated: false, reason: "no_window" }; + } + + const prompt = await dialog.showMessageBox(mainWindow, { + type: "question", + title: "发现新版本脚本", + message: `${remoteName} 有新版本可用: ${currentVersion} -> ${remoteVersion}`, + detail: `来源: ${getChannelUrl(settings.channel)}\n是否更新并立即生效?`, + buttons: ["立即更新", "跳过本次版本", "暂不更新"], + cancelId: 2, + defaultId: 0, + noLink: true + }); + + if (prompt.response === 1) { + const next = { ...settings, skipVersionPrompt: remoteVersion }; + await setSettings(next); + return { updated: false, reason: "skip_this_version", currentVersion, remoteVersion }; + } + + if (prompt.response !== 0) { + return { updated: false, reason: "cancelled", currentVersion, remoteVersion }; + } + + await writeManagedScript(app, remoteScript); + const next = { ...settings, skipVersionPrompt: "" }; + await setSettings(next); + + if (mainWindow && !mainWindow.isDestroyed()) { + await mainWindow.webContents.reload(); + } + + return { updated: true, currentVersion, remoteVersion }; +} + +function registerIpcHandlers() { + ipcMain.handle("elxmoj:get-settings", async () => getSettings()); + + ipcMain.handle("elxmoj:update-settings", async (_event, patch) => { + const current = await getSettings(); + const next = { ...current, ...patch }; + await setSettings(next); + return next; + }); + + ipcMain.handle("elxmoj:get-script-payload", async () => { + const scriptText = await readManagedScript(app, LOCAL_SCRIPT_PATH, getScriptBootstrapOptions()); + return { + name: extractName(scriptText), + version: extractVersion(scriptText), + requires: extractRequires(scriptText), + scriptText + }; + }); + + ipcMain.handle("elxmoj:check-update", async () => checkForScriptUpdate({ showNoUpdateDialog: true })); + ipcMain.handle("elxmoj:run-self-check", async () => runSelfCheck(true)); + ipcMain.handle("elxmoj:get-last-self-check", async () => lastCheckResult); + + ipcMain.handle("elxmoj:get-phpsessid", async () => { + const value = await getPhpSessionIdFromCookieStore(); + return value || ""; + }); + + ipcMain.handle("elxmoj:gm-xhr", async (_event, request) => { + const req = request || {}; + const url = String(req.url || ""); + if (!url) { + return { + ok: false, + error: "GM_xmlhttpRequest requires a non-empty url" + }; + } + + const method = String(req.method || "GET").toUpperCase(); + const headers = req.headers && typeof req.headers === "object" ? req.headers : {}; + const timeout = Number.isFinite(req.timeout) ? Number(req.timeout) : 20000; + const body = await patchApiAuthPayloadIfNeeded({ + url, + method, + headers, + body: req.data + }); + + return new Promise((resolve) => { + + const client = net.request({ + method, + url, + session: session.defaultSession + }); + + for (const [k, v] of Object.entries(headers)) { + if (v !== undefined && v !== null) { + client.setHeader(k, String(v)); + } + } + + const timer = setTimeout(() => { + try { + client.abort(); + } catch { + // ignore abort errors + } + resolve({ + ok: false, + error: `Timeout after ${timeout}ms` + }); + }, timeout); + + client.on("response", (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(Buffer.from(chunk))); + res.on("end", () => { + clearTimeout(timer); + resolve({ + ok: true, + status: res.statusCode, + statusText: res.statusMessage, + responseText: Buffer.concat(chunks).toString("utf8"), + finalUrl: url, + headers: res.headers || {} + }); + }); + }); + + client.on("error", (error) => { + clearTimeout(timer); + resolve({ + ok: false, + error: String(error?.message || error) + }); + }); + + if (body !== undefined && body !== null) { + if (typeof body === "string" || Buffer.isBuffer(body)) { + client.write(body); + } else { + client.write(String(body)); + } + } + + client.end(); + }); + }); + + ipcMain.handle("elxmoj:gm-cookie-list", async (_event, details) => { + const input = details && typeof details === "object" ? details : {}; + const query = { + url: typeof input.url === "string" ? input.url : XMOJ_HOME, + name: typeof input.name === "string" ? input.name : undefined, + domain: typeof input.domain === "string" ? input.domain : undefined, + path: typeof input.path === "string" ? input.path : undefined, + secure: typeof input.secure === "boolean" ? input.secure : undefined, + session: typeof input.session === "boolean" ? input.session : undefined, + httpOnly: typeof input.httpOnly === "boolean" ? input.httpOnly : undefined + }; + + try { + const cookies = await session.defaultSession.cookies.get(query); + return cookies; + } catch { + return []; + } + }); + + ipcMain.handle("elxmoj:gm-cookie-set", async (_event, details) => { + const input = details && typeof details === "object" ? details : {}; + const cookie = { + url: typeof input.url === "string" ? input.url : XMOJ_HOME, + name: String(input.name || ""), + value: String(input.value || ""), + domain: typeof input.domain === "string" ? input.domain : undefined, + path: typeof input.path === "string" ? input.path : "/", + secure: typeof input.secure === "boolean" ? input.secure : undefined, + httpOnly: typeof input.httpOnly === "boolean" ? input.httpOnly : undefined, + sameSite: typeof input.sameSite === "string" ? input.sameSite : undefined, + expirationDate: typeof input.expirationDate === "number" ? input.expirationDate : undefined + }; + + if (!cookie.name) { + return { success: false, error: "GM_cookie.set requires cookie name" }; + } + + try { + await session.defaultSession.cookies.set(cookie); + return { success: true }; + } catch (error) { + const message = String(error?.message || error); + const isHttpOnlyConflict = message.includes("EXCLUDE_OVERWRITE_HTTP_ONLY"); + + return { + success: false, + ignored: isHttpOnlyConflict, + error: message, + code: isHttpOnlyConflict ? "EXCLUDE_OVERWRITE_HTTP_ONLY" : "SET_FAILED" + }; + } + }); + + ipcMain.handle("elxmoj:gm-cookie-delete", async (_event, details) => { + const input = details && typeof details === "object" ? details : {}; + const targetUrl = typeof input.url === "string" ? input.url : XMOJ_HOME; + const targetName = String(input.name || ""); + + if (!targetName) { + return { success: false, error: "GM_cookie.delete requires cookie name" }; + } + + try { + await session.defaultSession.cookies.remove(targetUrl, targetName); + return { success: true }; + } catch (error) { + return { + success: false, + error: String(error?.message || error), + code: "DELETE_FAILED" + }; + } + }); +} + +async function bootstrap() { + createMainMenu(); + registerIpcHandlers(); + createMainWindow(); + + const settings = await getSettings(); + await ensureManagedScript(app, LOCAL_SCRIPT_PATH, getScriptBootstrapOptions()); + await runSelfCheck(process.argv.includes("--self-check")); + + if (settings.checkUpdateOnStartup) { + await checkForScriptUpdate({ showNoUpdateDialog: false }); + } +} + +app.whenReady().then(bootstrap); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createMainWindow(); + } +}); diff --git a/src/preload.js b/src/preload.js new file mode 100644 index 0000000..0fc81d9 --- /dev/null +++ b/src/preload.js @@ -0,0 +1,796 @@ +const { contextBridge, ipcRenderer } = require("electron"); +const { createHash } = require("node:crypto"); +const vm = require("node:vm"); + +const REQUIRE_FALLBACKS = { + "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js": [ + "https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js", + "https://unpkg.com/crypto-js@4.1.1/crypto-js.js" + ], + "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.2/purify.min.js": [ + "https://cdn.jsdelivr.net/npm/dompurify@3.0.2/dist/purify.min.js", + "https://unpkg.com/dompurify@3.0.2/dist/purify.min.js" + ], + "https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js": [ + "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js", + "https://unpkg.com/marked@4.3.0/marked.min.js" + ], + "https://gitee.com/mirrors_google/diff-match-patch/raw/master/javascript/diff_match_patch_uncompressed.js": [ + "https://cdnjs.cloudflare.com/ajax/libs/diff_match_patch/20121119/diff_match_patch_uncompressed.js", + "https://cdn.jsdelivr.net/gh/google/diff-match-patch@master/javascript/diff_match_patch_uncompressed.js", + "https://raw.githubusercontent.com/google/diff-match-patch/master/javascript/diff_match_patch_uncompressed.js" + ] +}; + +const ELXMOJ_INJECTION_LOCK_KEY = "__ELXMOJ_INJECTION_LOCK__"; +const ELXMOJ_RELOAD_GUARD_INSTALLED_KEY = "__ELXMOJ_RELOAD_GUARD_INSTALLED__"; +const ELXMOJ_RELOAD_LOG_KEY = "__ELXMOJ_RELOAD_LOG__"; +const ELXMOJ_BLOCK_NEXT_RELOAD_UNTIL_KEY = "__ELXMOJ_BLOCK_NEXT_RELOAD_UNTIL__"; +const ELXMOJ_COOKIE_SHIM_INSTALLED_KEY = "__ELXMOJ_COOKIE_SHIM_INSTALLED__"; +const ELXMOJ_SHADOW_PHPSESSID_KEY = "__ELXMOJ_SHADOW_PHPSESSID__"; +const ELXMOJ_SAVED_CREDENTIAL_KEY = "__ELXMOJ_SAVED_CREDENTIAL__"; +const ELXMOJ_TURNSTILE_BRIDGE_INSTALLED_KEY = "__ELXMOJ_TURNSTILE_BRIDGE_INSTALLED__"; + +function shouldInjectUserscriptInThisFrame() { + try { + return window.top === window.self; + } catch { + return false; + } +} + +function acquireInjectionLock() { + if (window[ELXMOJ_INJECTION_LOCK_KEY]) { + return false; + } + window[ELXMOJ_INJECTION_LOCK_KEY] = true; + return true; +} + +function installReloadLoopGuard() { + if (window[ELXMOJ_RELOAD_GUARD_INSTALLED_KEY]) { + return; + } + window[ELXMOJ_RELOAD_GUARD_INSTALLED_KEY] = true; + + const MAX_RELOADS_IN_WINDOW = 3; + const WINDOW_MS = 15000; + + const locationObject = window.location; + const locationProto = Object.getPrototypeOf(locationObject); + const originalReload = typeof locationObject.reload === "function" + ? locationObject.reload.bind(locationObject) + : null; + + if (!originalReload) { + return; + } + + const isReloadAllowed = () => { + try { + const now = Date.now(); + const blockedUntil = Number.parseInt(sessionStorage.getItem(ELXMOJ_BLOCK_NEXT_RELOAD_UNTIL_KEY) || "0", 10); + if (Number.isFinite(blockedUntil) && blockedUntil > now) { + return false; + } + + const raw = sessionStorage.getItem(ELXMOJ_RELOAD_LOG_KEY); + const parsed = JSON.parse(raw || "[]"); + const history = Array.isArray(parsed) ? parsed : []; + const recent = history.filter((ts) => Number.isFinite(ts) && now - ts <= WINDOW_MS); + + if (recent.length >= MAX_RELOADS_IN_WINDOW) { + return false; + } + + recent.push(now); + sessionStorage.setItem(ELXMOJ_RELOAD_LOG_KEY, JSON.stringify(recent)); + sessionStorage.removeItem(ELXMOJ_BLOCK_NEXT_RELOAD_UNTIL_KEY); + return true; + } catch { + return true; + } + }; + + const patchedReload = function patchedReload(...args) { + if (!isReloadAllowed()) { + console.warn("ELXMOJ blocked excessive page reload to prevent refresh loop."); + return; + } + + return originalReload(...args); + }; + + try { + locationObject.reload = patchedReload; + } catch { + // ignore when Location.reload is not writable on instance + } + + try { + if (locationProto && typeof locationProto.reload === "function") { + locationProto.reload = patchedReload; + } + } catch { + // ignore when Location.prototype.reload is not writable + } +} + +function blockNextReload(milliseconds = 8000) { + try { + const until = Date.now() + Math.max(1000, Number(milliseconds) || 0); + sessionStorage.setItem(ELXMOJ_BLOCK_NEXT_RELOAD_UNTIL_KEY, String(until)); + } catch { + // ignore storage failures + } +} + +function setShadowPhpSessionId(value) { + const normalized = String(value || "").trim(); + if (!normalized) { + return; + } + + try { + sessionStorage.setItem(ELXMOJ_SHADOW_PHPSESSID_KEY, normalized); + } catch { + // ignore storage failures + } +} + +function getShadowPhpSessionId() { + try { + return String(sessionStorage.getItem(ELXMOJ_SHADOW_PHPSESSID_KEY) || "").trim(); + } catch { + return ""; + } +} + +function installCookieVisibilityShim(initialPhpSessionId) { + if (window[ELXMOJ_COOKIE_SHIM_INSTALLED_KEY]) { + return; + } + + if (initialPhpSessionId) { + setShadowPhpSessionId(initialPhpSessionId); + } + + const docProto = Object.getPrototypeOf(document); + const descriptor = Object.getOwnPropertyDescriptor(docProto, "cookie"); + if (!descriptor || typeof descriptor.get !== "function" || typeof descriptor.set !== "function") { + return; + } + + const nativeGet = descriptor.get.bind(document); + const nativeSet = descriptor.set.bind(document); + + try { + Object.defineProperty(document, "cookie", { + configurable: true, + enumerable: true, + get() { + const raw = nativeGet() || ""; + if (/\bPHPSESSID=/i.test(raw)) { + return raw; + } + + const shadow = getShadowPhpSessionId(); + if (!shadow) { + return raw; + } + + return raw ? `${raw}; PHPSESSID=${shadow}` : `PHPSESSID=${shadow}`; + }, + set(value) { + return nativeSet(value); + } + }); + window[ELXMOJ_COOKIE_SHIM_INSTALLED_KEY] = true; + } catch { + // ignore non-configurable cookie descriptor + } +} + +function createStoragePrefix() { + return "ELXMOJ_GM_"; +} + +function setupHexMd5Polyfill() { + if (typeof window.hex_md5 === "function") return; + + window.hex_md5 = (input) => { + if (window.CryptoJS && typeof window.CryptoJS.MD5 === "function") { + return window.CryptoJS.MD5(String(input ?? "")).toString(); + } + + return createHash("md5").update(String(input ?? ""), "utf8").digest("hex"); + }; +} + +function setupCryptoJsFallback() { + if (window.CryptoJS && typeof window.CryptoJS.MD5 === "function") { + return; + } + + window.CryptoJS = { + MD5: (input) => { + const hex = createHash("md5").update(String(input ?? ""), "utf8").digest("hex"); + return { + toString: () => hex + }; + } + }; +} + +function setupMarkedFallback() { + if (window.marked && typeof window.marked.parse === "function") { + return; + } + + const escapeHtml = (input) => + String(input ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); + + const parse = (markdown) => { + const text = String(markdown ?? ""); + return text + .split(/\r?\n\r?\n/) + .map((block) => `

    ${escapeHtml(block).replace(/\r?\n/g, "
    ")}

    `) + .join("\n"); + }; + + window.marked = { + parse, + lexer: (markdown) => [{ type: "paragraph", text: String(markdown ?? "") }] + }; +} + +function setupDomPurifyFallback() { + if (window.DOMPurify && typeof window.DOMPurify.sanitize === "function") { + return; + } + + window.DOMPurify = { + sanitize: (input, options = {}) => { + const raw = String(input ?? ""); + + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(`
    ${raw}
    `, "text/html"); + const wrapper = doc.body.firstElementChild; + if (!wrapper) { + return ""; + } + + const allowedTagsInput = Array.isArray(options.ALLOWED_TAGS) ? options.ALLOWED_TAGS : []; + const allowedAttrsInput = Array.isArray(options.ALLOWED_ATTR) ? options.ALLOWED_ATTR : []; + const allowedTags = new Set(allowedTagsInput.map((value) => String(value).toLowerCase())); + const allowedAttrs = new Set(allowedAttrsInput.map((value) => String(value).toLowerCase())); + + const sanitizeElement = (element) => { + for (const child of Array.from(element.children)) { + const tagName = child.tagName.toLowerCase(); + const tagAllowed = allowedTags.size === 0 || allowedTags.has(tagName); + + if (!tagAllowed) { + child.replaceWith(doc.createTextNode(child.textContent || "")); + continue; + } + + for (const attr of Array.from(child.attributes)) { + const attrName = attr.name.toLowerCase(); + const value = String(attr.value || ""); + const isEventAttr = attrName.startsWith("on"); + const isJavascriptUrl = + /^(href|src|xlink:href)$/i.test(attrName) && /^\s*javascript:/i.test(value); + const attrAllowed = allowedAttrs.size === 0 || allowedAttrs.has(attrName); + + if (isEventAttr || isJavascriptUrl || !attrAllowed) { + child.removeAttribute(attr.name); + } + } + + sanitizeElement(child); + } + }; + + sanitizeElement(wrapper); + return wrapper.innerHTML; + } catch { + return raw.replace(/[\s\S]*?<\/script>/gi, ""); + } + } + }; +} + +function setupTurnstileCallbackBridge() { + if (window[ELXMOJ_TURNSTILE_BRIDGE_INSTALLED_KEY]) { + return; + } + + window[ELXMOJ_TURNSTILE_BRIDGE_INSTALLED_KEY] = true; + + window.addEventListener("message", (event) => { + const payload = event.data; + if (!payload || payload.__ELXMOJ_TURNSTILE_CALLBACK__ !== true) { + return; + } + + try { + if (typeof window.CaptchaLoadedCallback === "function") { + window.CaptchaLoadedCallback(...(payload.args || [])); + } + } catch (error) { + console.error("ELXMOJ turnstile callback bridge error:", error); + } + }); + + try { + const script = document.createElement("script"); + script.textContent = ` + (function () { + if (typeof window.CaptchaLoadedCallback === "function") { + return; + } + window.CaptchaLoadedCallback = function () { + window.postMessage({ + __ELXMOJ_TURNSTILE_CALLBACK__: true, + args: Array.prototype.slice.call(arguments) + }, "*"); + }; + })(); + `; + (document.documentElement || document.head || document.body).appendChild(script); + script.remove(); + } catch { + // ignore bridge injection failures + } +} + +function setupCredentialManagementFallback() { + const hasNativeCredentialApi = + typeof navigator !== "undefined" && + navigator.credentials && + typeof navigator.credentials.store === "function" && + typeof navigator.credentials.get === "function"; + + if (hasNativeCredentialApi && typeof window.PasswordCredential === "function") { + return; + } + + class LocalPasswordCredential { + constructor(init = {}) { + this.id = String(init.id || ""); + this.password = String(init.password || ""); + this.type = "password"; + } + } + + if (typeof window.PasswordCredential !== "function") { + window.PasswordCredential = LocalPasswordCredential; + } + + const readSaved = () => { + try { + const raw = localStorage.getItem(ELXMOJ_SAVED_CREDENTIAL_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== "object") return null; + if (!parsed.id || !parsed.password) return null; + return { + id: String(parsed.id), + password: String(parsed.password) + }; + } catch { + return null; + } + }; + + const writeSaved = (credential) => { + try { + localStorage.setItem( + ELXMOJ_SAVED_CREDENTIAL_KEY, + JSON.stringify({ + id: String(credential?.id || ""), + password: String(credential?.password || "") + }) + ); + } catch { + // ignore storage failures + } + }; + + const clearSaved = () => { + try { + localStorage.removeItem(ELXMOJ_SAVED_CREDENTIAL_KEY); + } catch { + // ignore storage failures + } + }; + + const fallbackCredentials = { + async store(credential) { + if (!credential || !credential.id || !credential.password) { + return null; + } + writeSaved(credential); + return credential; + }, + async get(options = {}) { + const wantsPassword = Boolean(options && options.password); + if (!wantsPassword) { + return null; + } + + const saved = readSaved(); + if (!saved) { + return null; + } + + return new window.PasswordCredential(saved); + }, + async preventSilentAccess() { + clearSaved(); + } + }; + + try { + if (!navigator.credentials) { + Object.defineProperty(navigator, "credentials", { + configurable: true, + enumerable: true, + value: fallbackCredentials + }); + return; + } + } catch { + // ignore and fall through to method patching + } + + try { + if (typeof navigator.credentials.store !== "function") { + navigator.credentials.store = fallbackCredentials.store; + } + if (typeof navigator.credentials.get !== "function") { + navigator.credentials.get = fallbackCredentials.get; + } + if (typeof navigator.credentials.preventSilentAccess !== "function") { + navigator.credentials.preventSilentAccess = fallbackCredentials.preventSilentAccess; + } + } catch { + // ignore non-writable navigator.credentials object + } +} + +function setupGmPolyfills(payload = null) { + const prefix = createStoragePrefix(); + + window.unsafeWindow = window; + + window.GM_getValue = (key, defaultValue = null) => { + const raw = localStorage.getItem(`${prefix}${key}`); + if (raw === null || raw === undefined) return defaultValue; + try { + return JSON.parse(raw); + } catch { + return raw; + } + }; + + window.GM_setValue = (key, value) => { + localStorage.setItem(`${prefix}${key}`, JSON.stringify(value)); + }; + + window.GM_setClipboard = async (text) => { + await navigator.clipboard.writeText(String(text ?? "")); + }; + + window.GM_registerMenuCommand = (_name, _callback) => { + // Electron 版本先使用应用菜单提供命令,不在页面内重复注册。 + }; + + window.GM_xmlhttpRequest = (details = {}) => { + let aborted = false; + + ipcRenderer + .invoke("elxmoj:gm-xhr", { + url: details.url, + method: details.method || "GET", + headers: details.headers || {}, + data: details.data, + timeout: details.timeout + }) + .then((result) => { + if (aborted) return; + + if (!result?.ok) { + if (typeof details.onerror === "function") { + details.onerror({ + error: String(result?.error || "Unknown request error"), + readyState: 4 + }); + } + return; + } + + if (typeof details.onload === "function") { + details.onload({ + status: result.status, + statusText: result.statusText, + responseText: result.responseText, + finalUrl: result.finalUrl || details.url, + responseHeaders: result.headers || {}, + readyState: 4 + }); + } + }) + .catch((error) => { + if (aborted) return; + if (typeof details.onerror === "function") { + details.onerror({ error: String(error?.message || error), readyState: 4 }); + } + }); + + return { + abort: () => { + aborted = true; + } + }; + }; + + window.GM_cookie = { + list: async (details = {}, callback) => { + try { + const cookies = await ipcRenderer.invoke("elxmoj:gm-cookie-list", details || {}); + if (typeof callback === "function") { + callback(cookies, null); + } + return cookies; + } catch (error) { + const errMessage = String(error?.message || error); + if (typeof callback === "function") { + callback([], errMessage); + } + return []; + } + }, + set: async (details = {}, callback) => { + try { + const result = await ipcRenderer.invoke("elxmoj:gm-cookie-set", details || {}); + if (!result?.success) { + const isHttpOnlyConflict = result?.code === "EXCLUDE_OVERWRITE_HTTP_ONLY" || result?.ignored === true; + const errMessage = String(result?.error || "GM_cookie.set failed"); + if (isHttpOnlyConflict) { + const isPhpSessid = String(details?.name || "").toUpperCase() === "PHPSESSID"; + if (isPhpSessid) { + setShadowPhpSessionId(details?.value); + blockNextReload(); + if (typeof callback === "function") { + callback(result, null); + } + throw new Error("PHPSESSID_HTTPONLY_CONFLICT"); + //这个conflict非常奇妙,会直接阻断刷新风暴TAT + } + if (typeof callback === "function") { + callback(result, null); + } + return result; + } + if (typeof callback === "function") { + callback(result, errMessage); + } + throw new Error(errMessage); + } + if (typeof callback === "function") { + callback(result, null); + } + return result; + } catch (error) { + const errMessage = String(error?.message || error); + if (typeof callback === "function") { + callback({ success: false, error: errMessage }, errMessage); + } + throw new Error(errMessage); + } + }, + delete: async (details = {}, callback) => { + try { + const result = await ipcRenderer.invoke("elxmoj:gm-cookie-delete", details || {}); + if (!result?.success) { + const errMessage = String(result?.error || "GM_cookie.delete failed"); + if (typeof callback === "function") { + callback(result, errMessage); + } + throw new Error(errMessage); + } + if (typeof callback === "function") { + callback(result, null); + } + return result; + } catch (error) { + const errMessage = String(error?.message || error); + if (typeof callback === "function") { + callback({ success: false, error: errMessage }, errMessage); + } + throw new Error(errMessage); + } + } + }; + + const gmApi = { + getValue: window.GM_getValue, + setValue: window.GM_setValue, + setClipboard: window.GM_setClipboard, + registerMenuCommand: window.GM_registerMenuCommand, + xmlHttpRequest: window.GM_xmlhttpRequest, + xmlhttpRequest: window.GM_xmlhttpRequest, + cookie: window.GM_cookie, + info: { + script: { + name: payload?.name || "XMOJ", + version: payload?.version || "0.0.0" + }, + scriptHandler: "ELXMOJ", + version: "1.0.0" + } + }; + + window.GM = gmApi; + window.GM_info = gmApi.info; +} + +async function loadRequireScripts(urls) { + for (const url of urls) { + await loadRequireScriptWithFallback(url); + } +} + +function getRequireCandidates(url) { + const fallbacks = REQUIRE_FALLBACKS[url] || []; + return [url, ...fallbacks]; +} + +function loadScriptTag(url) { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = url; + script.async = false; + script.onload = () => resolve(url); + script.onerror = () => reject(new Error(`Failed to load @require: ${url}`)); + document.head.appendChild(script); + }); +} + +function executeRequireScriptInCurrentContext(source, url) { + const code = String(source ?? ""); + if (!code.trim()) { + throw new Error(`Empty script body for @require: ${url}`); + } + + vm.runInThisContext(`${code}\n//# sourceURL=${url}`); +} + +async function loadScriptInCurrentContext(url) { + const result = await ipcRenderer.invoke("elxmoj:gm-xhr", { + url, + method: "GET", + timeout: 20000 + }); + + if (!result?.ok) { + throw new Error(`Failed to download @require script: ${String(result?.error || "unknown error")}`); + } + + executeRequireScriptInCurrentContext(result.responseText, url); +} + +async function loadRequireScriptWithFallback(url) { + const candidates = getRequireCandidates(url); + let lastError = null; + + for (const candidate of candidates) { + try { + await loadScriptInCurrentContext(candidate); + return; + } catch (error) { + lastError = error; + + // Final backup path: still try regular script tag in case a library must execute in page world. + try { + await loadScriptTag(candidate); + return; + } catch { + // ignore and continue to next candidate + } + } + } + + throw new Error( + `Failed to load @require after trying ${candidates.length} source(s): ${url}. Last error: ${String(lastError?.message || lastError)}` + ); +} + +async function injectUserscriptWhenReady() { + if (!shouldInjectUserscriptInThisFrame()) { + return; + } + + if (!acquireInjectionLock()) { + return; + } + + if (!location.hostname.endsWith("xmoj.tech") && location.hostname !== "116.62.212.172") { + return; + } + + try { + const settings = await ipcRenderer.invoke("elxmoj:get-settings"); + if (settings && settings.autoInjectUserscript === false) { + window.__ELXMOJ_INJECTION_STATUS__ = { + ok: false, + reason: "auto_inject_disabled" + }; + return; + } + + const payload = await ipcRenderer.invoke("elxmoj:get-script-payload"); + const phpSessionId = await ipcRenderer.invoke("elxmoj:get-phpsessid"); + setShadowPhpSessionId(phpSessionId); + + if (!payload || !payload.scriptText) { + window.__ELXMOJ_INJECTION_STATUS__ = { + ok: false, + reason: "script_payload_empty" + }; + return; + } + + setupGmPolyfills(payload); + setupCredentialManagementFallback(); + installReloadLoopGuard(); + installCookieVisibilityShim(String(phpSessionId || "")); + await loadRequireScripts(payload.requires || []); + setupCryptoJsFallback(); + setupMarkedFallback(); + setupDomPurifyFallback(); + setupTurnstileCallbackBridge(); + setupHexMd5Polyfill(); + + // 在页面上下文执行 userscript。 + const runner = new Function(payload.scriptText); + runner(); + + window.__ELXMOJ_INJECTION_STATUS__ = { + ok: true, + version: payload.version, + loadedAt: Date.now(), + requireCount: (payload.requires || []).length + }; + } catch (error) { + console.error("ELXMOJ injection failed:", error); + window.__ELXMOJ_INJECTION_STATUS__ = { + ok: false, + reason: String(error?.message || error) + }; + } +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + injectUserscriptWhenReady(); + }); +} else { + injectUserscriptWhenReady(); +} + +contextBridge.exposeInMainWorld("ELXMOJ", { + getSettings: () => ipcRenderer.invoke("elxmoj:get-settings"), + updateSettings: (patch) => ipcRenderer.invoke("elxmoj:update-settings", patch), + checkUpdate: () => ipcRenderer.invoke("elxmoj:check-update"), + runSelfCheck: () => ipcRenderer.invoke("elxmoj:run-self-check"), + getLastSelfCheck: () => ipcRenderer.invoke("elxmoj:get-last-self-check") +}); diff --git a/src/settings.html b/src/settings.html new file mode 100644 index 0000000..43d9a12 --- /dev/null +++ b/src/settings.html @@ -0,0 +1,202 @@ + + + + + + ELXMOJ 设置 + + + +
    +
    +

    ELXMOJ 设置

    +

    管理脚本更新通道、启动行为,以及手动自检。

    + +
    +
    更新通道
    +
    +
    +
    脚本来源
    +
    正式版: xmoj-bbs.me,预览版: dev.xmoj-bbs.me
    +
    + +
    +
    + +
    +
    启动行为
    +
    + +
    +
    + +
    +
    + +
    +
    工具
    +
    + + +
    +
    等待操作...
    +
    + + +
    +
    + + + + diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 0000000..51e1a61 --- /dev/null +++ b/src/settings.js @@ -0,0 +1,50 @@ +async function loadSettings() { + const settings = await window.ELXMOJ.getSettings(); + document.getElementById("channel").value = settings.channel || "stable"; + document.getElementById("checkUpdateOnStartup").checked = Boolean(settings.checkUpdateOnStartup); + document.getElementById("autoInjectUserscript").checked = Boolean(settings.autoInjectUserscript); + + const last = await window.ELXMOJ.getLastSelfCheck(); + if (last?.report) { + setStatus(`${last.report}\n\n时间: ${new Date(last.timestamp).toLocaleString()}`); + } +} + +function collectSettings() { + return { + channel: document.getElementById("channel").value, + checkUpdateOnStartup: document.getElementById("checkUpdateOnStartup").checked, + autoInjectUserscript: document.getElementById("autoInjectUserscript").checked + }; +} + +function setStatus(text) { + document.getElementById("status").textContent = text; +} + +async function saveSettings() { + const patch = collectSettings(); + const next = await window.ELXMOJ.updateSettings(patch); + setStatus(`已保存\n通道: ${next.channel}\n启动更新检查: ${next.checkUpdateOnStartup}\n自动注入: ${next.autoInjectUserscript}`); +} + +async function checkUpdateNow() { + setStatus("正在检查更新..."); + const result = await window.ELXMOJ.checkUpdate(); + setStatus(`更新检查结果:\n${JSON.stringify(result, null, 2)}`); +} + +async function runSelfCheckNow() { + setStatus("正在执行自检..."); + const result = await window.ELXMOJ.runSelfCheck(); + setStatus(result.report || "自检完成"); +} + +document.getElementById("btnSave").addEventListener("click", saveSettings); +document.getElementById("btnCheckUpdate").addEventListener("click", checkUpdateNow); +document.getElementById("btnSelfCheck").addEventListener("click", runSelfCheckNow); +document.getElementById("btnClose").addEventListener("click", () => window.close()); + +loadSettings().catch((error) => { + setStatus(`加载设置失败: ${String(error)}`); +}); diff --git a/src/storage.js b/src/storage.js new file mode 100644 index 0000000..4751ad0 --- /dev/null +++ b/src/storage.js @@ -0,0 +1,99 @@ +const fs = require("node:fs/promises"); +const path = require("node:path"); + +const DEFAULT_SETTINGS = { + channel: "stable", + checkUpdateOnStartup: true, + autoInjectUserscript: true, + skipVersionPrompt: "" +}; + +const APP_SCRIPT_NAME = "XMOJ.user.js"; +const SETTINGS_FILE_NAME = "settings.json"; + +function getPaths(app) { + const userDataDir = app.getPath("userData"); + return { + userDataDir, + settingsFile: path.join(userDataDir, SETTINGS_FILE_NAME), + managedScriptFile: path.join(userDataDir, APP_SCRIPT_NAME) + }; +} + +async function ensureDir(dirPath) { + await fs.mkdir(dirPath, { recursive: true }); +} + +async function readJsonOrDefault(filePath, fallback) { + try { + const raw = await fs.readFile(filePath, "utf8"); + return JSON.parse(raw); + } catch { + return fallback; + } +} + +async function writeJson(filePath, value) { + await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +async function loadSettings(app) { + const paths = getPaths(app); + await ensureDir(paths.userDataDir); + const saved = await readJsonOrDefault(paths.settingsFile, {}); + return { ...DEFAULT_SETTINGS, ...saved }; +} + +async function saveSettings(app, settings) { + const paths = getPaths(app); + await ensureDir(paths.userDataDir); + await writeJson(paths.settingsFile, settings); +} + +async function ensureManagedScript(app, localScriptPath, options = {}) { + const paths = getPaths(app); + await ensureDir(paths.userDataDir); + + try { + await fs.access(paths.managedScriptFile); + return paths.managedScriptFile; + } catch { + let initialContent = ""; + + if (typeof options.getInitialScriptContent === "function") { + try { + initialContent = String((await options.getInitialScriptContent()) || ""); + } catch { + initialContent = ""; + } + } + + if (!initialContent) { + initialContent = await fs.readFile(localScriptPath, "utf8"); + } + + await fs.writeFile(paths.managedScriptFile, initialContent, "utf8"); + return paths.managedScriptFile; + } +} + +async function readManagedScript(app, localScriptPath, options = {}) { + const managedPath = await ensureManagedScript(app, localScriptPath, options); + return fs.readFile(managedPath, "utf8"); +} + +async function writeManagedScript(app, content) { + const paths = getPaths(app); + await ensureDir(paths.userDataDir); + await fs.writeFile(paths.managedScriptFile, content, "utf8"); +} + +module.exports = { + DEFAULT_SETTINGS, + getPaths, + loadSettings, + saveSettings, + readManagedScript, + writeManagedScript, + ensureManagedScript +}; diff --git a/src/updater.js b/src/updater.js new file mode 100644 index 0000000..9d3aa7b --- /dev/null +++ b/src/updater.js @@ -0,0 +1,79 @@ +const https = require("node:https"); + +const STABLE_URL = "https://xmoj-bbs.me/XMOJ.user.js"; +const PREVIEW_URL = "https://dev.xmoj-bbs.me/XMOJ.user.js"; + +function getChannelUrl(channel) { + return channel === "preview" ? PREVIEW_URL : STABLE_URL; +} + +function downloadText(url) { + return new Promise((resolve, reject) => { + const req = https.get(url, { timeout: 15000 }, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode} while downloading ${url}`)); + res.resume(); + return; + } + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + }); + + req.on("timeout", () => { + req.destroy(new Error(`Timeout while downloading ${url}`)); + }); + req.on("error", reject); + }); +} + +function extractMetaValue(scriptText, key) { + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(`^\\s*//\\s*@${escaped}\\s+(.+)$`, "m"); + const match = scriptText.match(regex); + return match ? match[1].trim() : ""; +} + +function extractVersion(scriptText) { + return extractMetaValue(scriptText, "version"); +} + +function extractName(scriptText) { + return extractMetaValue(scriptText, "name") || "XMOJ"; +} + +function extractRequires(scriptText) { + const lines = scriptText.split(/\r?\n/); + const list = []; + for (const line of lines) { + const match = line.match(/^\s*\/\/\s*@require\s+(.+)$/); + if (match) { + list.push(match[1].trim()); + } + } + return list; +} + +function isNewerVersion(currentVersion, remoteVersion) { + const c = currentVersion.split(".").map((n) => Number.parseInt(n, 10) || 0); + const r = remoteVersion.split(".").map((n) => Number.parseInt(n, 10) || 0); + const maxLen = Math.max(c.length, r.length); + for (let i = 0; i < maxLen; i += 1) { + const cv = c[i] ?? 0; + const rv = r[i] ?? 0; + if (rv > cv) return true; + if (rv < cv) return false; + } + return false; +} + +module.exports = { + STABLE_URL, + PREVIEW_URL, + getChannelUrl, + downloadText, + extractVersion, + extractName, + extractRequires, + isNewerVersion +}; From 48fe7849449f93e0c373d16943995073f9c75359 Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Thu, 19 Mar 2026 22:14:22 +0800 Subject: [PATCH 02/22] =?UTF-8?q?=E5=A5=BD=E7=9A=84=E7=8E=B0=E5=9C=A8?= =?UTF-8?q?=E7=BB=88=E4=BA=8E=E6=9C=89turnstile=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/preload.js | 190 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 183 insertions(+), 7 deletions(-) diff --git a/src/preload.js b/src/preload.js index 0fc81d9..85a75d4 100644 --- a/src/preload.js +++ b/src/preload.js @@ -314,18 +314,114 @@ function setupTurnstileCallbackBridge() { window[ELXMOJ_TURNSTILE_BRIDGE_INSTALLED_KEY] = true; + const callbackStore = new Map(); + let callbackCounter = 0; + + const toPlainObject = (value) => { + if (!value || typeof value !== "object") { + return {}; + } + + const result = {}; + for (const [key, item] of Object.entries(value)) { + if (typeof item === "function") { + continue; + } + result[key] = item; + } + return result; + }; + + const bridgedTurnstile = { + render: (target, options = {}) => { + const safeOptions = toPlainObject(options); + const callbackId = `elxmoj_turnstile_cb_${Date.now()}_${++callbackCounter}`; + if (typeof options?.callback === "function") { + callbackStore.set(callbackId, options.callback); + } + + window.postMessage( + { + __ELXMOJ_TURNSTILE_RENDER__: true, + target, + options: safeOptions, + callbackId + }, + "*" + ); + + return callbackId; + }, + reset: (widgetId) => { + window.postMessage( + { + __ELXMOJ_TURNSTILE_RESET__: true, + widgetId + }, + "*" + ); + }, + remove: (widgetId) => { + window.postMessage( + { + __ELXMOJ_TURNSTILE_REMOVE__: true, + widgetId + }, + "*" + ); + } + }; + + if (!window.turnstile || typeof window.turnstile !== "object") { + window.turnstile = {}; + } + + if (typeof window.turnstile.render !== "function") { + window.turnstile.render = bridgedTurnstile.render; + } + if (typeof window.turnstile.reset !== "function") { + window.turnstile.reset = bridgedTurnstile.reset; + } + if (typeof window.turnstile.remove !== "function") { + window.turnstile.remove = bridgedTurnstile.remove; + } + window.addEventListener("message", (event) => { const payload = event.data; - if (!payload || payload.__ELXMOJ_TURNSTILE_CALLBACK__ !== true) { + if (!payload || typeof payload !== "object") { return; } - try { - if (typeof window.CaptchaLoadedCallback === "function") { - window.CaptchaLoadedCallback(...(payload.args || [])); + if (payload.__ELXMOJ_TURNSTILE_CALLBACK__ === true) { + try { + if (typeof window.CaptchaLoadedCallback === "function") { + window.CaptchaLoadedCallback(...(payload.args || [])); + } + } catch (error) { + console.error("ELXMOJ turnstile callback bridge error:", error); } - } catch (error) { - console.error("ELXMOJ turnstile callback bridge error:", error); + return; + } + + if (payload.__ELXMOJ_TURNSTILE_TOKEN__ === true) { + const callbackId = String(payload.callbackId || ""); + if (!callbackId) { + return; + } + + const callback = callbackStore.get(callbackId); + if (typeof callback === "function") { + try { + callback(String(payload.token || "")); + } catch (error) { + console.error("ELXMOJ turnstile token callback error:", error); + } + } + return; + } + + if (payload.__ELXMOJ_TURNSTILE_ERROR__ === true) { + console.warn("ELXMOJ turnstile page render failed:", payload.message || "unknown error"); } }); @@ -333,15 +429,95 @@ function setupTurnstileCallbackBridge() { const script = document.createElement("script"); script.textContent = ` (function () { - if (typeof window.CaptchaLoadedCallback === "function") { + if (window.__ELXMOJ_TURNSTILE_PAGE_BRIDGE__ === true) { return; } + window.__ELXMOJ_TURNSTILE_PAGE_BRIDGE__ = true; + + var originalCaptchaLoadedCallback = + typeof window.CaptchaLoadedCallback === "function" ? window.CaptchaLoadedCallback : null; window.CaptchaLoadedCallback = function () { + if (typeof originalCaptchaLoadedCallback === "function") { + try { + originalCaptchaLoadedCallback.apply(window, arguments); + } catch (error) { + console.error("ELXMOJ page original CaptchaLoadedCallback error:", error); + } + } window.postMessage({ __ELXMOJ_TURNSTILE_CALLBACK__: true, args: Array.prototype.slice.call(arguments) }, "*"); }; + + var renderWithBridge = function (target, options, callbackId) { + if (!window.turnstile || typeof window.turnstile.render !== "function") { + return false; + } + + var finalOptions = options && typeof options === "object" ? Object.assign({}, options) : {}; + finalOptions.callback = function (token) { + window.postMessage( + { + __ELXMOJ_TURNSTILE_TOKEN__: true, + callbackId: callbackId, + token: String(token || "") + }, + "*" + ); + }; + + try { + window.turnstile.render(target, finalOptions); + return true; + } catch (error) { + window.postMessage( + { + __ELXMOJ_TURNSTILE_ERROR__: true, + message: String((error && error.message) || error || "turnstile.render failed") + }, + "*" + ); + return true; + } + }; + + window.addEventListener("message", function (event) { + var payload = event && event.data; + if (!payload || typeof payload !== "object") { + return; + } + + if (payload.__ELXMOJ_TURNSTILE_RENDER__ === true) { + if (!renderWithBridge(payload.target, payload.options, payload.callbackId)) { + window.setTimeout(function () { + renderWithBridge(payload.target, payload.options, payload.callbackId); + }, 150); + } + return; + } + + if (payload.__ELXMOJ_TURNSTILE_RESET__ === true) { + if (window.turnstile && typeof window.turnstile.reset === "function") { + try { + window.turnstile.reset(payload.widgetId); + } catch { + // ignore turnstile reset failures + } + } + return; + } + + if (payload.__ELXMOJ_TURNSTILE_REMOVE__ === true) { + if (window.turnstile && typeof window.turnstile.remove === "function") { + try { + window.turnstile.remove(payload.widgetId); + } catch { + // ignore turnstile remove failures + } + } + } + }); })(); `; (document.documentElement || document.head || document.body).appendChild(script); From 92c76e009540e790c19c442a81e79cc20430bac7 Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Thu, 19 Mar 2026 22:19:07 +0800 Subject: [PATCH 03/22] fix problem of new page not rendering script --- src/main.js | 58 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/main.js b/src/main.js index ca5414d..92f8976 100644 --- a/src/main.js +++ b/src/main.js @@ -24,6 +24,49 @@ let lastCheckResult = null; const LOCAL_SCRIPT_PATH = path.join(__dirname, "..", "XMOJ.user.js"); const XMOJ_HOME = "https://www.xmoj.tech"; +const PRELOAD_PATH = path.join(__dirname, "preload.js"); + +function createAppWebPreferences() { + return { + preload: PRELOAD_PATH, + contextIsolation: true, + nodeIntegration: false, + sandbox: false + }; +} + +function getPopupWindowOptions() { + return { + width: 1280, + height: 820, + minWidth: 980, + minHeight: 640, + title: "ELXMOJ", + webPreferences: createAppWebPreferences() + }; +} + +function attachPopupInjectionBehavior(targetWindow) { + if (!targetWindow || targetWindow.isDestroyed()) { + return; + } + + targetWindow.webContents.setWindowOpenHandler(({ url }) => { + const nextUrl = String(url || ""); + if (!/^https?:\/\//i.test(nextUrl)) { + return { action: "deny" }; + } + + return { + action: "allow", + overrideBrowserWindowOptions: getPopupWindowOptions() + }; + }); + + targetWindow.webContents.on("did-create-window", (childWindow) => { + attachPopupInjectionBehavior(childWindow); + }); +} async function getPhpSessionIdFromCookieStore() { try { @@ -222,14 +265,10 @@ function createMainWindow() { minWidth: 1100, minHeight: 680, title: "ELXMOJ", - webPreferences: { - preload: path.join(__dirname, "preload.js"), - contextIsolation: true, - nodeIntegration: false, - sandbox: false - } + webPreferences: createAppWebPreferences() }); + attachPopupInjectionBehavior(mainWindow); mainWindow.loadURL(XMOJ_HOME); mainWindow.on("closed", () => { mainWindow = null; @@ -252,12 +291,7 @@ function openSettingsWindow() { title: "ELXMOJ 设置", parent: mainWindow || undefined, modal: Boolean(mainWindow), - webPreferences: { - preload: path.join(__dirname, "preload.js"), - contextIsolation: true, - nodeIntegration: false, - sandbox: false - } + webPreferences: createAppWebPreferences() }); settingsWindow.loadFile(path.join(__dirname, "settings.html")); From 5c6b89039d74237710bde35c47a8d18bb3fdf7e7 Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Thu, 19 Mar 2026 22:22:16 +0800 Subject: [PATCH 04/22] capture keyboard keys --- src/main.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/main.js b/src/main.js index 92f8976..c94bb3c 100644 --- a/src/main.js +++ b/src/main.js @@ -46,11 +46,78 @@ function getPopupWindowOptions() { }; } +function attachBrowserShortcutBehavior(targetWindow) { + if (!targetWindow || targetWindow.isDestroyed()) { + return; + } + + const webContents = targetWindow.webContents; + if (!webContents || webContents.__ELXMOJ_SHORTCUTS_ATTACHED__) { + return; + } + + webContents.__ELXMOJ_SHORTCUTS_ATTACHED__ = true; + + webContents.on("before-input-event", (event, input) => { + if (!input || input.type !== "keyDown") { + return; + } + + const key = String(input.key || ""); + const normalizedKey = key.length === 1 ? key.toLowerCase() : key; + const hasMeta = Boolean(input.meta); + const hasCtrlOrMeta = Boolean(input.control || input.meta); + const hasShift = Boolean(input.shift); + const hasAlt = Boolean(input.alt); + + const isHardReload = + (normalizedKey === "F5" && hasShift) || + (hasCtrlOrMeta && hasShift && normalizedKey === "r"); + if (isHardReload) { + event.preventDefault(); + webContents.reloadIgnoringCache(); + return; + } + + const isReload = normalizedKey === "F5" || (hasCtrlOrMeta && normalizedKey === "r"); + if (isReload) { + event.preventDefault(); + webContents.reload(); + return; + } + + const isGoBack = + (hasAlt && normalizedKey === "ArrowLeft") || + normalizedKey === "BrowserBack" || + (hasMeta && !hasShift && normalizedKey === "["); + if (isGoBack) { + event.preventDefault(); + if (webContents.canGoBack()) { + webContents.goBack(); + } + return; + } + + const isGoForward = + (hasAlt && normalizedKey === "ArrowRight") || + normalizedKey === "BrowserForward" || + (hasMeta && !hasShift && normalizedKey === "]"); + if (isGoForward) { + event.preventDefault(); + if (webContents.canGoForward()) { + webContents.goForward(); + } + } + }); +} + function attachPopupInjectionBehavior(targetWindow) { if (!targetWindow || targetWindow.isDestroyed()) { return; } + attachBrowserShortcutBehavior(targetWindow); + targetWindow.webContents.setWindowOpenHandler(({ url }) => { const nextUrl = String(url || ""); if (!/^https?:\/\//i.test(nextUrl)) { @@ -268,6 +335,7 @@ function createMainWindow() { webPreferences: createAppWebPreferences() }); + attachBrowserShortcutBehavior(mainWindow); attachPopupInjectionBehavior(mainWindow); mainWindow.loadURL(XMOJ_HOME); mainWindow.on("closed", () => { @@ -294,6 +362,7 @@ function openSettingsWindow() { webPreferences: createAppWebPreferences() }); + attachBrowserShortcutBehavior(settingsWindow); settingsWindow.loadFile(path.join(__dirname, "settings.html")); settingsWindow.on("closed", () => { settingsWindow = null; From 90329416e2426a39181a94f812edc2bd076bc780 Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Thu, 19 Mar 2026 22:32:04 +0800 Subject: [PATCH 05/22] Use npm install in code-check workflow Update .github/workflows/code-check.yml: remove the actions/setup-node cache setting and replace `npm ci` with `npm install` for the Install dependencies step. This changes how dependencies are installed in the code-check job, accommodating environments where `npm ci` or the previous cache configuration was problematic. --- .github/workflows/code-check.yml | 3 +-- .github/workflows/release-build.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 4d45191..263f8cb 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -21,10 +21,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - cache: npm - name: Install dependencies - run: npm ci + run: npm install - name: Run lint and syntax check run: npm run check diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 6728175..ade14c3 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -35,7 +35,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - cache: npm - name: Install Linux build dependencies if: runner.os == 'Linux' @@ -44,7 +43,7 @@ jobs: sudo apt-get install -y libarchive-tools rpm - name: Install dependencies - run: npm ci + run: npm install - name: Run code check run: npm run check From 1cc66df59bfe33cdda420095ca2ba542d4600074 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Thu, 19 Mar 2026 22:33:07 +0800 Subject: [PATCH 06/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main.js | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main.js b/src/main.js index c94bb3c..ed692dd 100644 --- a/src/main.js +++ b/src/main.js @@ -509,10 +509,27 @@ async function checkForScriptUpdate({ showNoUpdateDialog = false } = {}) { return { updated: true, currentVersion, remoteVersion }; } +function isTrustedIpcSender(event) { + try { + const url = event?.senderFrame?.url || ""; + if (!url) { + return false; + } + + const allowedPrefixes = ["file://", "app://"]; + return allowedPrefixes.some((prefix) => url.startsWith(prefix)); + } catch { + return false; + } +} + function registerIpcHandlers() { ipcMain.handle("elxmoj:get-settings", async () => getSettings()); - ipcMain.handle("elxmoj:update-settings", async (_event, patch) => { + ipcMain.handle("elxmoj:update-settings", async (event, patch) => { + if (!isTrustedIpcSender(event)) { + throw new Error("Unauthorized IPC sender"); + } const current = await getSettings(); const next = { ...current, ...patch }; await setSettings(next); @@ -529,8 +546,18 @@ function registerIpcHandlers() { }; }); - ipcMain.handle("elxmoj:check-update", async () => checkForScriptUpdate({ showNoUpdateDialog: true })); - ipcMain.handle("elxmoj:run-self-check", async () => runSelfCheck(true)); + ipcMain.handle("elxmoj:check-update", async (event) => { + if (!isTrustedIpcSender(event)) { + throw new Error("Unauthorized IPC sender"); + } + return checkForScriptUpdate({ showNoUpdateDialog: true }); + }); + ipcMain.handle("elxmoj:run-self-check", async (event) => { + if (!isTrustedIpcSender(event)) { + throw new Error("Unauthorized IPC sender"); + } + return runSelfCheck(true); + }); ipcMain.handle("elxmoj:get-last-self-check", async () => lastCheckResult); ipcMain.handle("elxmoj:get-phpsessid", async () => { From 0aaefbe17368d9d31f4993896d68f6a2c0a2479b Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Thu, 19 Mar 2026 22:33:24 +0800 Subject: [PATCH 07/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/settings.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/settings.js b/src/settings.js index 51e1a61..21e0a68 100644 --- a/src/settings.js +++ b/src/settings.js @@ -23,21 +23,33 @@ function setStatus(text) { } async function saveSettings() { - const patch = collectSettings(); - const next = await window.ELXMOJ.updateSettings(patch); - setStatus(`已保存\n通道: ${next.channel}\n启动更新检查: ${next.checkUpdateOnStartup}\n自动注入: ${next.autoInjectUserscript}`); + try { + const patch = collectSettings(); + const next = await window.ELXMOJ.updateSettings(patch); + setStatus(`已保存\n通道: ${next.channel}\n启动更新检查: ${next.checkUpdateOnStartup}\n自动注入: ${next.autoInjectUserscript}`); + } catch (error) { + setStatus(`保存设置失败: ${String(error)}`); + } } async function checkUpdateNow() { setStatus("正在检查更新..."); - const result = await window.ELXMOJ.checkUpdate(); - setStatus(`更新检查结果:\n${JSON.stringify(result, null, 2)}`); + try { + const result = await window.ELXMOJ.checkUpdate(); + setStatus(`更新检查结果:\n${JSON.stringify(result, null, 2)}`); + } catch (error) { + setStatus(`检查更新失败: ${String(error)}`); + } } async function runSelfCheckNow() { setStatus("正在执行自检..."); - const result = await window.ELXMOJ.runSelfCheck(); - setStatus(result.report || "自检完成"); + try { + const result = await window.ELXMOJ.runSelfCheck(); + setStatus(result.report || "自检完成"); + } catch (error) { + setStatus(`自检失败: ${String(error)}`); + } } document.getElementById("btnSave").addEventListener("click", saveSettings); From 00ca8d798d5afdef902df59c3acfce50acf769c8 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Thu, 19 Mar 2026 22:35:24 +0800 Subject: [PATCH 08/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/preload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/preload.js b/src/preload.js index 85a75d4..818fab4 100644 --- a/src/preload.js +++ b/src/preload.js @@ -895,11 +895,11 @@ async function injectUserscriptWhenReady() { return; } - if (!acquireInjectionLock()) { + if (!location.hostname.endsWith("xmoj.tech") && location.hostname !== "116.62.212.172") { return; } - if (!location.hostname.endsWith("xmoj.tech") && location.hostname !== "116.62.212.172") { + if (!acquireInjectionLock()) { return; } From 4aa644643b65c8c7b215c079fdc92511bd76858f Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Thu, 19 Mar 2026 22:37:21 +0800 Subject: [PATCH 09/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index ed692dd..83309aa 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,5 @@ const path = require("node:path"); -const { app, BrowserWindow, dialog, ipcMain, Menu, net, session } = require("electron"); +const { app, BrowserWindow, dialog, ipcMain, Menu, net, session, shell } = require("electron"); const { loadSettings, @@ -120,7 +120,31 @@ function attachPopupInjectionBehavior(targetWindow) { targetWindow.webContents.setWindowOpenHandler(({ url }) => { const nextUrl = String(url || ""); - if (!/^https?:\/\//i.test(nextUrl)) { + + let parsedTargetUrl; + try { + parsedTargetUrl = new URL(nextUrl); + } catch { + return { action: "deny" }; + } + + if (parsedTargetUrl.protocol !== "http:" && parsedTargetUrl.protocol !== "https:") { + return { action: "deny" }; + } + + let trustedOrigin = ""; + try { + trustedOrigin = new URL(XMOJ_HOME).origin; + } catch { + trustedOrigin = ""; + } + + const targetOrigin = parsedTargetUrl.origin; + + if (!trustedOrigin || targetOrigin !== trustedOrigin) { + shell.openExternal(nextUrl).catch(() => { + // Ignore failures to open external URLs + }); return { action: "deny" }; } From cde6c547ebef246fb0cb8db0de95fb2979219c4e Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Thu, 19 Mar 2026 22:41:33 +0800 Subject: [PATCH 10/22] Preload: fix setter return, regex, and GM API Make small cleanups in src/preload.js: remove the returned value from the cookie shim setter so the nativeSet result is not propagated; simplify the double-quote regex literal in the markdown fallback; and drop unused parameters from GM_registerMenuCommand (Electron provides menu commands, so the page-level registration is left as a no-op). These are minor refactors to tidy behavior and avoid unnecessary return/unused-arg usage. --- src/preload.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/preload.js b/src/preload.js index 818fab4..3adef2b 100644 --- a/src/preload.js +++ b/src/preload.js @@ -182,7 +182,7 @@ function installCookieVisibilityShim(initialPhpSessionId) { return raw ? `${raw}; PHPSESSID=${shadow}` : `PHPSESSID=${shadow}`; }, set(value) { - return nativeSet(value); + nativeSet(value); } }); window[ELXMOJ_COOKIE_SHIM_INSTALLED_KEY] = true; @@ -232,7 +232,7 @@ function setupMarkedFallback() { .replace(/&/g, "&") .replace(//g, ">") - .replace(/\"/g, """) + .replace(/"/g, """) .replace(/'/g, "'"); const parse = (markdown) => { @@ -665,7 +665,7 @@ function setupGmPolyfills(payload = null) { await navigator.clipboard.writeText(String(text ?? "")); }; - window.GM_registerMenuCommand = (_name, _callback) => { + window.GM_registerMenuCommand = () => { // Electron 版本先使用应用菜单提供命令,不在页面内重复注册。 }; From 67a56a0488720e720ff6995b721a47df0891f63e Mon Sep 17 00:00:00 2001 From: pythonSmall-Q Date: Thu, 19 Mar 2026 23:22:40 +0800 Subject: [PATCH 11/22] Add app icons and set window icons Add application icon assets (app.ico, app.png, favicon.ico) and include build/icons in packaged files. Configure platform-specific icons in package.json for Windows, macOS, and Linux. Update src/main.js to compute APP_ICON_PATH based on platform and set it on created BrowserWindow instances so the app windows use the provided icons. --- build/icons/app.ico | Bin 0 -> 15838 bytes build/icons/app.png | Bin 0 -> 15816 bytes favicon.ico | Bin 0 -> 15816 bytes package.json | 4 ++++ src/main.js | 10 ++++++++++ 5 files changed, 14 insertions(+) create mode 100644 build/icons/app.ico create mode 100644 build/icons/app.png create mode 100644 favicon.ico diff --git a/build/icons/app.ico b/build/icons/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..74d81080720fcdd297fd1b7087f100597e4aa8df GIT binary patch literal 15838 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 literal 0 HcmV?d00001 diff --git a/build/icons/app.png b/build/icons/app.png new file mode 100644 index 0000000000000000000000000000000000000000..23f636bd73db0d7e8cb9389d0cccb906742b5004 GIT binary patch literal 15816 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 Date: Fri, 20 Mar 2026 12:18:28 +0800 Subject: [PATCH 12/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/preload.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/preload.js b/src/preload.js index 3adef2b..4c08677 100644 --- a/src/preload.js +++ b/src/preload.js @@ -963,10 +963,22 @@ if (document.readyState === "loading") { injectUserscriptWhenReady(); } -contextBridge.exposeInMainWorld("ELXMOJ", { - getSettings: () => ipcRenderer.invoke("elxmoj:get-settings"), - updateSettings: (patch) => ipcRenderer.invoke("elxmoj:update-settings", patch), - checkUpdate: () => ipcRenderer.invoke("elxmoj:check-update"), - runSelfCheck: () => ipcRenderer.invoke("elxmoj:run-self-check"), - getLastSelfCheck: () => ipcRenderer.invoke("elxmoj:get-last-self-check") -}); +function isTrustedPreloadContext() { + try { + // Only expose the ELXMOJ bridge to local app pages (e.g., settings UI), + // and not to remote web content loaded over http/https. + return window.location && window.location.protocol === "file:"; + } catch { + return false; + } +} + +if (isTrustedPreloadContext()) { + contextBridge.exposeInMainWorld("ELXMOJ", { + getSettings: () => ipcRenderer.invoke("elxmoj:get-settings"), + updateSettings: (patch) => ipcRenderer.invoke("elxmoj:update-settings", patch), + checkUpdate: () => ipcRenderer.invoke("elxmoj:check-update"), + runSelfCheck: () => ipcRenderer.invoke("elxmoj:run-self-check"), + getLastSelfCheck: () => ipcRenderer.invoke("elxmoj:get-last-self-check") + }); +} From f5fcb6fc48680619e68bed18270288ef83d06621 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Fri, 20 Mar 2026 12:19:15 +0800 Subject: [PATCH 13/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 7088f44..be0f02c 100644 --- a/src/main.js +++ b/src/main.js @@ -558,7 +558,12 @@ function isTrustedIpcSender(event) { } function registerIpcHandlers() { - ipcMain.handle("elxmoj:get-settings", async () => getSettings()); + ipcMain.handle("elxmoj:get-settings", async (event) => { + if (!isTrustedIpcSender(event)) { + throw new Error("Unauthorized IPC sender"); + } + return getSettings(); + }); ipcMain.handle("elxmoj:update-settings", async (event, patch) => { if (!isTrustedIpcSender(event)) { From 8680f222f8856bf87ec24ff7956c4b71736fed35 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Fri, 20 Mar 2026 12:19:25 +0800 Subject: [PATCH 14/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index be0f02c..631d1fe 100644 --- a/src/main.js +++ b/src/main.js @@ -597,7 +597,12 @@ function registerIpcHandlers() { } return runSelfCheck(true); }); - ipcMain.handle("elxmoj:get-last-self-check", async () => lastCheckResult); + ipcMain.handle("elxmoj:get-last-self-check", async (event) => { + if (!isTrustedIpcSender(event)) { + throw new Error("Unauthorized IPC sender"); + } + return lastCheckResult; + }); ipcMain.handle("elxmoj:get-phpsessid", async () => { const value = await getPhpSessionIdFromCookieStore(); From 136ac09edc93a15e5d623fca2b52119b330efcda Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Fri, 20 Mar 2026 12:19:40 +0800 Subject: [PATCH 15/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 631d1fe..7077ebd 100644 --- a/src/main.js +++ b/src/main.js @@ -17,6 +17,8 @@ const { isNewerVersion } = require("./updater"); +const ALLOWED_GM_XHR_HOSTS = new Set(["www.xmoj.tech"]); + let mainWindow = null; let settingsWindow = null; let settingsCache = null; @@ -609,7 +611,11 @@ function registerIpcHandlers() { return value || ""; }); - ipcMain.handle("elxmoj:gm-xhr", async (_event, request) => { + ipcMain.handle("elxmoj:gm-xhr", async (event, request) => { + if (!isTrustedIpcSender(event)) { + throw new Error("Unauthorized IPC sender"); + } + const req = request || {}; const url = String(req.url || ""); if (!url) { @@ -619,6 +625,23 @@ function registerIpcHandlers() { }; } + let parsedUrl; + try { + parsedUrl = new URL(url); + } catch { + return { + ok: false, + error: "GM_xmlhttpRequest requires a valid URL" + }; + } + + if (!["http:", "https:"].includes(parsedUrl.protocol) || !ALLOWED_GM_XHR_HOSTS.has(parsedUrl.hostname)) { + return { + ok: false, + error: "GM_xmlhttpRequest URL is not allowed" + }; + } + const method = String(req.method || "GET").toUpperCase(); const headers = req.headers && typeof req.headers === "object" ? req.headers : {}; const timeout = Number.isFinite(req.timeout) ? Number(req.timeout) : 20000; From 66408997cbe18d5a9da5cf08906597ce5a210c98 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Fri, 20 Mar 2026 12:19:55 +0800 Subject: [PATCH 16/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/preload.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/preload.js b/src/preload.js index 4c08677..b9ee652 100644 --- a/src/preload.js +++ b/src/preload.js @@ -847,7 +847,16 @@ function executeRequireScriptInCurrentContext(source, url) { throw new Error(`Empty script body for @require: ${url}`); } - vm.runInThisContext(`${code}\n//# sourceURL=${url}`); + // Run third-party @require code in a sandboxed VM context without Node/Electron globals. + const sandbox = { + console + }; + const context = vm.createContext(sandbox); + const script = new vm.Script(`${code}\n//# sourceURL=${url}`, { + filename: url, + displayErrors: true + }); + script.runInContext(context); } async function loadScriptInCurrentContext(url) { From ec3d072436c1a9759246194877a8fd37fa6876d8 Mon Sep 17 00:00:00 2001 From: Shan Wenxiao Date: Fri, 20 Mar 2026 12:20:12 +0800 Subject: [PATCH 17/22] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/preload.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/preload.js b/src/preload.js index b9ee652..10455a0 100644 --- a/src/preload.js +++ b/src/preload.js @@ -945,9 +945,13 @@ async function injectUserscriptWhenReady() { setupTurnstileCallbackBridge(); setupHexMd5Polyfill(); - // 在页面上下文执行 userscript。 - const runner = new Function(payload.scriptText); - runner(); + // 在页面上下文执行 userscript:通过注入