"
}
}
}
\ No newline at end of file
diff --git a/XMOJ.user.js b/XMOJ.user.js
index 494f9a4ea..bddb25baa 100644
--- a/XMOJ.user.js
+++ b/XMOJ.user.js
@@ -1,6 +1,6 @@
// ==UserScript==
// @name XMOJ
-// @version 3.7.0
+// @version 3.8.0
// @description XMOJ增强脚本
// @author @XMOJ-Script-dev, @langningchen and the community
// @namespace https://github/langningchen
@@ -44,6 +44,90 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
+// The /web application owns its DOM. Never run the legacy body.innerHTML rewrites
+// inside it; InitializeContestWebApp maps its Bootstrap 3 markup to Bootstrap 5.
+function IsContestWebApp(pathname = location.pathname) {
+ return pathname === "/web" || pathname.startsWith("/web/");
+}
+
+function GetContestRoute(url = location.href) {
+ const path = new URL(url, location.origin).pathname;
+ const match = path.match(/^\/web\/contest(?:\/(\d+)(?:\/([^/]+)(?:\/(std|solution))?)?)?\/?$/);
+ if (!match) return null;
+ const cid = match[1] || null;
+ const num = match[2] ? decodeURIComponent(match[2]) : null;
+ return {cid, num: num === "rank-correct" ? null : num,
+ page: !cid ? "list" : num === "rank-correct" ? "rank" : match[3] || (num ? "problem" : "contest")};
+}
+
+function GetContestProblemURL(cid, num) {
+ return "/web/contest/" + encodeURIComponent(cid) + (num == null ? "" : "/" + encodeURIComponent(num));
+}
+
+function GetContestProblemIndex(num) {
+ // XMOJ's new API uses the letter; submit.php still uses its zero-based index.
+ return typeof num === "string" && num.length === 1 ? num.charCodeAt(0) - 65 : -1;
+}
+
+async function FetchContestAPI(path, signal) {
+ const response = await fetch("/api/" + path, {credentials: "same-origin", signal});
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.detail || "无法读取比赛数据 (" + response.status + ")");
+ return data;
+}
+
+function CacheContestProblems(cid, data) {
+ if (!Array.isArray(data.problems)) return;
+ const prefix = "UserScript-Contest-" + cid;
+ if (data.contest) localStorage.setItem(prefix + "-Name", data.contest.title || "");
+ for (const problem of data.problems) {
+ const index = GetContestProblemIndex(problem.num);
+ if (index >= 0 && problem.problemId) {
+ localStorage.setItem(prefix + "-Problem-" + index + "-PID", problem.problemId);
+ localStorage.setItem("UserScript-Problem-" + problem.problemId + "-Name", problem.problemTitle || problem.title || problem.num);
+ }
+ }
+}
+
+// The contest pages moved to the /web app and the site no longer links to the old
+// ones. Returns the new address for an old contest page, or null.
+function GetContestWebRedirect(url = location.href) {
+ const address = new URL(url, location.origin);
+ const cid = address.searchParams.get("cid");
+ const validCid = cid !== null && /^\d+$/.test(cid);
+ const pid = address.searchParams.get("pid");
+ const num = pid !== null && /^\d+$/.test(pid) && Number(pid) < 26 ? String.fromCharCode(65 + Number(pid)) : null;
+ let target = null;
+ if (address.pathname === "/contest.php") {
+ if (cid === null) {
+ const page = address.searchParams.get("page");
+ target = "/web/contest" + (page ? "?page=" + encodeURIComponent(page) : "");
+ } else if (validCid) {
+ target = GetContestProblemURL(cid);
+ }
+ } else if (address.pathname === "/contestrank-correct.php" && validCid) {
+ const user = address.searchParams.get("user_id");
+ target = GetContestProblemURL(cid) + "/rank-correct" + (user ? "?user_id=" + encodeURIComponent(user) : "");
+ } else if (validCid && num) {
+ if (address.pathname === "/problem.php") target = GetContestProblemURL(cid, num);
+ else if (address.pathname === "/problem_std.php") target = GetContestProblemURL(cid, num) + "/std";
+ else if (address.pathname === "/problem_solution.php") target = GetContestProblemURL(cid, num) + "/solution";
+ }
+ return target === null ? null : new URL(target, address.origin).href;
+}
+
+async function GetContestProblemData(cid, index, signal) {
+ if (!/^\d+$/.test(String(cid)) || !/^\d+$/.test(String(index))) throw new Error("无效的比赛题号");
+ const num = String.fromCharCode(65 + Number(index));
+ const data = await FetchContestAPI("contest/" + encodeURIComponent(cid) + "/" + encodeURIComponent(num), signal);
+ if (!data.problem || !data.problem.problemId) throw new Error("无法确定原题题号");
+ CacheContestProblems(cid, data);
+ localStorage.setItem("UserScript-Contest-" + cid + "-Problem-" + index + "-PID", data.problem.problemId);
+ localStorage.setItem("UserScript-Problem-" + data.problem.problemId + "-Name", data.problem.title || "");
+ localStorage.setItem("UserScript-Problem-" + data.problem.problemId + "-IOFilename", data.problem.name || "");
+ return data.problem;
+}
+
const MonochromeSkinCSS = `
/* Fonts loaded via to avoid layout shift */
@@ -250,6 +334,12 @@ const MonochromeSkinCSS = `
letter-spacing: 0.05em !important;
font-size: 0.85rem !important;
}
+ /* Header cells are inverted, so their links must be too: the global link
+ color would otherwise match the header background. */
+ thead th a, thead td a, th.header a, th.headerSortUp a, th.headerSortDown a {
+ color: var(--mono-white) !important;
+ border-bottom-color: var(--mono-white) !important;
+ }
td, th {
border-color: var(--mono-gray-300) !important;
text-align: center !important;
@@ -409,10 +499,6 @@ const MonochromeSkinCSS = `
display: none !important;
}
}
- .refreshList {
- cursor: pointer;
- }
-
/* Contain images */
img {
max-width: 100% !important;
@@ -476,13 +562,16 @@ const NewBootstrapSkinCSS = `
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;
}`;
+// Paints the theme's page background on the canvas, with a matching color-scheme,
+// so that while a page is hidden, and below short pages, a dark theme never shows
+// the browser's white default. Uses the active skin's background variable.
+const ThemeCanvasCSS = `
+ html[data-bs-theme='dark'] { background: var(--mono-white, var(--bs-body-bg, #1a1a1a)) !important; color-scheme: dark; }
+ html[data-bs-theme='light'] { background: var(--mono-white, var(--bs-body-bg, #fff)); color-scheme: light; }
+`;
+
// Set to true by the early block if Bootstrap CSS was injected from the @resource
// cache. Checked in the IIFE to decide whether a CDN fallback is needed.
let _earlyBootstrapInjected = false;
@@ -490,12 +579,29 @@ let _earlyBootstrapInjected = false;
// reveal and observer teardown at DOMContentLoaded time (more reliable than
// adding a DOMContentLoaded listener inside the sandboxed early block).
let _foucStyle = null;
+// Shows a page hidden by the early block. Called once the page reaches its styled
+// state (navbar converted, top bar applied), with a safety delay as a fallback.
+function RevealPage() {
+ if (_foucStyle) { _foucStyle.remove(); _foucStyle = null; }
+}
let _earlyObs = null;
// Runs synchronously at document-start. When NewBootstrap is enabled, we apply
// the saved theme and inject Bootstrap CSS + the skin CSS before the first paint,
// and we block the page's own old stylesheets from loading at all.
(() => {
+ // Old contest pages: move to the /web app before anything is drawn.
+ const ContestWebRedirect = GetContestWebRedirect();
+ if (ContestWebRedirect) {
+ ApplyContestWebTheme();
+ const Hide = document.createElement("style");
+ Hide.textContent = "body { visibility: hidden !important; }";
+ (document.head || document.documentElement).appendChild(Hide);
+ location.replace(ContestWebRedirect);
+ return;
+ }
+ // /web gets the same Bootstrap 5 and skin setup below as the legacy pages.
+ if (IsContestWebApp()) ApplyContestWebTheme();
try {
let get = (k) => {
let v = localStorage.getItem("UserScript-Setting-" + k);
@@ -526,15 +632,18 @@ let _earlyObs = null;
let skinCSS = isMono ? MonochromeSkinCSS : NewBootstrapSkinCSS;
if (get("AddAnimation")) skinCSS += `.status, .test-case { transition: ${isMono ? "100ms ease" : "0.5s"} !important; }`;
if (get("AddColorText")) skinCSS += `.red { color: red !important; } .green { color: green !important; } .blue { color: blue !important; }`;
+ skinCSS += ThemeCanvasCSS;
let skinStyle = document.createElement("style");
skinStyle.textContent = skinCSS;
head.appendChild(skinStyle);
- // Hide the page until old stylesheets are evicted and our CSS is in place.
- // Revealed by the IIFE right after its DOMContentLoaded wait (more reliable
- // than a DOMContentLoaded listener here due to sandbox context differences).
+ // Hide the page until old stylesheets are evicted and the page is styled; see
+ // RevealPage. Only the body is hidden: opacity on the root would also hide the
+ // themed canvas and show the browser's white default instead. Opacity rather than
+ // visibility, because innerText skips invisible text and handlers read it before
+ // the reveal (for example the scores on status.php).
_foucStyle = document.createElement("style");
- _foucStyle.textContent = "html { opacity: 0 !important; }";
+ _foucStyle.textContent = "body { opacity: 0 !important; }";
head.appendChild(_foucStyle);
let blocked = ["bootstrap.min.css", "white.css", "semantic.min.css", "bootstrap-theme.min.css", "problem.css"];
@@ -545,6 +654,10 @@ let _earlyObs = null;
node.remove();
});
_earlyObs.observe(document.documentElement, { childList: true, subtree: true });
+ // The script can start after the parser has already read : the observer
+ // only sees links added from now on, so remove the ones already there as well.
+ for (let link of document.querySelectorAll("link"))
+ if (blocked.some(h => link.href && link.href.indexOf(h) !== -1)) link.remove();
} catch (e) {
console.error("[XMOJ-Script] early init error:", e);
}
@@ -715,8 +828,14 @@ let GetUserInfo = async (Username) => {
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 Rating = (ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)") != null && ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)") != null) ? (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText.trim()) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText.trim())).toFixed(3) * 1000 : 0;
+ let Temp = (ParsedDocument.querySelector("#statics > tbody") != null) ? ParsedDocument.querySelector("#statics > tbody").children : [];
+ //页面结构异常(如403/404页面)时返回默认值,不写入缓存
+ if (Temp.length == 0) {
+ return {
+ "Rating": Rating, "EmailHash": undefined
+ }
+ }
let Email = Temp[Temp.length - 1].children[1].innerText.trim();
let EmailHash = CryptoJS.MD5(Email).toString();
localStorage.setItem("UserScript-User-" + Username + "-UserRating", Rating);
@@ -1674,29 +1793,6 @@ let PeriodicCloudSync = () => {
});
};
-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;
@@ -1999,68 +2095,699 @@ GM_registerMenuCommand("重置数据", () => {
}
});
-// Wrapped in an async IIFE so that `await` is valid in Violentmonkey,
-// which executes userscripts as classic scripts (not ES modules).
-(async () => {
-if (document.readyState === "loading") {
- await new Promise(r => document.addEventListener("DOMContentLoaded", r, { once: true }));
-}
-// Reveal the page now that DOMContentLoaded has fired. Remove any old Bootstrap
-// stylesheets the preload scanner fetched (un-applies them from the CSSOM), then
-// remove the FOUC hide so the user sees the correct final state immediately.
-if (_earlyObs) { _earlyObs.disconnect(); _earlyObs = null; }
-if (_foucStyle) {
- let _blocked = ["bootstrap.min.css", "white.css", "semantic.min.css", "bootstrap-theme.min.css", "problem.css"];
- for (let _link of document.querySelectorAll("link")) {
- if (_blocked.some(h => _link.href && _link.href.indexOf(h) !== -1)) _link.remove();
+// The contest problem switcher, fixed at the left edge, as on the old contest
+// problem pages. Labels come from each problem's URL, since the list order does not
+// have to follow the problem letters.
+function CreateProblemSwitcher(ProblemList, IsCurrent) {
+ 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.documentElement.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";
+ problemSwitcher.style.zIndex = "990";
+
+ for (let i = 0; i < ProblemList.length; i++) {
+ let Label = GetContestRoute(ProblemList[i].url)?.num;
+ if (!Label) Label = i < 26 ? String.fromCharCode(65 + i) : String.fromCharCode(97 + (i - 26));
+ let Button = document.createElement("a");
+ Button.href = ProblemList[i].url;
+ Button.title = String(ProblemList[i].title || "").trim();
+ Button.className = "btn btn-outline-secondary mb-2" + (IsCurrent(i, ProblemList[i]) ? " active" : "");
+ Button.textContent = Label;
+ problemSwitcher.appendChild(Button);
}
- _foucStyle.remove(); _foucStyle = null;
+ return problemSwitcher;
}
-//otherwise CurrentUsername might be undefined
-let loginStatus;
-await fetch("https://www.xmoj.tech/loginpage.php")
- .then((response) => response.text())
- .then((data) => (loginStatus = data));
-const logined = loginStatus == "Please logout First!";
-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";
- return;
+
+// The ResetType user menu. Shared by the legacy navbar and the /web app so both
+// menus offer the same entries and animate the same way.
+function CreateUserMenuItems() {
+ let Entries = [
+ ["修改帐号", () => { location.href = "https://www.xmoj.tech/modifypage.php"; }],
+ ["个人中心", () => { location.href = "https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername; }],
+ ["短消息", () => { location.href = "https://www.xmoj.tech/mail.php"; }],
+ ["插件设置", () => { location.href = "https://www.xmoj.tech/index.php?ByUserScript=1"; }],
+ ["插件更新日志", () => { location.href = "https://www.xmoj.tech/modifypage.php?ByUserScript=1"; }],
+ ["注销", () => {
+ 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";
+ }]
+ ];
+ return Entries.map(([Text, Action]) => {
+ let Item = document.createElement("li");
+ Item.className = "dropdown-item";
+ Item.innerText = Text;
+ Item.addEventListener("click", Action);
+ return Item;
+ });
}
-SearchParams = new URLSearchParams(location.search);
-let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-script.uk/" : "https://www.xmoj-script.uk")
-const profileElement = document.querySelector("#profile");
-if (profileElement === null) {
- if (!logined) {
- location.href = "https://www.xmoj.tech/loginpage.php";
- }
- return;
+function InitializeUserMenu(PopupUL, ParentLi, Items) {
+ PopupUL.style.cursor = 'pointer';
+ Items.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';
+ Items.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 = () => {
+ Items.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();
+ }
+ };
+ ParentLi.addEventListener("click", toggleDropdownItems);
+ document.addEventListener("click", (event) => {
+ if (!ParentLi.contains(event.target) && PopupUL.style.display === 'block') {
+ hideDropdownItems();
+ }
+ });
}
-CurrentUsername = profileElement.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");
-initTheme = () => {
+// Synchronous and dependency-free: paint the saved theme before Vue, API
+// responses, fonts, or any CDN resources are ready. Reused for later changes.
+// With NewBootstrap the early block gives /web the same Bootstrap 5 and skin CSS
+// as the legacy pages, so this only adds what the app's own markup still needs.
+function ApplyContestWebTheme() {
+ const get = name => {
+ const value = localStorage.getItem("UserScript-Setting-" + name);
+ return value === null ? !["DebugMode", "SuperDebug", "ReplaceXM"].includes(name) : value === "true";
+ };
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
+ let style = document.getElementById("xmoj-web-style");
+ if (!style) {
+ style = document.createElement("style");
+ style.id = "xmoj-web-style";
+ (document.head || document.documentElement).appendChild(style);
+ }
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);
+ const dark = saved === "auto" ? prefersDark.matches : saved === "dark";
+ document.documentElement.setAttribute("data-bs-theme", dark ? "dark" : "light");
+ localStorage.setItem("UserScript-Setting-DarkMode", String(dark));
+ const modern = get("NewBootstrap");
+ style.textContent = ThemeCanvasCSS + `
+ #app .xmoj-script-tools { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
+ #app .copy-btn { margin-left: 10px; }
+ #app .xmoj-script-countdown { margin-left: 8px; white-space: nowrap; }
+ #app .xmoj-script-editor { margin: 12px 0; }
+ #app .xmoj-script-has-editors .syntaxhighlighter,
+ #app .xmoj-script-has-editors .xmoj-std-code { display: none !important; }
+ #app .xmoj-std-overlay { pointer-events: none; }
+ #app .xmoj-script-code-ready { display: none !important; }
+ #app #rank td.well { color: #222 !important; }
+ #app .xmoj-problem-head h3 { font-size: 1rem; font-weight: inherit !important; font-family: inherit !important; margin: 0; }
+ #app .xmoj-problem-actions .btn { margin: 0 5px; }
+ #app .xmoj-problem-body pre { font-size: 1rem; padding: 0.3em 0.5em; margin: 0.5em 0; }
+ #app .in-out { overflow: hidden; display: flex; padding: 0.5em 0; }
+ #app .in-out .in-out-item { flex: 1; overflow: hidden; }
+ #app .cnt-row .title { font-weight: bolder; font-size: 1.1rem; }
+ #app a.copy-btn { float: right; margin-left: 0; padding: 0 0.4em; border: 1px solid var(--bs-primary, #337ab7); border-radius: 3px; color: var(--bs-primary, #337ab7); cursor: pointer; text-decoration: none; }
+ #app a.copy-btn:hover { background-color: var(--bs-secondary-bg, #f5f5f5); }
+ @media (max-width: 600px) { #app .in-out { flex-direction: column; } #app .in-out-item { margin: 0 !important; } }
+ #app .dropdown-menu[data-xmoj-script-menu] > li:not([data-xmoj-script]) { display: none !important; }
+ ` + (modern ? `
+ /* The app still renders Bootstrap 3 markup. Its classes are mapped to
+ Bootstrap 5 in InitializeContestWebApp; these cover the layout-only ones. */
+ #app .navbar-header { display: flex; align-items: center; justify-content: space-between; }
+ #app .navbar .icon-bar, #app .navbar .caret { display: none; }
+ #app .well { padding: 1rem; margin-bottom: 1rem; background-color: var(--bs-secondary-bg); border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius); }
+ #app .form-inline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
+ #app .form-inline .form-control { width: auto; }
+ ` : `
+ :root { --bs-secondary-bg: #f5f5f5; }
+ [data-bs-theme='dark'] { --bs-secondary-bg: #292929; }
+ [data-bs-theme='dark'] body, [data-bs-theme='dark'] #app .jumbotron,
+ [data-bs-theme='dark'] #app .navbar, [data-bs-theme='dark'] #app .dropdown-menu,
+ [data-bs-theme='dark'] #app .form-control, [data-bs-theme='dark'] #app pre {
+ background: #1a1a1a !important; color: #eee !important;
+ }
+ [data-bs-theme='dark'] #app a { color: #b8d6ff; }
+ [data-bs-theme='dark'] #app .pagination > li > a,
+ [data-bs-theme='dark'] #app .pagination > li > span,
+ [data-bs-theme='dark'] #app .well { background-color: #292929; color: #eee; }
+ [data-bs-theme='dark'] #app .navbar-default .navbar-nav > li > a,
+ [data-bs-theme='dark'] #app .dropdown-menu > li > a { color: #eee; }
+ [data-bs-theme='dark'] #app .navbar-nav > .active > a,
+ [data-bs-theme='dark'] #app .dropdown-menu > li > a:hover,
+ [data-bs-theme='dark'] #app .table-striped > tbody > tr:nth-of-type(odd),
+ [data-bs-theme='dark'] #app .table-hover > tbody > tr:hover { background: #292929; }
+ [data-bs-theme='dark'] #app .btn { background: #292929; color: #eee; border-color: #737373; }
+ `);
+ // The legacy problem switcher replaces the app's own problem bar.
+ if (get("ProblemSwitcher")) style.textContent += "#app .xmoj-problem-nav { display: none !important; }";
+ // Like the legacy pages, which remove the broadcast marquee.
+ if (get("RemoveUseless")) style.textContent += "#app .xmoj-broadcast { display: none !important; }";
+ // With NewBootstrap the early block already adds these to the skin CSS.
+ if (!modern && get("AddColorText")) style.textContent += ".red {color: #e64747;} .green {color: #399a39;} .blue {color: #428bca;}";
+ if (!modern && get("AddAnimation")) style.textContent += "#app .btn {transition: background-color 100ms, color 100ms;}";
+ return dark;
+}
+
+// Enhancements for the Vue contest app. Vue's elements and router links keep their
+// identity: only Bootstrap 5 classes and our own controls are added to them.
+async function InitializeContestWebApp() {
+ const root = document.getElementById("app");
+ if (!root) return;
+ const owned = "data-xmoj-script";
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
+ let routeKey = "";
+ let route = null;
+ let routeData = null;
+ let controller = null;
+ let scheduled = false;
+ let editors = [];
+ let serverOffset = 0;
+ let revealed = false;
+ const countdowns = new Map();
+
+ initTheme = () => {
+ const dark = ApplyContestWebTheme();
+ for (const entry of editors) entry.editor?._monacoEditor.updateOptions({theme: dark ? "vs-dark" : "vs"});
+ };
+ initTheme();
+ prefersDark.addEventListener("change", initTheme);
+ InitializeImageEnlarger();
+ if (UtilityEnabled("NewBootstrap") && !_earlyBootstrapInjected) {
+ // The @resource was not cached yet (first install/update): load Bootstrap 5
+ // and the skin the same way the legacy pages do.
+ const blocked = ["bootstrap.min.css", "white.css", "semantic.min.css", "bootstrap-theme.min.css", "problem.css"];
+ for (const link of document.querySelectorAll("link")) {
+ if (blocked.some(name => link.href && link.href.indexOf(name) !== -1)) link.remove();
+ }
+ const bootstrap = document.createElement("link");
+ bootstrap.rel = "stylesheet";
+ bootstrap.href = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css";
+ document.head.appendChild(bootstrap);
+ const mono = UtilityEnabled("MonochromeUI");
+ const skin = document.createElement("style");
+ skin.textContent = mono ? MonochromeSkinCSS : NewBootstrapSkinCSS;
+ if (UtilityEnabled("AddAnimation")) skin.textContent += `.status, .test-case { transition: ${mono ? "100ms ease" : "0.5s"} !important; }`;
+ if (UtilityEnabled("AddColorText")) skin.textContent += `.red { color: red !important; } .green { color: green !important; } .blue { color: blue !important; }`;
+ document.head.appendChild(skin);
+ }
+
+ function MakeControl(tag, name, text) {
+ const node = document.createElement(tag);
+ node.setAttribute(owned, name);
+ node.textContent = text;
+ if (tag === "button") {
+ node.type = "button";
+ node.className = "btn btn-outline-secondary";
+ }
+ return node;
+ }
+
+ function AddLink(parent, name, label, href, className = "btn btn-outline-secondary") {
+ if (parent.querySelector(`[${owned}="${name}"]`)) return;
+ const link = MakeControl("a", name, label);
+ link.href = href;
+ link.className = className;
+ parent.appendChild(link);
+ }
+
+ function AddCopy(parent, name, readText) {
+ if (parent.querySelector(`[${owned}="${name}"]`)) return;
+ const button = MakeControl("button", name, "复制");
+ button.className = "btn btn-sm btn-outline-secondary copy-btn";
+ button.addEventListener("click", () => {
+ GM_setClipboard(readText());
+ button.textContent = "复制成功";
+ setTimeout(() => { button.textContent = "复制"; }, 1000);
+ });
+ parent.appendChild(button);
+ }
+
+ // Mirrors the legacy navbar conversion in main(), plus the other Bootstrap 3
+ // classes the app renders. Runs on every render, because Vue rewrites the class
+ // attribute of elements whose classes are bound (e.g. the active tab).
+ function ApplyBootstrap5Markup() {
+ const nav = root.querySelector(".navbar");
+ if (nav) {
+ nav.classList.add("navbar-expand-lg", "bg-body-tertiary");
+ nav.querySelector("#xmoj-navbar > .navbar-nav:not(.navbar-right)")?.classList.add("me-auto", "mb-2", "mb-lg-0");
+ for (const item of nav.querySelectorAll(".navbar-nav > li")) {
+ item.classList.add("nav-item");
+ const link = item.firstElementChild;
+ if (!link) continue;
+ link.classList.add("nav-link");
+ link.classList.toggle("active", item.classList.contains("active"));
+ }
+ for (const toggle of nav.querySelectorAll('[data-toggle="dropdown"]')) {
+ toggle.setAttribute("data-bs-toggle", "dropdown");
+ toggle.removeAttribute("data-toggle");
+ toggle.parentElement.classList.add("dropdown");
+ }
+ const toggler = nav.querySelector(".navbar-toggle");
+ if (toggler) {
+ toggler.classList.add("navbar-toggler");
+ if (toggler.hasAttribute("data-toggle")) {
+ toggler.setAttribute("data-bs-toggle", "collapse");
+ toggler.setAttribute("data-bs-target", toggler.getAttribute("data-target") || "#xmoj-navbar");
+ toggler.removeAttribute("data-toggle");
+ toggler.removeAttribute("data-target");
+ }
+ if (!toggler.querySelector(".navbar-toggler-icon")) {
+ const icon = MakeControl("span", "toggler-icon", "");
+ icon.className = "navbar-toggler-icon";
+ toggler.appendChild(icon);
+ }
+ }
+ }
+ for (const node of root.querySelectorAll(".dropdown-menu > li > a")) node.classList.add("dropdown-item");
+ for (const node of root.querySelectorAll(".btn-default")) node.classList.add("btn-outline-secondary");
+ for (const node of root.querySelectorAll(".btn-xs")) node.classList.add("btn-sm");
+ for (const node of root.querySelectorAll(".sr-only")) node.classList.add("visually-hidden");
+ for (const node of root.querySelectorAll(".label")) node.classList.add("badge");
+ for (const node of root.querySelectorAll(".label-info")) node.classList.add("text-bg-info");
+ for (const item of root.querySelectorAll(".pagination > li")) {
+ item.classList.add("page-item");
+ item.firstElementChild?.classList.add("page-link");
+ }
+ }
+
+ function EnhanceNav() {
+ const nav = root.querySelector("#xmoj-navbar");
+ if (!nav) return;
+ const menu = nav.querySelector(".dropdown-menu");
+ if (menu && UtilityEnabled("ResetType")) {
+ if (!menu.hasAttribute("data-xmoj-script-menu")) {
+ // Same menu as the legacy navbar. Vue keeps rendering its own items, so
+ // they are hidden by CSS instead of replacing the list.
+ menu.setAttribute("data-xmoj-script-menu", "");
+ const items = CreateUserMenuItems();
+ for (const item of items) {
+ item.setAttribute(owned, "user-menu");
+ menu.appendChild(item);
+ }
+ InitializeUserMenu(menu, menu.parentElement, items);
+ }
+ } else if (menu && !menu.querySelector(`[${owned}="settings"]`)) {
+ const item = MakeControl("li", "settings", "");
+ const link = document.createElement("a");
+ link.href = "/index.php?ByUserScript=1";
+ link.textContent = "插件设置";
+ item.appendChild(link);
+ menu.appendChild(item);
+ }
+ const links = nav.querySelector("ul");
+ if (links && UtilityEnabled("Discussion") && !links.querySelector(`[${owned}="discussion-nav"]`)) {
+ const item = MakeControl("li", "discussion-nav", "");
+ const link = document.createElement("a");
+ link.href = "/discuss3/discuss.php";
+ link.textContent = "讨论";
+ item.appendChild(link);
+ links.appendChild(item);
+ }
+ if (UtilityEnabled("Translate")) {
+ // Same rename as the legacy navbar. Edit the text node Vue renders, so
+ // switching the app's language can still replace it.
+ const problems = nav.querySelector(".navbar-nav > li:nth-child(2) > a");
+ const text = problems && [...problems.childNodes].find(node => node.nodeType === Node.TEXT_NODE && node.nodeValue.trim() === "问题");
+ if (text) text.nodeValue = text.nodeValue.replace("问题", "题库");
+ }
+ }
+
+ function ClearRoute() {
+ controller?.abort();
+ countdowns.clear();
+ for (const entry of editors) {
+ entry.editor?.dispose();
+ entry.original?.classList.remove("xmoj-script-code-ready", "xmoj-script-has-editors");
+ }
+ editors = [];
+ root.querySelectorAll(`[${owned}]:not([${owned}="settings"]):not([${owned}="discussion-nav"]):not([${owned}="toggler-icon"]):not([${owned}="user-menu"])`).forEach(node => node.remove());
+ document.querySelectorAll(`[${owned}="problem-switcher"]`).forEach(node => node.remove());
+ routeData = null;
+ }
+
+ function AddCountdown(parent, endTime, label) {
+ if (!UtilityEnabled("AutoCountdown") || !parent || !Number.isFinite(endTime)) return;
+ let node = parent.querySelector(`[${owned}="countdown"]`);
+ if (!node) {
+ node = MakeControl("span", "countdown", "");
+ node.className = "xmoj-script-countdown";
+ parent.appendChild(node);
+ }
+ countdowns.set(node, {endTime, label});
+ }
+
+ function ParseServerTime(value) {
+ // The API returns China local time without an offset, regardless of browser timezone.
+ return Date.parse(String(value || "").replace(" ", "T") + "+08:00");
+ }
+
+ function UpdateCountdowns() {
+ for (const [node, item] of countdowns) {
+ if (!node.isConnected) { countdowns.delete(node); continue; }
+ const seconds = Math.max(0, Math.ceil((item.endTime - Date.now() - serverOffset) / 1000));
+ const days = Math.floor(seconds / 86400);
+ const hours = String(Math.floor(seconds / 3600) % 24).padStart(2, "0");
+ const minutes = String(Math.floor(seconds / 60) % 60).padStart(2, "0");
+ const text = item.label + (days ? days + "天 " : "") + hours + ":" + minutes + ":" + String(seconds % 60).padStart(2, "0");
+ if (node.textContent !== text) node.textContent = text;
+ }
+ }
+
+ async function LoadRouteData(signal, key, current) {
+ try {
+ let path = "contest/" + current.cid;
+ if (current.page === "list") {
+ const query = new URL(key).searchParams;
+ path = "contest/list?" + new URLSearchParams({pageNum: query.get("page") || "1", pageSize: "20", keyword: query.get("keyword") || ""});
+ } else if (current.page === "rank") {
+ // The native view owns sorting and incremental ranking requests.
+ return;
+ } else if (current.num) {
+ path += "/" + encodeURIComponent(current.num) + (current.page === "problem" ? "" : "/" + current.page);
+ }
+ const data = await FetchContestAPI(path, signal);
+ if (signal.aborted || location.href !== key) return;
+ routeData = data;
+ if (data.serverTime) serverOffset = ParseServerTime(data.serverTime) - Date.now();
+ if (!Number.isFinite(serverOffset)) serverOffset = 0;
+ if (current.cid) CacheContestProblems(current.cid, data);
+ if (data.problem?.problemId) {
+ const problem = data.problem;
+ localStorage.setItem("UserScript-Contest-" + current.cid + "-Problem-" + GetContestProblemIndex(current.num) + "-PID", problem.problemId);
+ localStorage.setItem("UserScript-Problem-" + problem.problemId + "-IOFilename", problem.name || "");
+ localStorage.setItem("UserScript-Problem-" + problem.problemId + "-Name", problem.title || "");
+ }
+ ScheduleEnhance();
+ } catch (error) {
+ if (!signal.aborted) console.error("[XMOJ-Script] Contest API:", error);
+ }
+ }
+
+ function EnhanceList() {
+ if (!routeData?.list) return;
+ for (const row of root.querySelectorAll(".xmoj-scroll-x tbody tr")) {
+ const link = row.querySelector('a[href*="/web/contest/"]');
+ if (!link) continue;
+ const cid = GetContestRoute(link.href)?.cid;
+ const data = routeData.list.find(item => String(item.contestId) === cid);
+ if (!data) continue;
+ localStorage.setItem("UserScript-Contest-" + cid + "-Name", data.title);
+ const status = data.status;
+ if (status?.state === "pending" || status?.state === "running") {
+ AddCountdown(row.cells[2], ParseServerTime(status.state === "pending" ? status.startTime : status.endTime), status.state === "pending" ? "距开始 " : "剩余 ");
+ }
+ const creator = row.cells[4];
+ if (creator && data.createdBy) {
+ // Like the legacy contest list, the creator's name becomes the link. The
+ // name is a text node Vue owns: blank it rather than removing it.
+ for (const node of creator.childNodes) {
+ if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim() === data.createdBy) node.nodeValue = "";
+ }
+ AddLink(creator, "creator", data.createdBy, "/userinfo.php?user=" + encodeURIComponent(data.createdBy), "");
+ }
+ }
+ }
+
+ function EnhanceContest() {
+ const table = root.querySelector(".xmoj-problems-table");
+ if (!table || !routeData?.problems) return;
+ const head = root.querySelector(".xmoj-contest-head");
+ const contest = routeData.contest;
+ const timeline = contest?.myTimeline;
+ const state = contest?.status?.state;
+ if (timeline?.enabled || state === "running" || state === "pending") {
+ AddCountdown(head, ParseServerTime(timeline?.enabled ? timeline.endTime : state === "pending" ? contest.startTime : contest.endTime), state === "pending" ? "距开始 " : "剩余 ");
+ }
+ let toolbar = root.querySelector(`[${owned}="contest-tools"]`);
+ if (!toolbar) {
+ toolbar = MakeControl("div", "contest-tools", "");
+ toolbar.className = "xmoj-script-tools";
+ table.parentElement.before(toolbar);
+ if (UtilityEnabled("OpenAllProblem")) {
+ for (const unsolved of [false, true]) {
+ const button = MakeControl("button", "open-problems", unsolved ? "打开未解决题目" : "打开全部题目");
+ button.addEventListener("click", () => {
+ for (const problem of routeData.problems) {
+ if (!unsolved || problem.myStatus !== "AC") window.open(GetContestProblemURL(route.cid, problem.num), "_blank", "noopener");
+ }
+ });
+ toolbar.appendChild(button);
+ }
+ }
+ if (UtilityEnabled("AutoCheat")) AddResubmitButton(toolbar);
+ }
+ if (UtilityEnabled("MoreSTD") && contest?.enableStd) {
+ for (const row of table.tBodies[0].rows) {
+ const link = row.querySelector('a[href*="/web/contest/"]');
+ const num = link && GetContestRoute(link.href)?.num;
+ if (!num || row.querySelector('a[href$="/std"]')) continue;
+ const headers = Array.from(table.tHead.rows[0].cells);
+ const index = headers.findIndex(cell => /标程|Std/.test(cell.textContent));
+ if (index >= 0) AddLink(row.cells[index], "std-link", "打开", GetContestProblemURL(route.cid, num) + "/std", "");
+ }
+ }
+ }
+
+ function AddResubmitButton(toolbar) {
+ const button = MakeControl("button", "resubmit", "自动提交当年代码");
+ toolbar.appendChild(button);
+ button.addEventListener("click", async () => {
+ if (!CurrentUsername || !routeData) return;
+ const current = route;
+ const signal = controller.signal;
+ const problems = routeData.problems;
+ button.disabled = true;
+ try {
+ for (const problem of problems) {
+ if (signal.aborted) return;
+ if (problem.myStatus === "AC" || !problem.problemId) continue;
+ // Only retrieve this user's accepted code, never another user's submission.
+ const query = new URLSearchParams({problem_id: problem.problemId, user_id: CurrentUsername, jresult: "4"});
+ const response = await fetch("/status.php?" + query, {signal});
+ if (!response.ok) throw new Error("无法读取提交记录");
+ const page = new DOMParser().parseFromString(await response.text(), "text/html");
+ const sid = page.querySelector("#result-tab > tbody > tr > td:nth-child(2)")?.textContent.trim();
+ if (!/^\d+$/.test(sid || "")) continue;
+ const sourceResponse = await fetch("/getsource.php?id=" + sid, {signal});
+ if (!sourceResponse.ok) throw new Error("无法读取代码");
+ const source = (await sourceResponse.text()).split("/**************************************************************")[0].trim();
+ if (!source || /^\s* setTimeout(resolve, 1000));
+ }
+ button.textContent = "处理完成,请刷新查看状态";
+ } catch (error) {
+ if (!signal.aborted) button.textContent = error.message;
+ } finally { if (!signal.aborted) button.disabled = false; }
+ });
+ }
+
+ function EnhanceProblem() {
+ const problem = routeData?.problem;
+ if (problem?.problemId) {
+ const heading = root.querySelector(".xmoj-problem-head h2");
+ if (heading && !heading.querySelector(`[${owned}="problem-id"]`)) heading.appendChild(MakeControl("small", "problem-id", " (" + problem.problemId + ")"));
+ if (UtilityEnabled("Discussion")) {
+ for (const actions of root.querySelectorAll(".xmoj-problem-actions")) {
+ AddLink(actions, "discuss-problem", "讨论", "/discuss3/discuss.php?pid=" + encodeURIComponent(problem.problemId));
+ }
+ }
+ }
+ // Same buttons as the legacy problem page: full size and all outlined.
+ for (const button of root.querySelectorAll(".xmoj-problem-actions .btn")) {
+ button.classList.remove("btn-sm", "btn-primary", "btn-default");
+ button.classList.add("btn-outline-secondary");
+ }
+ // The legacy page removes the repeated buttons below the statement.
+ if (UtilityEnabled("RemoveUseless")) {
+ for (const head of root.querySelectorAll(".xmoj-problem-head")) {
+ if (!head.querySelector("h2") && head.querySelector(".xmoj-problem-actions")) head.style.display = "none";
+ }
+ }
+ // Sample boxes are cards, as on the legacy page.
+ for (const pre of root.querySelectorAll(".xmoj-problem-body .in-out pre")) pre.classList.add("card");
+ if (UtilityEnabled("CopyMD")) InitializeProblemMarkdownCopy(root, root, AddCopy);
+ // Built from this page's API response rather than a cached copy.
+ if (UtilityEnabled("ProblemSwitcher") && Array.isArray(routeData?.problems) && !document.querySelector(`[${owned}="problem-switcher"]`)) {
+ const list = routeData.problems.map(item => ({title: item.problemTitle || item.title || item.num, url: new URL(GetContestProblemURL(route.cid, item.num), location.origin).href}));
+ if (list.length) {
+ const switcher = CreateProblemSwitcher(list, (index, item) => GetContestRoute(item.url)?.num === route.num);
+ switcher.setAttribute(owned, "problem-switcher");
+ // Switch problems through the app's own (hidden) router links, so the
+ // page does not reload.
+ switcher.addEventListener("click", event => {
+ const link = event.target.closest("a[href]");
+ const target = link && GetContestRoute(link.href);
+ const routerLink = target && [...root.querySelectorAll(".xmoj-problem-nav a")].find(a => GetContestRoute(a.href)?.num === target.num);
+ if (routerLink) {
+ event.preventDefault();
+ routerLink.click();
+ }
+ });
+ document.body.appendChild(switcher);
+ }
+ }
+ }
+
+ function AddCodeEditor(parent, original, code) {
+ if (editors.some(entry => entry.original === original && entry.code === code)) return;
+ const host = MakeControl("div", "code-editor", "");
+ host.className = "xmoj-script-editor";
+ const codeHost = document.createElement("div");
+ const height = Math.max(100, Math.min(600, code.split("\n").length * 20 + 24));
+ codeHost.style.height = height + "px";
+ host.appendChild(codeHost);
+ AddCopy(host, "copy-code", () => code);
+ parent.appendChild(host);
+ const entry = {original, host, code, editor: null};
+ editors.push(entry);
+ const signal = controller.signal;
+ createMonacoEditor(codeHost, {value: code, language: "cpp", readOnly: true, height: height + "px", automaticLayout: true}).then(editor => {
+ if (signal.aborted || !host.isConnected) { editor.dispose(); return; }
+ entry.editor = editor;
+ original.classList.add(original.classList.contains("xmoj-std-stage") ? "xmoj-script-has-editors" : "xmoj-script-code-ready");
+ }).catch(error => {
+ host.remove();
+ console.error("[XMOJ-Script] Read-only editor:", error);
+ });
+ }
+
+ function EnhanceCode() {
+ if (route.page === "std" && routeData?.stdList) {
+ const stage = root.querySelector(".xmoj-std-stage");
+ const body = stage?.querySelector(".jumbotron");
+ if (body) for (const code of routeData.stdList) AddCodeEditor(body, stage, code);
+ }
+ if (route.page === "solution") {
+ const body = root.querySelector(".xmoj-solution-body");
+ const heading = root.querySelector(".xmoj-problem-head h2");
+ if (body && heading && UtilityEnabled("CopyMD")) AddCopy(heading, "copy-solution", () => GetMDText(body).trim());
+ if (body) for (const code of body.querySelectorAll("pre.prettyprint")) AddCodeEditor(code.parentElement, code, code.textContent);
+ }
+ }
+
+ function Enhance() {
+ scheduled = false;
+ observer.disconnect();
+ try {
+ if (routeKey !== location.href) {
+ ClearRoute();
+ routeKey = location.href;
+ route = GetContestRoute();
+ controller = new AbortController();
+ if (route) void LoadRouteData(controller.signal, routeKey, route);
+ }
+ // Vue can replace a loaded view on the same route (e.g. retry/language switch).
+ editors = editors.filter(entry => {
+ if (entry.host.isConnected) return true;
+ entry.editor?.dispose();
+ entry.original?.classList.remove("xmoj-script-code-ready", "xmoj-script-has-editors");
+ return false;
+ });
+ if (UtilityEnabled("NewBootstrap")) ApplyBootstrap5Markup();
+ EnhanceNav();
+ if (!revealed && root.querySelector(".navbar")) {
+ // As on the legacy pages: top bar first, then show the page.
+ revealed = true;
+ if (UtilityEnabled("NewTopBar")) new NavbarStyler();
+ RevealPage();
+ }
+ if (!route) return;
+ if (route.page === "list") EnhanceList();
+ if (route.page === "contest") EnhanceContest();
+ if (route.page === "problem") EnhanceProblem();
+ if (route.page === "std" || route.page === "solution") EnhanceCode();
+ if (UtilityEnabled("NewBootstrap")) root.querySelector("#rank")?.classList.add("table", "table-hover");
+ if (UtilityEnabled("Translate")) {
+ const labels = {Rank: "排名", User: "用户", Nick: "昵称", Name: "姓名", Solved: "AC数", Mark: "得分"};
+ for (const cell of root.querySelectorAll("#rank thead th")) {
+ if (labels[cell.textContent]) cell.textContent = labels[cell.textContent];
+ }
+ }
+ UpdateCountdowns();
+ } finally { observer.observe(root, {childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["class"]}); }
}
-};
-initTheme();
-PeriodicCloudSync();
-setInterval(PeriodicCloudSync, 60 * 60 * 1000);
+ function ScheduleEnhance() {
+ if (!scheduled) { scheduled = true; requestAnimationFrame(Enhance); }
+ }
+ const observer = new MutationObserver(ScheduleEnhance);
+ Enhance();
+ window.addEventListener("popstate", ScheduleEnhance);
+ setInterval(() => {
+ UpdateCountdowns();
+ if (UtilityEnabled("NewTopBar")) new NavbarStyler();
+ }, 1000);
+ window.addEventListener("focus", () => {
+ if (!UtilityEnabled("AutoRefresh") || !["list", "contest", "rank"].includes(route?.page)) return;
+ if (root.querySelector('input:focus, textarea:focus') || root.querySelector(`[${owned}="resubmit"]:disabled`)) return;
+ // Reload lets Vue refresh both its data and sort state; editing its rendered
+ // table alone would cause stale rows to return on the next native sort.
+ location.reload();
+ });
+ // The app's #profile replacement arrives asynchronously. Authentication must
+ // come from /api/nav, never from the absence of the legacy DOM element.
+ try {
+ const nav = await FetchContestAPI("nav");
+ if (!nav.loggedIn) {
+ if (UtilityEnabled("AutoLogin")) {
+ localStorage.setItem("UserScript-LastPage", location.pathname + location.search + location.hash);
+ location.href = "/loginpage.php";
+ }
+ return;
+ }
+ CurrentUsername = nav.userId || "";
+ PeriodicCloudSync();
+ setInterval(PeriodicCloudSync, 60 * 60 * 1000);
+ } catch (error) { console.error("[XMOJ-Script] Navigation API:", error); }
+}
class NavbarStyler {
constructor() {
@@ -2205,80 +2932,80 @@ class NavbarStyler {
}
}
-function replaceMarkdownImages(text, string) {
- return text.replace(/!\[.*?\]\(.*?\)/g, string);
+// Wrapped in an async IIFE so that `await` is valid in Violentmonkey,
+// which executes userscripts as classic scripts (not ES modules).
+(async () => {
+if (GetContestWebRedirect()) return;
+if (document.readyState === "loading") {
+ await new Promise(r => document.addEventListener("DOMContentLoaded", r, { once: true }));
+}
+// Remove any old Bootstrap stylesheets the preload scanner fetched (un-applies them
+// from the CSSOM). The page stays hidden until it is styled: main() and the /web app
+// call RevealPage once the navbar is in its final state, so there is no unstyled frame
+// or navbar jump. The timeouts make sure the page is shown even if that never happens:
+// 1.5 seconds after main() starts (it first waits for the login check), and at the
+// latest 4 seconds after DOMContentLoaded.
+if (_earlyObs) { _earlyObs.disconnect(); _earlyObs = null; }
+if (_foucStyle) {
+ let _blocked = ["bootstrap.min.css", "white.css", "semantic.min.css", "bootstrap-theme.min.css", "problem.css"];
+ for (let _link of document.querySelectorAll("link")) {
+ if (_blocked.some(h => _link.href && _link.href.indexOf(h) !== -1)) _link.remove();
+ }
+ setTimeout(RevealPage, 4000);
+}
+if (IsContestWebApp()) {
+ await InitializeContestWebApp();
+ return;
+}
+//otherwise CurrentUsername might be undefined
+let loginStatus;
+await fetch("https://www.xmoj.tech/loginpage.php")
+ .then((response) => response.text())
+ .then((data) => (loginStatus = data));
+const logined = loginStatus == "Please logout First!";
+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";
+ return;
}
-function GetMDText(element) {
- let result = '';
- const blockTags = new Set([
- 'P', 'DIV', 'SECTION', 'ARTICLE', 'HEADER', 'FOOTER', 'NAV',
- 'UL', 'OL', 'LI', 'PRE', 'BLOCKQUOTE',
- 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
- 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR'
- ]);
- const cellTags = new Set(['TD', 'TH']);
-
- function traverse(node) {
- if (node.nodeType === Node.TEXT_NODE) {
- result += node.textContent;
- return;
- }
-
- if (node.nodeType !== Node.ELEMENT_NODE) {
- return;
- }
-
- const tag = node.nodeName.toUpperCase();
-
- // Preserve line breaks for
- if (tag === 'BR') {
- result += '\n';
- return;
- }
-
- // Convert images to Markdown
- if (tag === 'IMG') {
- const src = node.getAttribute('src');
- if (src) {
- let resolvedSrc = src;
- try {
- resolvedSrc = new URL(src, location.href).href;
- } catch (e) {
- // Fallback to the raw src if URL construction fails
- }
- result += ``;
- }
- return;
- }
-
- const isBlock = blockTags.has(tag);
- const isCell = cellTags.has(tag);
-
- if (isBlock && !result.endsWith('\n')) {
- result += '\n';
- }
-
- // Keep table cells visually separated when copied as plain text.
- if (isCell && result.length > 0 && !result.endsWith('\n') && !result.endsWith('\t') && !result.endsWith(' ')) {
- result += '\t';
- }
-
- for (let child of node.childNodes) {
- traverse(child);
- }
-
- if (isCell && !result.endsWith('\n') && !result.endsWith('\t')) {
- result += '\t';
- }
+SearchParams = new URLSearchParams(location.search);
+let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-script.uk/" : "https://www.xmoj-script.uk")
+const profileElement = document.querySelector("#profile");
+if (profileElement === null) {
+ RevealPage();
+ if (!logined) {
+ location.href = "https://www.xmoj.tech/loginpage.php";
+ }
+ return;
+}
+CurrentUsername = profileElement.innerText;
+CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, "");
+let IsAdmin = AdminUserList.indexOf(CurrentUsername) !== -1;
- if (isBlock && !result.endsWith('\n')) {
- result += '\n';
- }
+const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
+const applyTheme = (theme) => {
+ if (document.querySelector("html") != null) 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");
+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();
+PeriodicCloudSync();
+setInterval(PeriodicCloudSync, 60 * 60 * 1000);
- traverse(element);
- return result;
+
+function replaceMarkdownImages(text, string) {
+ return text.replace(/!\[.*?\]\(.*?\)/g, string);
}
async function main() {
@@ -2307,11 +3034,11 @@ async function main() {
let Discussion = null;
if (UtilityEnabled("Discussion")) {
Discussion = document.createElement("li");
- document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion);
+ if (document.querySelector("#navbar > ul:nth-child(1)") != null) 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 = "题库";
+ if (document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a") != null) document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a").innerText = "题库";
}
//send analytics
RequestAPI("SendData", {});
@@ -2407,13 +3134,13 @@ async function main() {
} 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;
+ if (document.querySelector("nav") != null) document.querySelector("nav").className = "navbar navbar-expand-lg bg-body-tertiary";
+ if (document.querySelector("#navbar > ul:nth-child(1)") != null) document.querySelector("#navbar > ul:nth-child(1)").classList = "navbar-nav me-auto mb-2 mb-lg-0";
+ if (document.querySelector("body > div > nav > div > div.navbar-header") != null) document.querySelector("body > div > nav > div > div.navbar-header").outerHTML = `${UtilityEnabled("ReplaceXM") ? "高老师" : "小明"}的OJ`;
+ if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li") != null) document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li").classList = "nav-item dropdown";
+ if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a") != null) document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").className = "nav-link dropdown-toggle";
+ if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a > span.caret") != null) document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a > span.caret").remove();
+ Temp = (document.querySelector("#navbar > ul:nth-child(1)") != null) ? 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");
@@ -2422,8 +3149,12 @@ async function main() {
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 (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a") != null) document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").setAttribute("data-bs-toggle", "dropdown");
+ if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a") != null) document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").removeAttribute("data-toggle");
+ // The navbar is in its final state: apply the top bar now rather than on
+ // the first interval tick, then show the page.
+ if (UtilityEnabled("NewTopBar")) new NavbarStyler();
+ RevealPage();
}
if (UtilityEnabled("RemoveUseless") && document.getElementsByTagName("marquee")[0] != undefined) {
document.getElementsByTagName("marquee")[0].remove();
@@ -2504,86 +3235,12 @@ async function main() {
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 != "个人中心") {
+ } 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)") != null && 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();
- }
- });
+ PopupUL.innerHTML = "";
+ let MenuItems = CreateUserMenuItems();
+ for (let Item of MenuItems) PopupUL.appendChild(Item);
+ InitializeUserMenu(PopupUL, document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"), MenuItems);
}
}
if (UtilityEnabled("AutoCountdown")) {
@@ -2642,17 +3299,17 @@ async function main() {
let spacer = document.createElement("div");
spacer.style.height = '48px';
document.body.insertBefore(spacer, document.body.firstChild);
- UpdateDiv.querySelector(".btn-close").addEventListener("click", function () {
+ if (UpdateDiv.querySelector(".btn-close") != null) 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 (document.querySelector("body > div") != null) 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);
+ if (document.querySelector("body") != null) document.querySelector("body").appendChild(UpdateDiv);
UpdateDiv.className = "modal fade";
UpdateDiv.id = "UpdateModal";
UpdateDiv.tabIndex = -1;
@@ -2767,7 +3424,9 @@ async function main() {
dispatchEvent(new Event("focus"));
- if (location.pathname == "/index.php" || location.pathname == "/") {
+ if ((location.pathname == "/index.php" || location.pathname == "/") && document.querySelector("body > div > div.mt-3 > div > div.col-md-4") == null) {
+ //页面结构异常(如403/404页面),跳过处理
+ } else if (location.pathname == "/index.php" || location.pathname == "/") {
if (new URL(location.href).searchParams.get("ByUserScript") != null) {
document.title = "脚本设置";
localStorage.setItem("UserScript-Opened", "true");
@@ -3046,7 +3705,7 @@ async function main() {
FeedbackCard.appendChild(FeedbackCardBody);
Container.appendChild(FeedbackCard);
} else {
- let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children;
+ let Temp = (document.querySelector("body > div > div.mt-3 > div > div.col-md-8") != null) ? 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;
@@ -3057,7 +3716,7 @@ async function main() {
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 = "";
+ if (document.querySelector("body > div > div.mt-3 > div > div.col-md-8") != null) 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";
@@ -3072,10 +3731,10 @@ async function main() {
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);
+ if (document.querySelector("body > div > div.mt-3 > div > div.col-md-8") != null) 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 = `
+ let CountDownData = (document.querySelector("#countdown_list") != null) ? document.querySelector("#countdown_list").innerHTML : "";
+ if (document.querySelector("body > div > div.mt-3 > div > div.col-md-4") != null) document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
倒计时
${CountDownData}
`;
@@ -3083,38 +3742,38 @@ async function main() {
for (let i = 0; i < Tables.length; i++) {
TidyTable(Tables[i]);
}
- document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML += `
+ if (document.querySelector("body > div > div.mt-3 > div > div.col-md-4") != null) 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`);
+ if (document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body") != null) 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;
+ if (document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body") != null) 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 (document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input") != null) document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input").placeholder = "题目编号";
+ if (document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button") != null) document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button").innerText = "确认";
+ if (document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input") != null) document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input").placeholder = "标题或内容";
+ if (document.querySelector("#problemset > thead > tr > th:nth-child(1)") != null) 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%";
+ if (document.querySelector("#problemset > thead > tr > th:nth-child(1)") != null) document.querySelector("#problemset > thead > tr > th:nth-child(1)").style.width = "5%";
+ if (document.querySelector("#problemset > thead > tr > th:nth-child(2)") != null) document.querySelector("#problemset > thead > tr > th:nth-child(2)").style.width = "10%";
+ if (document.querySelector("#problemset > thead > tr > th:nth-child(3)") != null) document.querySelector("#problemset > thead > tr > th:nth-child(3)").style.width = "75%";
+ if (document.querySelector("#problemset > thead > tr > th:nth-child(4)") != null) document.querySelector("#problemset > thead > tr > th:nth-child(4)").style.width = "5%";
+ if (document.querySelector("#problemset > thead > tr > th:nth-child(5)") != null) 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 (document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)") != null) document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)").outerHTML = `