diff --git a/.gitignore b/.gitignore index 2e71e24c..bd658d4e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .idea .vscode .DS_Store +dist/ diff --git a/Claude.md b/Claude.md new file mode 100644 index 00000000..39ef864c --- /dev/null +++ b/Claude.md @@ -0,0 +1,464 @@ +# XMOJ-Script Project Documentation for Claude + +## Project Overview + +XMOJ-Script is a Greasemonkey/Tampermonkey userscript that enhances the XMOJ (小明 Online Judge) platform. The project has been refactored from a monolithic 5000-line file into a modular, maintainable architecture using Rollup bundler. + +**Original File**: XMOJ.user.js (~5000 lines, unmaintainable) +**Current Structure**: Modular ES6 with features, pages, utilities, and core modules +**Build Output**: dist/XMOJ.user.js (preserves userscript header) + +## Architecture + +### Three-Layer Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ FEATURES LAYER │ +│ (20 modules) - User-facing functionality controlled │ +│ by UtilityEnabled() flags in localStorage │ +│ Examples: AutoLogin, DarkMode, Discussion, etc. │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ PAGES LAYER │ +│ (8 modules) - Page-specific styling and DOM │ +│ manipulation, routed by location.pathname │ +│ Examples: problem.js, contest.js, userinfo.js │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ UTILITIES & CORE LAYER │ +│ Shared utilities (10 modules) + core application logic │ +│ bootstrap.js contains legacy code being refactored │ +└─────────────────────────────────────────────────────────┘ +``` + +### Directory Structure + +``` +src/ +├── core/ # Core application logic +│ ├── bootstrap.js # Main app (LEGACY - being refactored out) +│ ├── config.js # UtilityEnabled() feature flags +│ ├── constants.js # Global constants +│ └── menu.js # Greasemonkey menu commands +│ +├── features/ # Feature modules (by UtilityEnabled name) +│ ├── index.js # Feature loader (routes to features) +│ ├── auto-login.js # AutoLogin feature +│ ├── dark-mode.js # DarkMode feature +│ └── ... (20 total) # Each feature is self-contained +│ +├── pages/ # Page-specific modules (by pathname) +│ ├── index.js # Page loader (routes to pages) +│ ├── problem.js # /problem.php +│ ├── contest.js # /contest.php +│ └── ... (8 total) # Each page handles its own styling +│ +├── utils/ # Shared utilities +│ ├── html.js # HTML escaping, sanitization +│ ├── time.js # Time formatting +│ ├── api.js # API requests +│ └── ... (10 total) # Pure utility functions +│ +└── main.js # Entry point (ties everything together) +``` + +## Key Concepts + +### 1. Features + +Features are user-facing functionality controlled by `UtilityEnabled()` flags stored in localStorage. + +**Pattern:** +```javascript +import { UtilityEnabled } from '../core/config.js'; + +export function init(context) { + if (!UtilityEnabled("FeatureName")) { + return; // Feature disabled, do nothing + } + + // Feature implementation here +} +``` + +**Key Points:** +- Each feature checks `UtilityEnabled()` before executing +- Features are initialized in `src/features/index.js` +- Features run BEFORE page modules (early in page load) +- Most features default to enabled (except DebugMode, SuperDebug, ReplaceXM) + +**Current Features (20):** +AutoLogin, Discussion, CopySamples, CompareSource, RemoveUseless, ReplaceXM, ReplaceYN, AddAnimation, AddColorText, SavePassword, RemoveAlerts, ReplaceLinks, AutoO2, Translate, AutoCountdown, MoreSTD, ExportACCode, OpenAllProblem, DarkMode, ImproveACRate + +### 2. Pages + +Pages are route-specific modules that handle page styling and DOM manipulation. + +**Pattern:** +```javascript +export async function init(context) { + // Access utilities from context + const { SearchParams, RenderMathJax, RequestAPI } = context; + + // Page-specific initialization here +} +``` + +**Key Points:** +- Pages are routed by `location.pathname` in `src/pages/index.js` +- Pages receive a context object with utilities +- Pages run AFTER bootstrap.js setup (on window.load event) +- Pages handle styling that doesn't belong to features + +**Current Pages (8 modules, 9 routes):** +- `/problem.php` - Problem view +- `/contest.php` - Contest list/view +- `/status.php` - Submission status +- `/submitpage.php` - Code submission +- `/problemset.php` - Problem list +- `/userinfo.php` - User profile +- `/loginpage.php` - Login form +- `/contestrank-oi.php` - OI ranking +- `/contestrank-correct.php` - Correct ranking + +### 3. Context Object + +The context object is passed to page modules and provides access to utilities: + +```javascript +const pageContext = { + SearchParams: new URLSearchParams(location.search), + RenderMathJax, // MathJax rendering + RequestAPI, // API requests + TidyTable, // Table styling + GetUserInfo, // User information + GetUserBadge, // User badges + GetUsernameHTML, // Username formatting + GetRelativeTime, // Relative time formatting + SmartAlert, // Alert system + Style, // Global style element + IsAdmin, // Admin flag +}; +``` + +## Development Workflow + +### Building + +```bash +npm install # Install dependencies +npm run build # Build once +npm run watch # Watch mode (auto-rebuild) +``` + +**Output:** `dist/XMOJ.user.js` (includes userscript header) + +### Adding a New Feature + +1. Create `src/features/feature-name.js`: +```javascript +import { UtilityEnabled } from '../core/config.js'; + +export function init(context) { + if (!UtilityEnabled("FeatureName")) return; + // Implementation +} +``` + +2. Add to `src/features/index.js`: +```javascript +import { init as initFeatureName } from './feature-name.js'; + +// In initializeFeatures(): +initFeatureName(); + +// In getExtractedFeatures(): +return [..., 'FeatureName']; +``` + +3. Test build and commit + +### Adding a New Page + +1. Create `src/pages/page-name.js`: +```javascript +export async function init(context) { + const { SearchParams } = context; + // Page-specific code +} +``` + +2. Add to `src/pages/index.js`: +```javascript +import { init as initPageName } from './page-name.js'; + +const PAGE_ROUTES = { + ... + '/page-name.php': initPageName, +}; +``` + +3. Test build and commit + +## Important Patterns & Conventions + +### 1. Feature Extraction Pattern + +When extracting a feature from bootstrap.js: +- Search for `UtilityEnabled("FeatureName")` +- Extract ALL related code (search entire file) +- Create self-contained module +- Document extracted line numbers in comments +- Update feature loader +- Test build + +### 2. Page Extraction Pattern + +When extracting a page from bootstrap.js: +- Search for `location.pathname == "/page.php"` +- Extract page-specific code (styling, DOM manipulation) +- Preserve feature flag checks (features handle their own checks) +- Create module with context parameter +- Update page loader +- Test build + +### 3. Utility Functions + +Utilities are made globally available via `window` for compatibility: +```javascript +// In main.js +window.RequestAPI = RequestAPI; +window.GetUserInfo = GetUserInfo; +// etc. +``` + +This allows bootstrap.js (legacy code) to call utilities. + +### 4. Error Handling + +Always wrap page/feature initialization in try-catch: +```javascript +try { + // Implementation +} catch (error) { + console.error('[ModuleName] Error:', error); +} +``` + +### 5. Async Operations + +Many operations are async (MathJax, API calls, user info): +```javascript +export async function init(context) { + await RenderMathJax(); + const userInfo = await GetUserInfo(userId); + // ... +} +``` + +## Important Technical Details + +### 1. localStorage Keys + +The script uses localStorage extensively: +- `UserScript-Setting-{FeatureName}` - Feature flags (true/false) +- `UserScript-User-{UserID}-*` - User data cache +- `UserScript-Problem-{PID}-*` - Problem data cache +- `UserScript-Contest-{CID}-*` - Contest data cache +- `UserScript-LastPage` - Last visited page (for AutoLogin) + +### 2. Feature Flags Default + +Most features default to enabled. Exceptions: +- `DebugMode` - false by default +- `SuperDebug` - false by default +- `ReplaceXM` - false by default (changes "小明" to "高老师") + +### 3. External Libraries + +The script uses these external libraries (loaded by bootstrap.js): +- **Bootstrap 5** - UI framework +- **jQuery** - DOM manipulation (legacy) +- **CodeMirror** - Code editor (for submit page) +- **MathJax** - Math rendering +- **DOMPurify** - HTML sanitization +- **CryptoJS** - MD5 hashing (for passwords) +- **FileSaver.js** - File downloads +- **marked.js** - Markdown rendering + +### 4. Greasemonkey APIs + +The script uses these GM APIs: +- `GM_registerMenuCommand` - Menu commands +- `GM_xmlhttpRequest` - Cross-origin requests +- `GM_setClipboard` - Clipboard access +- `GM_setValue` / `GM_getValue` - Persistent storage (optional) +- `GM_cookie` - Cookie access + +### 5. API Endpoints + +The script communicates with XMOJ backend via `RequestAPI()`: +- `GetUserInfo` - User information +- `GetUserBadge` - User badges +- `GetPostCount` - Discussion counts +- `GetMailMentionList` - Mail/mention notifications +- `LastOnline` - Last online time +- And many more... + +### 6. Time Synchronization + +The script syncs with server time: +```javascript +window.diff // Global time difference with server +``` + +Used by countdown timers (AutoCountdown feature, contest timers). + +### 7. Dark Mode + +DarkMode feature sets `data-bs-theme` attribute: +```javascript +document.querySelector("html").setAttribute("data-bs-theme", "dark"); +``` + +Many components check this for conditional styling. + +## Remaining Refactoring Work + +### Features Still in bootstrap.js (21 remaining) + +These features have NOT been extracted yet: +- AddUnits +- ApplyData, AutoCheat, AutoRefresh +- BBSPopup, CompileError, CopyMD +- DebugMode, DownloadPlayback +- IOFile, LoginFailed, MessagePopup +- NewBootstrap, NewDownload, NewTopBar +- ProblemSwitcher, Rating +- RefreshSolution, ResetType +- UploadStd + +### Pages That Could Be Extracted + +Additional pages that could benefit from extraction: +- `/index.php` - Home page +- `/modifypage.php` - Settings/changelog +- `/reinfo.php` - Test case info +- `/ceinfo.php` - Compile error info +- `/showsource.php` - Source code view +- `/problem_std.php` - Standard solution +- `/comparesource.php` - Source comparison (partially in features) +- `/downloads.php` - Downloads page +- `/problemstatus.php` - Problem statistics +- `/problem_solution.php` - Problem solution +- `/open_contest.php` - Open contests +- `/mail.php` - Mail system +- `/contest_video.php` - Contest video +- `/problem_video.php` - Problem video + +### Legacy Code in bootstrap.js + +The `bootstrap.js` file still contains: +- ~4400 lines of legacy code +- All remaining features (see above) +- All remaining page handlers +- Some duplicate code (features extracted but not removed from bootstrap) + +**Strategy:** Continue extracting features and pages until bootstrap.js only contains: +- Initial setup (theme, navbar, resources) +- Core application logic that doesn't fit elsewhere +- Minimal glue code + +## Git Workflow + +**Branch:** `claude/refactor-userscript-structure-011CUxWM4EozUNd9os4Da49N` + +**Commit Message Pattern:** +``` +Extract N features/pages: Name1, Name2, Name3 + +Description of what was extracted and why. + +New features/pages: +- Name1: Description +- Name2: Description + +Updated: +- Files that changed +- Build output size + +Total extracted: X +Remaining: Y +``` + +**Push Pattern:** +```bash +git add -A +git commit -m "..." +git push -u origin claude/refactor-userscript-structure-011CUxWM4EozUNd9os4Da49N +``` + +## Testing + +**Manual Testing Required:** +- Build completes without errors +- Output file has userscript header +- File size is reasonable (~500-600KB currently) +- No obvious syntax errors in output + +**No Automated Tests:** This project has no test suite. Changes must be tested manually on the XMOJ website. + +## Common Pitfalls + +1. **Don't remove code from bootstrap.js** until confirming it's fully extracted and working in the new module +2. **Preserve userscript header** - Rollup config handles this, don't modify without checking +3. **Check UtilityEnabled logic** - Features must self-check if enabled +4. **Context object** - Pages need utilities passed via context +5. **Async/await** - Many operations are async, handle properly +6. **localStorage keys** - Use existing patterns, don't create new schemes +7. **Global window vars** - Utilities must be on window for bootstrap.js compatibility +8. **Line number references** - When extracting, note original line numbers in comments + +## Success Metrics + +**Started with:** +- 1 file: XMOJ.user.js (5000 lines) +- No organization +- Impossible to maintain + +**Currently:** +- 20 feature modules (organized by functionality) +- 8 page modules (organized by route) +- 10 utility modules (shared code) +- 4 core modules (application logic) +- Clean architecture +- Easy to find and modify code +- ~549KB build output (reasonable) + +**Goal:** +- Extract all remaining 21 features +- Extract all remaining pages +- Reduce bootstrap.js to <1000 lines of essential code +- Maintain or reduce bundle size +- Keep all functionality working + +## Contact & Resources + +**Documentation:** +- README_REFACTORING.md - Detailed refactoring guide +- This file (Claude.md) - AI assistant documentation + +**Key Files to Understand:** +1. `src/main.js` - Entry point, shows initialization flow +2. `src/features/index.js` - Feature loader, shows all features +3. `src/pages/index.js` - Page loader, shows all pages +4. `src/core/config.js` - UtilityEnabled implementation +5. `rollup.config.js` - Build configuration + +**External Resources:** +- XMOJ Website: https://www.xmoj.tech +- Rollup Docs: https://rollupjs.org +- Greasemonkey API: https://wiki.greasespot.net/Greasemonkey_Manual:API diff --git a/README_REFACTORING.md b/README_REFACTORING.md new file mode 100644 index 00000000..4e7760ec --- /dev/null +++ b/README_REFACTORING.md @@ -0,0 +1,190 @@ +# XMOJ-Script Refactoring + +This project has been refactored to use a modular structure with a bundler (Rollup) for better maintainability. + +## Project Structure + +``` +XMOJ-Script/ +├── src/ # Source code (modular) +│ ├── core/ # Core application logic +│ │ ├── constants.js # Constants and configuration values +│ │ ├── config.js # Feature configuration (UtilityEnabled) +│ │ ├── bootstrap.js # Main application logic and initialization +│ │ └── menu.js # Greasemonkey menu commands +│ ├── utils/ # Utility modules +│ │ ├── alerts.js # Alert utilities +│ │ ├── api.js # API request utilities +│ │ ├── credentials.js # Credential storage utilities +│ │ ├── format.js # Size formatting utilities +│ │ ├── html.js # HTML escaping and purifying +│ │ ├── mathjax.js # MathJax rendering +│ │ ├── table.js # Table styling utilities +│ │ ├── time.js # Time formatting utilities +│ │ ├── user.js # User information utilities +│ │ └── version.js # Version comparison utilities +│ ├── features/ # Feature modules (to be extracted) +│ └── main.js # Main entry point +├── dist/ # Built output +│ └── XMOJ.user.js # Bundled userscript (generated) +├── rollup.config.js # Rollup bundler configuration +├── package.json # NPM package configuration +└── XMOJ.user.js # Original userscript (legacy) +``` + +## Building + +```bash +# Install dependencies +npm install + +# Build once +npm run build + +# Watch for changes and rebuild automatically +npm run watch +``` + +The bundled output will be in `dist/XMOJ.user.js`. + +## Features + +The userscript includes the following features (controlled via `UtilityEnabled`): + +- AddAnimation - Add animations to UI elements +- AddColorText - Add colored text +- AddUnits - Add units to numbers (KB, MB, etc.) +- ApplyData - Apply user data +- AutoCheat - Auto-cheat features +- AutoCountdown - Automatic countdown +- AutoLogin - Automatic login +- AutoO2 - Auto O2 compilation flag +- AutoRefresh - Auto-refresh pages +- BBSPopup - BBS popup notifications +- CompareSource - Compare source code +- CompileError - Compile error enhancements +- CopyMD - Copy as Markdown +- CopySamples - Copy sample inputs/outputs +- DarkMode - Dark mode theme +- DebugMode - Debug mode logging +- Discussion - Discussion features +- DownloadPlayback - Download playback +- ExportACCode - Export AC code +- IOFile - IO file handling +- ImproveACRate - Improve AC rate display +- LoginFailed - Login failure handling +- MessagePopup - Message popup notifications +- MoreSTD - More standard solutions +- NewBootstrap - New Bootstrap UI +- NewDownload - New download features +- NewTopBar - New top navigation bar +- OpenAllProblem - Open all problems +- ProblemSwitcher - Problem switcher +- Rating - User rating display +- RefreshSolution - Refresh solution display +- RemoveAlerts - Remove alert popups +- RemoveUseless - Remove useless elements +- ReplaceLinks - Replace links +- ReplaceXM - Replace "小明" with "高老师" +- ReplaceYN - Replace Y/N text +- ResetType - Reset type display +- SavePassword - Save password +- SuperDebug - Super debug mode +- Translate - Translation features +- UploadStd - Upload standard solutions + +## Page Modules + +Page-specific styling and functionality has been extracted into separate modules under `src/pages/`: + +- **problem.js** - `/problem.php` - Problem view page with submit button fixes, sample data styling, discussion button +- **contest.js** - `/contest.php` - Contest list and contest view with countdown timers, problem list formatting +- **status.js** - `/status.php` - Submission status page +- **submit.js** - `/submitpage.php` - Code submission page +- **problemset.js** - `/problemset.php` - Problem list page with search forms and column widths +- **userinfo.js** - `/userinfo.php` - User profile page with avatar, AC problems, badge management +- **login.js** - `/loginpage.php` - Login page with Bootstrap-styled form +- **contestrank.js** - `/contestrank-oi.php` and `/contestrank-correct.php` - Contest ranking pages with colored cells + +Page modules are loaded automatically based on the current pathname and handle page-specific DOM manipulations that don't belong to any particular feature. + +## Extracted Features + +The following features have been extracted into separate modules under `src/features/`: + +### Currently Extracted +- **AutoLogin** (`auto-login.js`) - Automatically redirects to login page when not authenticated +- **Discussion** (`discussion.js`) - Complete forum system with post creation, viewing, and replies +- **CopySamples** (`copy-samples.js`) - Fixes copy functionality for test samples +- **CompareSource** (`compare-source.js`) - Side-by-side code comparison with diff highlighting +- **RemoveUseless** (`remove-useless.js`) - Removes unwanted page elements (marquees, footers, etc.) +- **ReplaceXM** (`replace-xm.js`) - Replaces "小明" references with "高老师" +- **ReplaceYN** (`replace-yn.js`) - Replaces Y/N status indicators with symbols (✓/✗/⏳) +- **AddAnimation** (`add-animation.js`) - Adds CSS transitions to status and test-case elements +- **AddColorText** (`add-color-text.js`) - Adds CSS classes for colored text (red, green, blue) +- **SavePassword** (`save-password.js`) - Automatically saves and fills login credentials +- **RemoveAlerts** (`remove-alerts.js`) - Removes redundant alerts and warnings +- **ReplaceLinks** (`replace-links.js`) - Replaces bracketed links with styled buttons +- **AutoO2** (`auto-o2.js`) - Automatically enables O2 optimization flag for code submissions +- **Translate** (`translate.js`) - Translates English text to Chinese throughout the site +- **AutoCountdown** (`auto-countdown.js`) - Automatically updates countdown timers on the page +- **MoreSTD** (`more-std.js`) - Adds standard solution links to contest problem tables +- **ExportACCode** (`export-ac-code.js`) - Exports all accepted code solutions as a ZIP file +- **OpenAllProblem** (`open-all-problem.js`) - Adds buttons to open all problems or only unsolved ones +- **DarkMode** (`dark-mode.js`) - Enables dark theme for the website +- **ImproveACRate** (`improve-ac-rate.js`) - Adds a button to resubmit already-AC'd problems + +### Feature Extraction Pattern + +Each feature module follows this pattern: + +```javascript +import { UtilityEnabled } from '../core/config.js'; +// Import other needed utilities... + +/** + * Initialize the FeatureName feature + */ +export function init(context) { + // Check if feature is enabled + if (!UtilityEnabled("FeatureName")) { + return; + } + + // Feature implementation... +} +``` + +### Adding New Features + +To extract additional features from `bootstrap.js`: + +1. Identify code that checks `UtilityEnabled("FeatureName")` +2. Create `src/features/feature-name.js` with the extracted code +3. Add import to `src/features/index.js` +4. Add initialization call in `initializeFeatures()` +5. Test the build with `npm run build` + +### Remaining Features to Extract + +The following features remain in `bootstrap.js` and can be extracted following the pattern above: + +- AddUnits +- ApplyData, AutoCheat, AutoRefresh +- BBSPopup, CompileError, CopyMD +- DebugMode, DownloadPlayback +- IOFile, LoginFailed, MessagePopup +- NewBootstrap, NewDownload, NewTopBar +- ProblemSwitcher, Rating +- RefreshSolution, ResetType +- UploadStd + +## Development + +The codebase is now set up for easier development: + +1. Edit files in `src/` +2. Run `npm run watch` to automatically rebuild on changes +3. Test the bundled output in `dist/XMOJ.user.js` + +All original functionality has been preserved - the refactoring only changes the code structure, not the behavior. diff --git a/Update.json b/Update.json index 043b5b37..fd2fe77e 100644 --- a/Update.json +++ b/Update.json @@ -3214,6 +3214,17 @@ } ], "Notes": "No release notes were provided for this release." + }, + "2.5.3": { + "UpdateDate": 1766796708973, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 882, + "Description": "refactor userscript structure " + } + ], + "Notes": "No release notes were provided for this release." } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 389809d0..d1a72261 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 2.5.2 +// @version 2.5.3 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen diff --git a/package.json b/package.json index b70e2ba3..a8ccbe08 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,12 @@ { "name": "xmoj-script", - "version": "2.5.2", + "version": "2.5.3", "description": "an improvement script for xmoj.tech", - "main": "AddonScript.js", + "type": "module", + "main": "dist/XMOJ.user.js", "scripts": { + "build": "rollup -c", + "watch": "rollup -c -w", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -18,5 +21,10 @@ "bugs": { "url": "https://github.com/XMOJ-Script-dev/XMOJ-Script/issues" }, - "homepage": "https://github.com/XMOJ-Script-dev/XMOJ-Script#readme" + "homepage": "https://github.com/XMOJ-Script-dev/XMOJ-Script#readme", + "devDependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "rollup": "^4.53.1" + } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..713901cb --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,21 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import { readFileSync } from 'fs'; + +// Read the original userscript header +const originalScript = readFileSync('./XMOJ.user.js', 'utf-8'); +const headerMatch = originalScript.match(/\/\/ ==UserScript==[\s\S]*?\/\/ ==\/UserScript==/); +const header = headerMatch ? headerMatch[0] : ''; + +export default { + input: 'src/main.js', + output: { + file: 'dist/XMOJ.user.js', + format: 'iife', + banner: header + '\n', + }, + plugins: [ + nodeResolve(), + commonjs(), + ], +}; diff --git a/src/core/bootstrap.js b/src/core/bootstrap.js new file mode 100644 index 00000000..2fa777c7 --- /dev/null +++ b/src/core/bootstrap.js @@ -0,0 +1,4333 @@ +/** + * Bootstrap initialization and main application logic + */ + +import { UtilityEnabled } from './config.js'; +import { AdminUserList } from './constants.js'; +import { SmartAlert } from '../utils/alerts.js'; +import { RequestAPI } from '../utils/api.js'; +import { GetRelativeTime } from '../utils/time.js'; +import { TidyTable } from '../utils/table.js'; +import { compareVersions } from '../utils/version.js'; +import { clearCredential } from '../utils/credentials.js'; + +// Turnstile Captcha Site Key +const CaptchaSiteKey = "0x4AAAAAAALBT58IhyDViNmv"; +// Make it globally available for Turnstile callbacks +if (typeof unsafeWindow !== 'undefined') { + unsafeWindow.CaptchaSiteKey = CaptchaSiteKey; +} else if (typeof window !== 'undefined') { + window.CaptchaSiteKey = CaptchaSiteKey; +} + +// Time difference for server synchronization (initialized to 0) +let diff = 0; + +// Theme management +const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); + +const applyTheme = (theme) => { + document.querySelector("html").setAttribute("data-bs-theme", theme); + localStorage.setItem("UserScript-Setting-DarkMode", theme === "dark" ? "true" : "false"); +}; + +const applySystemTheme = (e) => applyTheme(e.matches ? "dark" : "light"); + +export let initTheme = () => { + const saved = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + if (saved === "auto") { + applyTheme(prefersDark.matches ? "dark" : "light"); + prefersDark.addEventListener("change", applySystemTheme); + } else { + applyTheme(saved); + prefersDark.removeEventListener("change", applySystemTheme); + } +}; + +// NavbarStyler class for styling the navigation bar +export class NavbarStyler { + constructor() { + try { + this.navbar = document.querySelector('.navbar.navbar-expand-lg.bg-body-tertiary'); + if (this.navbar && UtilityEnabled("NewTopBar")) { + this.init(); + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } + + init() { + try { + this.applyStyles(); + this.createOverlay(); + this.createSpacer(); + window.addEventListener('resize', () => this.updateBlurOverlay()); + this.updateBlurOverlay(); + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } + + applyStyles() { + try { + let n = this.navbar; + n.classList.add('fixed-top', 'container', 'ml-auto'); + Object.assign(n.style, { + position: 'fixed', + borderRadius: '28px', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.5)', + margin: '16px auto', + backgroundColor: 'rgba(255, 255, 255, 0)', + opacity: '0.75', + zIndex: '1000' + }); + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } + + createOverlay() { + try { + if (!document.getElementById('blur-overlay')) { + let overlay = document.createElement('div'); + overlay.id = 'blur-overlay'; + document.body.appendChild(overlay); + + let style = document.createElement('style'); + style.textContent = ` + #blur-overlay { + position: fixed; + backdrop-filter: blur(4px); + z-index: 999; + pointer-events: none; + border-radius: 28px; + } + `; + document.head.appendChild(style); + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } + + updateBlurOverlay() { + try { + let overlay = document.getElementById('blur-overlay'); + let n = this.navbar; + Object.assign(overlay.style, { + top: `${n.offsetTop}px`, + left: `${n.offsetLeft}px`, + width: `${n.offsetWidth}px`, + height: `${n.offsetHeight}px` + }); + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } + + createSpacer() { + try { + let spacer = document.getElementById('navbar-spacer'); + let newHeight = this.navbar.offsetHeight + 24; + if (!spacer) { + spacer = document.createElement('div'); + spacer.id = 'navbar-spacer'; + spacer.style.height = `${newHeight}px`; + spacer.style.width = '100%'; + document.body.insertBefore(spacer, document.body.firstChild); + } else { + let currentHeight = parseInt(spacer.style.height, 10); + if (currentHeight !== newHeight) { + document.body.removeChild(spacer); + spacer = document.createElement('div'); + spacer.id = 'navbar-spacer'; + spacer.style.height = `${newHeight}px`; + spacer.style.width = '100%'; + document.body.insertBefore(spacer, document.body.firstChild); + } + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } + } +} + +// Utility function to replace markdown images +export function replaceMarkdownImages(text, string) { + return text.replace(/!\[.*?\]\(.*?\)/g, string); +} + +// Main initialization function +export async function main() { + try { + // Create SearchParams for use throughout bootstrap.js + const SearchParams = new URLSearchParams(location.search); + + if (location.href.startsWith('http://')) { + //use https + location.href = location.href.replace('http://', 'https://'); + } + if (location.host != "www.xmoj.tech") { + location.host = "www.xmoj.tech"; + } else { + if (location.href === 'https://www.xmoj.tech/open_contest_sign_up.php') { + return; + } + + // Get current username + let CurrentUsername = ""; + if (document.querySelector("#profile") !== null) { + CurrentUsername = document.querySelector("#profile").innerText; + CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); + } + + // Check if current user is admin + let IsAdmin = AdminUserList.indexOf(CurrentUsername) !== -1; + window.IsAdmin = IsAdmin; // Make available globally for page modules + + // Determine server URL + let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me"); + + document.body.classList.add("placeholder-glow"); + if (document.querySelector("#navbar") != null) { + if (document.querySelector("body > div > div.jumbotron") != null) { + document.querySelector("body > div > div.jumbotron").className = "mt-3"; + } + + if (UtilityEnabled("AutoLogin") && document.querySelector("#profile") != null && document.querySelector("#profile").innerHTML == "登录" && location.pathname != "/login.php" && location.pathname != "/loginpage.php" && location.pathname != "/lostpassword.php") { + localStorage.setItem("UserScript-LastPage", location.pathname + location.search); + location.href = "https://www.xmoj.tech/loginpage.php"; + } + + let Discussion = null; + if (UtilityEnabled("Discussion")) { + Discussion = document.createElement("li"); + document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); + Discussion.innerHTML = "讨论"; + } + if (UtilityEnabled("Translate")) { + document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a").innerText = "题库"; + } + //send analytics + RequestAPI("SendData", {}); + if (UtilityEnabled("ReplaceLinks")) { + document.body.innerHTML = String(document.body.innerHTML).replaceAll(/\[([^<]*)<\/a>\]/g, ""); + } + if (UtilityEnabled("ReplaceXM")) { + document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("海上", "上海"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小红", "徐师娘"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小粉", "彩虹"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("提交上节课的代码", "自动提交当年代码"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("高老师们", "我们"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("自高老师", "自我"); + document.title = String(document.title).replaceAll("小明", "高老师"); + } + + if (UtilityEnabled("NewBootstrap")) { + let Temp = document.querySelectorAll("link"); + for (var i = 0; i < Temp.length; i++) { + if (Temp[i].href.indexOf("bootstrap.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("white.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("semantic.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("bootstrap-theme.min.css") != -1) { + Temp[i].remove(); + } else if (Temp[i].href.indexOf("problem.css") != -1) { + Temp[i].remove(); + } + } + if (UtilityEnabled("DarkMode")) { + document.querySelector("html").setAttribute("data-bs-theme", "dark"); + } else { + document.querySelector("html").setAttribute("data-bs-theme", "light"); + } + var resources = [{ + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/theme/darcula.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/merge/merge.min.css', + rel: 'stylesheet' + }, { + type: 'link', + href: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css', + rel: 'stylesheet' + }, { + type: 'script', + src: 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.js', + isModule: true + }]; + let loadResources = async () => { + let promises = resources.map(resource => { + return new Promise((resolve, reject) => { + let element; + if (resource.type === 'script') { + element = document.createElement('script'); + element.src = resource.src; + if (resource.isModule) { + element.type = 'module'; + } + element.onload = resolve; + element.onerror = reject; + } else if (resource.type === 'link') { + element = document.createElement('link'); + element.href = resource.href; + element.rel = resource.rel; + resolve(); // Stylesheets don't have an onload event + } + document.head.appendChild(element); + }); + }); + + await Promise.all(promises); + }; + if (location.pathname == "/submitpage.php") { + await loadResources(); + } else { + loadResources(); + } + document.querySelector("nav").className = "navbar navbar-expand-lg bg-body-tertiary"; + document.querySelector("#navbar > ul:nth-child(1)").classList = "navbar-nav me-auto mb-2 mb-lg-0"; + document.querySelector("body > div > nav > div > div.navbar-header").outerHTML = `${UtilityEnabled("ReplaceXM") ? "高老师" : "小明"}的OJ`; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li").classList = "nav-item dropdown"; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").className = "nav-link dropdown-toggle"; + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a > span.caret").remove(); + Temp = document.querySelector("#navbar > ul:nth-child(1)").children; + for (var i = 0; i < Temp.length; i++) { + if (Temp[i].classList.contains("active")) { + Temp[i].classList.remove("active"); + Temp[i].children[0].classList.add("active"); + } + Temp[i].classList.add("nav-item"); + Temp[i].children[0].classList.add("nav-link"); + } + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").setAttribute("data-bs-toggle", "dropdown"); + document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > a").removeAttribute("data-toggle"); + } + if (UtilityEnabled("RemoveUseless") && document.getElementsByTagName("marquee")[0] != undefined) { + document.getElementsByTagName("marquee")[0].remove(); + } + let Style = document.createElement("style"); + document.body.appendChild(Style); + Style.innerHTML = ` + nav { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + blockquote { + border-left: 5px solid var(--bs-secondary-bg); + padding: 0.5em 1em; + } + .status_y:hover { + box-shadow: #52c41a 1px 1px 10px 0px !important; + } + .status_n:hover { + box-shadow: #fe4c61 1px 1px 10px 0px !important; + } + .status_w:hover { + box-shadow: #ffa900 1px 1px 10px 0px !important; + } + .test-case { + border-radius: 5px !important; + } + .test-case:hover { + box-shadow: rgba(0, 0, 0, 0.3) 0px 10px 20px 3px !important; + } + .data[result-item] { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + .software_list { + width: unset !important; + } + .software_item { + margin: 5px 10px !important; + background-color: var(--bs-secondary-bg) !important; + } + .item-txt { + color: var(--bs-emphasis-color) !important; + } + .cnt-row { + justify-content: inherit; + align-items: stretch; + width: 100% !important; + padding: 1rem 0; + } + .cnt-row-head { + padding: 0.8em 1em; + background-color: var(--bs-secondary-bg); + border-radius: 0.3rem 0.3rem 0 0; + width: 100%; + } + .cnt-row-body { + padding: 1em; + border: 1px solid var(--bs-secondary-bg); + border-top: none; + border-radius: 0 0 0.3rem 0.3rem; + }`; + if (UtilityEnabled("AddAnimation")) { + Style.innerHTML += `.status, .test-case { + transition: 0.5s !important; + }`; + } + if (UtilityEnabled("AddColorText")) { + Style.innerHTML += `.red { + color: red !important; + } + .green { + color: green !important; + } + .blue { + color: blue !important; + }`; + } + + if (UtilityEnabled("RemoveUseless")) { + if (document.getElementsByClassName("footer")[0] != null) { + document.getElementsByClassName("footer")[0].remove(); + } + } + + if (UtilityEnabled("ReplaceYN")) { + let Temp = document.getElementsByClassName("status_y");//AC + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "✓"; + } + Temp = document.getElementsByClassName("status_n");//WA + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "✗"; + } + Temp = document.getElementsByClassName("status_w");//Waiting + for (let i = 0; i < Temp.length; i++) { + Temp[i].innerText = "⏳"; + } + } + + let Temp = document.getElementsByClassName("page-item"); + for (let i = 0; i < Temp.length; i++) { + Temp[i].children[0].className = "page-link"; + } + if (document.getElementsByClassName("pagination")[0] != null) { + document.getElementsByClassName("pagination")[0].classList.add("justify-content-center"); + } + + Temp = document.getElementsByTagName("table"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].querySelector("thead") != null) { + TidyTable(Temp[i]); + } + } + + setInterval(() => { + try { + let CurrentDate = new Date(new Date().getTime() + diff); + let Year = CurrentDate.getFullYear(); + if (Year > 3000) { + Year -= 1900; + } + let Month = CurrentDate.getMonth() + 1; + let _Date = CurrentDate.getDate(); + let Hours = CurrentDate.getHours(); + let Minutes = CurrentDate.getMinutes(); + let Seconds = CurrentDate.getSeconds(); + document.getElementById("nowdate").innerHTML = Year + "-" + (Month < 10 ? "0" : "") + Month + "-" + (_Date < 10 ? "0" : "") + _Date + " " + (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; + } catch (Error) { + } + if (UtilityEnabled("NewTopBar")) { + new NavbarStyler(); + } + if (UtilityEnabled("ResetType")) { + if (document.querySelector("#profile") != undefined && document.querySelector("#profile").innerHTML == "登录") { + let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); + PopupUL.innerHTML = ``; + PopupUL.children[0].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/loginpage.php"; + }); + let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); + document.addEventListener("click", (event) => { + if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { + hideDropdownItems(); + } + }); + } else if (document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul") != undefined && document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul > li:nth-child(2)").innerText != "个人中心") { + let PopupUL = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li > ul"); + PopupUL.style.cursor = 'pointer'; + PopupUL.innerHTML = ` + + + + + `; + PopupUL.children[0].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/modifypage.php"; + }); + PopupUL.children[1].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername; + }); + PopupUL.children[2].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/mail.php"; + }); + PopupUL.children[3].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/index.php?ByUserScript=1"; + }); + PopupUL.children[4].addEventListener("click", () => { + location.href = "https://www.xmoj.tech/modifypage.php?ByUserScript=1"; + }); + PopupUL.children[5].addEventListener("click", () => { + clearCredential(); + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + }) + .catch((error) => { + console.error(error); + }); //We can no longer rely of the server to set the cookie for us + location.href = "https://www.xmoj.tech/logout.php"; + }); + Array.from(PopupUL.children).forEach(item => { + item.style.opacity = 0; + item.style.transform = 'translateY(-16px)'; + item.style.transition = 'transform 0.3s ease, opacity 0.5s ease'; + }); + let showDropdownItems = () => { + PopupUL.style.display = 'block'; + Array.from(PopupUL.children).forEach((item, index) => { + clearTimeout(item._timeout); + item.style.opacity = 0; + item.style.transform = 'translateY(-4px)'; + item._timeout = setTimeout(() => { + item.style.opacity = 1; + item.style.transform = 'translateY(2px)'; + }, index * 36); + }); + }; + let hideDropdownItems = () => { + Array.from(PopupUL.children).forEach((item) => { + clearTimeout(item._timeout); + item.style.opacity = 0; + item.style.transform = 'translateY(-16px)'; + }); + setTimeout(() => { + PopupUL.style.display = 'none'; + }, 100); + }; + let toggleDropdownItems = () => { + if (PopupUL.style.display === 'block') { + hideDropdownItems(); + } else { + showDropdownItems(); + } + }; + let parentLi = document.querySelector("#navbar > ul.nav.navbar-nav.navbar-right > li"); + parentLi.addEventListener("click", toggleDropdownItems); + document.addEventListener("click", (event) => { + if (!parentLi.contains(event.target) && PopupUL.style.display === 'block') { + hideDropdownItems(); + } + }); + } + } + if (UtilityEnabled("AutoCountdown")) { + let Temp = document.getElementsByClassName("UpdateByJS"); + for (let i = 0; i < Temp.length; i++) { + let EndTime = Temp[i].getAttribute("EndTime"); + if (EndTime === null) { + Temp[i].classList.remove("UpdateByJS"); + continue; + } + let TimeStamp = parseInt(EndTime) - new Date().getTime(); + if (TimeStamp < 3000) { + Temp[i].classList.remove("UpdateByJS"); + location.reload(); + } + let CurrentDate = new Date(TimeStamp); + let Day = parseInt((TimeStamp / 1000 / 60 / 60 / 24).toFixed(0)); + let Hour = CurrentDate.getUTCHours(); + let Minute = CurrentDate.getUTCMinutes(); + let Second = CurrentDate.getUTCSeconds(); + Temp[i].innerHTML = (Day !== 0 ? Day + "天" : "") + (Hour !== 0 ? (Hour < 10 ? "0" : "") + Hour + "小时" : "") + (Minute !== 0 ? (Minute < 10 ? "0" : "") + Minute + "分" : "") + (Second !== 0 ? (Second < 10 ? "0" : "") + Second + "秒" : ""); + } + } + }, 100); + + // Check for updates + fetch(ServerURL + "/Update.json", {cache: "no-cache"}) + .then((Response) => { + return Response.json(); + }) + .then((Response) => { + let CurrentVersion = GM_info.script.version; + let LatestVersion; + for (let i = Object.keys(Response.UpdateHistory).length - 1; i >= 0; i--) { + let VersionInfo = Object.keys(Response.UpdateHistory)[i]; + if (UtilityEnabled("DebugMode") || Response.UpdateHistory[VersionInfo].Prerelease == false) { + LatestVersion = VersionInfo; + break; + } + } + if (compareVersions(CurrentVersion, LatestVersion)) { + let UpdateDiv = document.createElement("div"); + UpdateDiv.innerHTML = ` + `; + if (UtilityEnabled("NewTopBar")) { + UpdateDiv.style.position = 'fixed'; + UpdateDiv.style.top = '72px'; + UpdateDiv.style.left = '50%'; + UpdateDiv.style.transform = 'translateX(-50%)'; + UpdateDiv.style.zIndex = '1001'; + let spacer = document.createElement("div"); + spacer.style.height = '48px'; + document.body.insertBefore(spacer, document.body.firstChild); + UpdateDiv.querySelector(".btn-close").addEventListener("click", function () { + document.body.removeChild(spacer); + }); + } + document.body.appendChild(UpdateDiv); + // Try to move update div before mt-3, but handle DOM structure differences + try { + const container = document.querySelector("body > div"); + const mt3 = document.querySelector("body > div > div.mt-3"); + if (container && mt3 && mt3.parentNode === container) { + container.insertBefore(UpdateDiv, mt3); + } + } catch (e) { + console.warn('[XMOJ-Script] Could not reposition update div:', e); + } + } + if (localStorage.getItem("UserScript-Update-LastVersion") != GM_info.script.version) { + localStorage.setItem("UserScript-Update-LastVersion", GM_info.script.version); + let UpdateDiv = document.createElement("div"); + document.querySelector("body").appendChild(UpdateDiv); + UpdateDiv.className = "modal fade"; + UpdateDiv.id = "UpdateModal"; + UpdateDiv.tabIndex = -1; + let UpdateDialog = document.createElement("div"); + UpdateDiv.appendChild(UpdateDialog); + UpdateDialog.className = "modal-dialog"; + let UpdateContent = document.createElement("div"); + UpdateDialog.appendChild(UpdateContent); + UpdateContent.className = "modal-content"; + let UpdateHeader = document.createElement("div"); + UpdateContent.appendChild(UpdateHeader); + UpdateHeader.className = "modal-header"; + let UpdateTitle = document.createElement("h5"); + UpdateHeader.appendChild(UpdateTitle); + UpdateTitle.className = "modal-title"; + UpdateTitle.innerText = "更新日志"; + let UpdateCloseButton = document.createElement("button"); + UpdateHeader.appendChild(UpdateCloseButton); + UpdateCloseButton.type = "button"; + UpdateCloseButton.className = "btn-close"; + UpdateCloseButton.setAttribute("data-bs-dismiss", "modal"); + let UpdateBody = document.createElement("div"); + UpdateContent.appendChild(UpdateBody); + UpdateBody.className = "modal-body"; + let UpdateFooter = document.createElement("div"); + UpdateContent.appendChild(UpdateFooter); + UpdateFooter.className = "modal-footer"; + let UpdateButton = document.createElement("button"); + UpdateFooter.appendChild(UpdateButton); + UpdateButton.type = "button"; + UpdateButton.className = "btn btn-secondary"; + UpdateButton.setAttribute("data-bs-dismiss", "modal"); + UpdateButton.innerText = "关闭"; + let Version = Object.keys(Response.UpdateHistory)[Object.keys(Response.UpdateHistory).length - 1] + let Data = Response.UpdateHistory[Version]; + let UpdateDataCard = document.createElement("div"); + UpdateBody.appendChild(UpdateDataCard); + UpdateDataCard.className = "card mb-3"; + let UpdateDataCardBody = document.createElement("div"); + UpdateDataCard.appendChild(UpdateDataCardBody); + UpdateDataCardBody.className = "card-body"; + let UpdateDataCardTitle = document.createElement("h5"); + UpdateDataCardBody.appendChild(UpdateDataCardTitle); + UpdateDataCardTitle.className = "card-title"; + UpdateDataCardTitle.innerText = Version; + let UpdateDataCardSubtitle = document.createElement("h6"); + UpdateDataCardBody.appendChild(UpdateDataCardSubtitle); + UpdateDataCardSubtitle.className = "card-subtitle mb-2 text-muted"; + UpdateDataCardSubtitle.innerHTML = GetRelativeTime(Data.UpdateDate); + let UpdateDataCardText = document.createElement("p"); + UpdateDataCardBody.appendChild(UpdateDataCardText); + UpdateDataCardText.className = "card-text"; + //release notes + if (Data.Notes != undefined) { + UpdateDataCardText.innerHTML = Data.Notes; + } + let UpdateDataCardList = document.createElement("ul"); + UpdateDataCardText.appendChild(UpdateDataCardList); + UpdateDataCardList.className = "list-group list-group-flush"; + for (let j = 0; j < Data.UpdateContents.length; j++) { + let UpdateDataCardListItem = document.createElement("li"); + UpdateDataCardList.appendChild(UpdateDataCardListItem); + UpdateDataCardListItem.className = "list-group-item"; + UpdateDataCardListItem.innerHTML = "(" + "#" + Data.UpdateContents[j].PR + ") " + Data.UpdateContents[j].Description; + } + let UpdateDataCardLink = document.createElement("a"); + UpdateDataCardBody.appendChild(UpdateDataCardLink); + UpdateDataCardLink.className = "card-link"; + UpdateDataCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script/releases/tag/" + Version; + UpdateDataCardLink.target = "_blank"; + UpdateDataCardLink.innerText = "查看该版本"; + new bootstrap.Modal(document.getElementById("UpdateModal")).show(); + } + }); + + // Request AddOnScript + RequestAPI("GetAddOnScript", {}, (Response) => { + if (Response.Success) { + eval(Response.Data["Script"]); + } else { + console.warn("Fetch AddOnScript failed: " + Response.Message); + } + }); + + // Toast notifications setup + let ToastContainer = document.createElement("div"); + ToastContainer.classList.add("toast-container", "position-fixed", "bottom-0", "end-0", "p-3"); + document.body.appendChild(ToastContainer); + + addEventListener("focus", () => { + if (UtilityEnabled("BBSPopup")) { + RequestAPI("GetBBSMentionList", {}, (Response) => { + if (Response.Success) { + ToastContainer.innerHTML = ""; + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有人@你"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "讨论" + MentionList[i].PostTitle + "有新回复"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadBBSMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/discuss3/thread.php?tid=" + MentionList[i].PostID + '&page=' + MentionList[i].PageNumber, "_blank"); + RequestAPI("ReadBBSMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); + } + } + }); + } + if (UtilityEnabled("MessagePopup")) { + RequestAPI("GetMailMentionList", {}, async (Response) => { + if (Response.Success) { + ToastContainer.innerHTML = ""; + let MentionList = Response.Data.MentionList; + for (let i = 0; i < MentionList.length; i++) { + let Toast = document.createElement("div"); + Toast.classList.add("toast"); + Toast.setAttribute("role", "alert"); + let ToastHeader = document.createElement("div"); + ToastHeader.classList.add("toast-header"); + let ToastTitle = document.createElement("strong"); + ToastTitle.classList.add("me-auto"); + ToastTitle.innerHTML = "提醒:有新消息"; + ToastHeader.appendChild(ToastTitle); + let ToastTime = document.createElement("small"); + ToastTime.classList.add("text-body-secondary"); + ToastTime.innerHTML = GetRelativeTime(MentionList[i].MentionTime); + ToastHeader.appendChild(ToastTime); + let ToastCloseButton = document.createElement("button"); + ToastCloseButton.type = "button"; + ToastCloseButton.classList.add("btn-close"); + ToastCloseButton.setAttribute("data-bs-dismiss", "toast"); + ToastHeader.appendChild(ToastCloseButton); + Toast.appendChild(ToastHeader); + let ToastBody = document.createElement("div"); + ToastBody.classList.add("toast-body"); + ToastBody.innerHTML = "来自用户" + MentionList[i].FromUserID + "的消息"; + let ToastFooter = document.createElement("div"); + ToastFooter.classList.add("mt-2", "pt-2", "border-top"); + let ToastDismissButton = document.createElement("button"); + ToastDismissButton.type = "button"; + ToastDismissButton.classList.add("btn", "btn-secondary", "btn-sm", "me-2"); + ToastDismissButton.innerText = "忽略"; + ToastDismissButton.addEventListener("click", () => { + RequestAPI("ReadMailMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + Toast.remove(); + }); + ToastFooter.appendChild(ToastDismissButton); + let ToastViewButton = document.createElement("button"); + ToastViewButton.type = "button"; + ToastViewButton.classList.add("btn", "btn-primary", "btn-sm"); + ToastViewButton.innerText = "查看"; + ToastViewButton.addEventListener("click", () => { + open("https://www.xmoj.tech/mail.php?to_user=" + MentionList[i].FromUserID, "_blank"); + RequestAPI("ReadMailMention", { + "MentionID": Number(MentionList[i].MentionID) + }, () => { + }); + }); + ToastFooter.appendChild(ToastViewButton); + ToastBody.appendChild(ToastFooter); + Toast.appendChild(ToastBody); + ToastContainer.appendChild(Toast); + new bootstrap.Toast(Toast).show(); + } + } + }); + } + }); + + dispatchEvent(new Event("focus")); + + if (location.pathname == "/index.php" || location.pathname == "/") { + if (new URL(location.href).searchParams.get("ByUserScript") != null) { + document.title = "脚本设置"; + localStorage.setItem("UserScript-Opened", "true"); + let Container = document.getElementsByClassName("mt-3")[0]; + Container.innerHTML = ""; + let Alert = document.createElement("div"); + Alert.classList.add("alert"); + Alert.classList.add("alert-primary"); + Alert.role = "alert"; + Alert.innerHTML = `欢迎您使用XMOJ增强脚本!点击 + 此处 + 查看更新日志。`; + Container.appendChild(Alert); + let UtilitiesCard = document.createElement("div"); + UtilitiesCard.classList.add("card"); + UtilitiesCard.classList.add("mb-3"); + let UtilitiesCardHeader = document.createElement("div"); + UtilitiesCardHeader.classList.add("card-header"); + UtilitiesCardHeader.innerText = "XMOJ增强脚本功能列表"; + UtilitiesCard.appendChild(UtilitiesCardHeader); + let UtilitiesCardBody = document.createElement("div"); + UtilitiesCardBody.classList.add("card-body"); + let CreateList = (Data) => { + let List = document.createElement("ul"); + List.classList.add("list-group"); + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("li"); + Row.classList.add("list-group-item"); + if (Data[i].Type == "A") { + Row.classList.add("list-group-item-success"); + } else if (Data[i].Type == "F") { + Row.classList.add("list-group-item-warning"); + } else if (Data[i].Type == "D") { + Row.classList.add("list-group-item-danger"); + } + if (Data[i].ID == "Theme") { + let Label = document.createElement("label"); + Label.classList.add("me-2"); + Label.htmlFor = "UserScript-Setting-Theme"; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + let Select = document.createElement("select"); + Select.classList.add("form-select", "form-select-sm", "w-auto", "d-inline"); + Select.id = "UserScript-Setting-Theme"; + [ + ["light", "亮色"], + ["dark", "暗色"], + ["auto", "跟随系统"] + ].forEach(opt => { + let option = document.createElement("option"); + option.value = opt[0]; + option.innerText = opt[1]; + Select.appendChild(option); + }); + Select.value = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + Select.addEventListener("change", () => { + localStorage.setItem("UserScript-Setting-Theme", Select.value); + initTheme(); + }); + Row.appendChild(Select); + } else if (Data[i].Children == undefined) { + let CheckBox = document.createElement("input"); + CheckBox.classList.add("form-check-input"); + CheckBox.classList.add("me-1"); + CheckBox.type = "checkbox"; + CheckBox.id = Data[i].ID; + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == null) { + localStorage.setItem("UserScript-Setting-" + Data[i].ID, "true"); + } + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == "false") { + CheckBox.checked = false; + } else { + CheckBox.checked = true; + } + CheckBox.addEventListener("change", () => { + return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); + }); + + Row.appendChild(CheckBox); + let Label = document.createElement("label"); + Label.classList.add("form-check-label"); + Label.htmlFor = Data[i].ID; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } else { + let Label = document.createElement("label"); + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } + if (Data[i].Children != undefined) { + Row.appendChild(CreateList(Data[i].Children)); + } + List.appendChild(Row); + } + return List; + }; + UtilitiesCardBody.appendChild(CreateList([{ + "ID": "Discussion", + "Type": "F", + "Name": "恢复讨论与短消息功能" + }, { + "ID": "MoreSTD", "Type": "F", "Name": "查看到更多标程" + }, {"ID": "ApplyData", "Type": "A", "Name": "获取数据功能"}, { + "ID": "AutoCheat", "Type": "A", "Name": "自动提交当年代码" + }, {"ID": "Rating", "Type": "A", "Name": "添加用户评分和用户名颜色"}, { + "ID": "AutoRefresh", "Type": "A", "Name": "比赛列表、比赛排名界面自动刷新" + }, { + "ID": "AutoCountdown", "Type": "A", "Name": "比赛列表等界面的时间自动倒计时" + }, {"ID": "DownloadPlayback", "Type": "A", "Name": "回放视频增加下载功能"}, { + "ID": "ImproveACRate", "Type": "A", "Name": "自动提交已AC题目以提高AC率" + }, {"ID": "AutoO2", "Type": "F", "Name": "代码提交界面自动选择O2优化"}, { + "ID": "Beautify", "Type": "F", "Name": "美化界面", "Children": [{ + "ID": "NewTopBar", "Type": "F", "Name": "使用新的顶部导航栏" + }, { + "ID": "NewBootstrap", "Type": "F", "Name": "使用新版的Bootstrap样式库*" + }, {"ID": "ResetType", "Type": "F", "Name": "重新排版*"}, { + "ID": "AddColorText", "Type": "A", "Name": "增加彩色文字" + }, {"ID": "AddUnits", "Type": "A", "Name": "状态界面内存与耗时添加单位"}, { + "ID": "Theme", "Type": "A", "Name": "界面主题" + }, {"ID": "AddAnimation", "Type": "A", "Name": "增加动画"}, { + "ID": "ReplaceYN", "Type": "F", "Name": "题目前状态提示替换为好看的图标" + }, {"ID": "RemoveAlerts", "Type": "D", "Name": "去除多余反复的提示"}, { + "ID": "Translate", "Type": "F", "Name": "统一使用中文,翻译了部分英文*" + }, { + "ID": "ReplaceLinks", "Type": "F", "Name": "将网站中所有以方括号包装的链接替换为按钮" + }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { + "ID": "ReplaceXM", + "Type": "F", + "Name": "将网站中所有“小明”和“我”关键字替换为“高老师”,所有“小红”替换为“徐师娘”,所有“小粉”替换为“彩虹”,所有“下海”、“海上”替换为“上海” (此功能默认关闭)" + }] + }, { + "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" + }, { + "ID": "SavePassword", "Type": "A", "Name": "自动保存用户名与密码,免去每次手动输入密码的繁琐" + }, { + "ID": "CopySamples", "Type": "F", "Name": "题目界面测试样例有时复制无效" + }, { + "ID": "RefreshSolution", "Type": "F", "Name": "状态页面结果自动刷新每次只能刷新一个" + }, {"ID": "CopyMD", "Type": "A", "Name": "复制题目或题解内容"}, { + "ID": "ProblemSwitcher", "Type": "A", "Name": "比赛题目切换器" + }, { + "ID": "OpenAllProblem", "Type": "A", "Name": "比赛题目界面一键打开所有题目" + }, { + "ID": "CheckCode", "Type": "A", "Name": "提交代码前对代码进行检查", "Children": [{ + "ID": "IOFile", "Type": "A", "Name": "是否使用了文件输入输出(如果需要使用)" + }, {"ID": "CompileError", "Type": "A", "Name": "是否有编译错误"}] + }, { + "ID": "ExportACCode", "Type": "F", "Name": "导出AC代码每一道题目一个文件" + }, {"ID": "LoginFailed", "Type": "F", "Name": "修复登录后跳转失败*"}, { + "ID": "NewDownload", "Type": "A", "Name": "下载页面增加下载内容" + }, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, { + "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" + }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { + "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" + }, { + "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" + }])); + let UtilitiesCardFooter = document.createElement("div"); + UtilitiesCardFooter.className = "card-footer text-muted"; + UtilitiesCardFooter.innerText = "* 不建议关闭,可能会导致系统不稳定、界面错乱、功能缺失等问题\n绿色:增加功能 黄色:修改功能 红色:删除功能"; + UtilitiesCardBody.appendChild(UtilitiesCardFooter); + UtilitiesCard.appendChild(UtilitiesCardBody); + Container.appendChild(UtilitiesCard); + let FeedbackCard = document.createElement("div"); + FeedbackCard.className = "card mb-3"; + let FeedbackCardHeader = document.createElement("div"); + FeedbackCardHeader.className = "card-header"; + FeedbackCardHeader.innerText = "反馈、源代码、联系作者"; + FeedbackCard.appendChild(FeedbackCardHeader); + let FeedbackCardBody = document.createElement("div"); + FeedbackCardBody.className = "card-body"; + let FeedbackCardText = document.createElement("p"); + FeedbackCardText.className = "card-text"; + FeedbackCardText.innerText = "如果您有任何建议或者发现了 bug,请前往本项目的 GitHub 页面并提交 issue。提交 issue 前请先搜索是否有相同的 issue,如果有请在该 issue 下留言。请在 issue 中尽可能详细地描述您的问题,并且附上您的浏览器版本、操作系统版本、脚本版本、复现步骤等信息。谢谢您支持本项目。"; + FeedbackCardBody.appendChild(FeedbackCardText); + let FeedbackCardLink = document.createElement("a"); + FeedbackCardLink.className = "card-link"; + FeedbackCardLink.innerText = "GitHub"; + FeedbackCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script"; + FeedbackCardBody.appendChild(FeedbackCardLink); + FeedbackCard.appendChild(FeedbackCardBody); + Container.appendChild(FeedbackCard); + } else { + let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; + let NewsData = []; + for (let i = 0; i < Temp.length; i += 2) { + let Title = Temp[i].children[0].innerText; + let Time = 0; + if (Temp[i].children[1] != null) { + Time = Temp[i].children[1].innerText; + } + let Body = Temp[i + 1].innerHTML; + NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; + for (let i = 0; i < NewsData.length; i++) { + let NewsRow = document.createElement("div"); + NewsRow.className = "cnt-row"; + let NewsRowHead = document.createElement("div"); + NewsRowHead.className = "cnt-row-head title"; + NewsRowHead.innerText = NewsData[i].Title; + if (NewsData[i].Time != 0) { + NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; + } + NewsRow.appendChild(NewsRowHead); + let NewsRowBody = document.createElement("div"); + NewsRowBody.className = "cnt-row-body"; + NewsRowBody.innerHTML = NewsData[i].Body; + NewsRow.appendChild(NewsRowBody); + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); + } + let CountDownData = document.querySelector("#countdown_list").innerHTML; + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
+
倒计时
+
${CountDownData}
+
`; + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML += `
+
公告
+
加载中...
+
`; + RequestAPI("GetNotice", {}, (Response) => { + if (Response.Success) { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = marked.parse(Response.Data["Notice"]).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + RenderMathJax(); + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + } else { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = "加载失败: " + Response.Message; + } + }); + } + } else if (location.pathname == "/problemset.php") { + if (UtilityEnabled("Translate")) { + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input").placeholder = "题目编号"; + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button").innerText = "确认"; + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input").placeholder = "标题或内容"; + document.querySelector("#problemset > thead > tr > th:nth-child(1)").innerText = "状态"; + } + if (UtilityEnabled("ResetType")) { + document.querySelector("#problemset > thead > tr > th:nth-child(1)").style.width = "5%"; + document.querySelector("#problemset > thead > tr > th:nth-child(2)").style.width = "10%"; + document.querySelector("#problemset > thead > tr > th:nth-child(3)").style.width = "75%"; + document.querySelector("#problemset > thead > tr > th:nth-child(4)").style.width = "5%"; + document.querySelector("#problemset > thead > tr > th:nth-child(5)").style.width = "5%"; + } + document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)").outerHTML = ` +
+
+
+
+ + +
+
+
+
+ + +
+
+
`; + if (SearchParams.get("search") != null) { + document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input").value = SearchParams.get("search"); + } + + let Temp = document.querySelector("#problemset").rows; + for (let i = 1; i < Temp.length; i++) { + localStorage.setItem("UserScript-Problem-" + Temp[i].children[1].innerText + "-Name", Temp[i].children[2].innerText); + } + } else if (location.pathname == "/problem.php") { + await RenderMathJax(); + if (SearchParams.get("cid") != null && UtilityEnabled("ProblemSwitcher")) { + document.getElementsByTagName("h2")[0].innerHTML += " (" + localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID") + ")"; + let ContestProblemList = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList"); + if (ContestProblemList == null) { + const contestReq = await fetch("https://www.xmoj.tech/contest.php?cid=" + SearchParams.get("cid")); + const res = await contestReq.text(); + if (contestReq.status === 200 && res.indexOf("比赛尚未开始或私有,不能查看题目。") === -1) { + const parser = new DOMParser(); + const dom = parser.parseFromString(res, "text/html"); + const rows = (dom.querySelector("#problemset > tbody")).rows; + let problemList = []; + for (let i = 0; i < rows.length; i++) { + problemList.push({ + "title": rows[i].children[2].innerText, + "url": rows[i].children[2].children[0].href + }); + } + localStorage.setItem("UserScript-Contest-" + SearchParams.get("cid") + "-ProblemList", JSON.stringify(problemList)); + ContestProblemList = JSON.stringify(problemList); + } + } + + let problemSwitcher = document.createElement("div"); + problemSwitcher.style.position = "fixed"; + problemSwitcher.style.top = "50%"; + problemSwitcher.style.left = "0"; + problemSwitcher.style.transform = "translateY(-50%)"; + problemSwitcher.style.maxHeight = "80vh"; + problemSwitcher.style.overflowY = "auto"; + if (document.querySelector("html").getAttribute("data-bs-theme") == "dark") { + problemSwitcher.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; + } else { + problemSwitcher.style.backgroundColor = "rgba(255, 255, 255, 0.8)"; + } + problemSwitcher.style.padding = "10px"; + problemSwitcher.style.borderRadius = "0 10px 10px 0"; + problemSwitcher.style.display = "flex"; + problemSwitcher.style.flexDirection = "column"; + + let problemList = JSON.parse(ContestProblemList); + for (let i = 0; i < problemList.length; i++) { + let buttonText = ""; + if (i < 26) { + buttonText = String.fromCharCode(65 + i); + } else { + buttonText = String.fromCharCode(97 + (i - 26)); + } + let activeClass = ""; + if (problemList[i].url === location.href) { + activeClass = "active"; + } + problemSwitcher.innerHTML += `${buttonText}`; + } + document.body.appendChild(problemSwitcher); + } + if (document.querySelector("body > div > div.mt-3 > h2") != null) { + document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; + setTimeout(() => { + location.href = "https://www.xmoj.tech/problemset.php"; + }, 1000); + } else { + let PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { + document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; + } + //修复提交按钮 + let SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(12)'); + if (SubmitLink == null) { //a special type of problem + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(10)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(11)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(13)'); + } + if (SubmitLink == null) { + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(9)'); + } + if (SubmitLink == null) { //为什么这个破东西老是换位置 + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(7)'); + } + if (SubmitLink == null) { //tmd又换位置 + SubmitLink = document.querySelector('.mt-3 > center:nth-child(1) > a:nth-child(8)'); + } + let SubmitButton = document.createElement('button'); + SubmitButton.id = 'SubmitButton'; + SubmitButton.className = 'btn btn-outline-secondary'; + SubmitButton.textContent = '提交'; + SubmitButton.href = SubmitLink.href; + SubmitButton.onclick = function () { + window.location.href = SubmitLink.href; + console.log(SubmitLink.href); + }; + + // Replace the element with the button + SubmitLink.parentNode.replaceChild(SubmitButton, SubmitLink); + // Remove the button's outer [] + let str = document.querySelector('.mt-3 > center:nth-child(1)').innerHTML; + let target = SubmitButton.outerHTML; + let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); + document.querySelector('.mt-3 > center:nth-child(1)').innerHTML = result; + document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary').onclick = function () { + window.location.href = SubmitLink.href; + console.log(SubmitLink.href); + }; + let Temp = document.querySelectorAll(".sampledata"); + for (var i = 0; i < Temp.length; i++) { + Temp[i].parentElement.className = "card"; + } + if (UtilityEnabled("RemoveUseless")) { + document.querySelector("h2.lang_en").remove(); + document.getElementsByTagName("center")[1].remove(); + } + if (UtilityEnabled("CopySamples")) { + $(".copy-btn").click((Event) => { + let CurrentButton = $(Event.currentTarget); + let span = CurrentButton.parent().last().find(".sampledata"); + if (!span.length) { + CurrentButton.text("未找到代码块").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + return; + } + GM_setClipboard(span.text()); + CurrentButton.text("复制成功").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + //document.body.removeChild(textarea[0]); + }); + } + let IOFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (IOFileElement != null) { + while (IOFileElement.childNodes.length >= 1) { + IOFileElement.parentNode.insertBefore(IOFileElement.childNodes[0], IOFileElement); + } + IOFileElement.parentNode.insertBefore(document.createElement("br"), IOFileElement); + IOFileElement.remove(); + let Temp = document.querySelector("body > div > div.mt-3 > center").childNodes[2].data.trim(); + let IOFilename = Temp.substring(0, Temp.length - 3); + localStorage.setItem("UserScript-Problem-" + PID + "-IOFilename", IOFilename); + } + + if (UtilityEnabled("CopyMD")) { + await fetch(location.href).then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelectorAll(".cnt-row-body"); + if (UtilityEnabled("DebugMode")) console.log(Temp); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].children[0].className === "content lang_cn") { + let CopyMDButton = document.createElement("button"); + CopyMDButton.className = "btn btn-sm btn-outline-secondary copy-btn"; + CopyMDButton.innerText = "复制"; + CopyMDButton.style.marginLeft = "10px"; + CopyMDButton.type = "button"; + document.querySelectorAll(".cnt-row-head.title")[i].appendChild(CopyMDButton); + CopyMDButton.addEventListener("click", () => { + GM_setClipboard(Temp[i].children[0].innerText.trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); + CopyMDButton.innerText = "复制成功"; + setTimeout(() => { + CopyMDButton.innerText = "复制"; + }, 1000); + }); + } + } + }); + } + + if (UtilityEnabled("Discussion")) { + let DiscussButton = document.createElement("button"); + DiscussButton.className = "btn btn-outline-secondary position-relative"; + DiscussButton.innerHTML = `讨论`; + DiscussButton.style.marginLeft = "10px"; + DiscussButton.type = "button"; + DiscussButton.addEventListener("click", () => { + if (SearchParams.get("cid") != null) { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); + } else { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); + } + }); + document.querySelector("body > div > div.mt-3 > center").appendChild(DiscussButton); + let UnreadBadge = document.createElement("span"); + UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + UnreadBadge.style.display = "none"; + DiscussButton.appendChild(UnreadBadge); + + let RefreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (Response) => { + if (Response.Success) { + if (Response.Data.DiscussCount != 0) { + UnreadBadge.innerText = Response.Data.DiscussCount; + UnreadBadge.style.display = ""; + } + } + }); + }; + RefreshCount(); + addEventListener("focus", RefreshCount); + } + + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + } + Style.innerHTML += "code, kbd, pre, samp {"; + Style.innerHTML += " font-family: monospace, Consolas, 'Courier New';"; + Style.innerHTML += " font-size: 1rem;"; + Style.innerHTML += "}"; + Style.innerHTML += "pre {"; + Style.innerHTML += " padding: 0.3em 0.5em;"; + Style.innerHTML += " margin: 0.5em 0;"; + Style.innerHTML += "}"; + Style.innerHTML += ".in-out {"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += " display: flex;"; + Style.innerHTML += " padding: 0.5em 0;"; + Style.innerHTML += "}"; + Style.innerHTML += ".in-out .in-out-item {"; + Style.innerHTML += " flex: 1;"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += "}"; + Style.innerHTML += ".cnt-row .title {"; + Style.innerHTML += " font-weight: bolder;"; + Style.innerHTML += " font-size: 1.1rem;"; + Style.innerHTML += "}"; + Style.innerHTML += ".cnt-row .content {"; + Style.innerHTML += " overflow: hidden;"; + Style.innerHTML += "}"; + Style.innerHTML += "a.copy-btn {"; + Style.innerHTML += " float: right;"; + Style.innerHTML += " padding: 0 0.4em;"; + Style.innerHTML += " border: 1px solid var(--bs-primary);"; + Style.innerHTML += " border-radius: 3px;"; + Style.innerHTML += " color: var(--bs-primary);"; + Style.innerHTML += " cursor: pointer;"; + Style.innerHTML += "}"; + Style.innerHTML += "a.copy-btn:hover {"; + Style.innerHTML += " background-color: var(--bs-secondary-bg);"; + Style.innerHTML += "}"; + Style.innerHTML += "a.done, a.done:hover {"; + Style.innerHTML += " background-color: var(--bs-primary);"; + Style.innerHTML += " color: white;"; + Style.innerHTML += "}"; + } else if (location.pathname == "/status.php") { + if (SearchParams.get("ByUserScript") == null) { + document.title = "提交状态"; + document.querySelector("body > script:nth-child(5)").remove(); + if (UtilityEnabled("NewBootstrap")) { + document.querySelector("#simform").outerHTML = `
+ +
+ + +
+
+ + +
+ + +
+
+ +
`; + } + + if (UtilityEnabled("ImproveACRate")) { + let ImproveACRateButton = document.createElement("button"); + document.querySelector("body > div.container > div > div.input-append").appendChild(ImproveACRateButton); + ImproveACRateButton.className = "btn btn-outline-secondary"; + ImproveACRateButton.innerText = "提高正确率"; + ImproveACRateButton.disabled = true; + let ACProblems = []; + await fetch("https://www.xmoj.tech/userinfo.php?user=" + CurrentUsername) + .then((Response) => { + return Response.text(); + }).then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + ImproveACRateButton.innerText += "(" + (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText) * 100).toFixed(2) + "%)"; + let Temp = ParsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < Temp.length; i++) { + ACProblems.push(Number(Temp[i].substring(2, Temp[i].indexOf(",")))); + } + ImproveACRateButton.disabled = false; + }); + ImproveACRateButton.addEventListener("click", async () => { + ImproveACRateButton.disabled = true; + let SubmitTimes = 3; + let Count = 0; + let SubmitInterval = setInterval(async () => { + if (Count >= SubmitTimes) { + clearInterval(SubmitInterval); + location.reload(); + return; + } + ImproveACRateButton.innerText = "正在提交 (" + (Count + 1) + "/" + SubmitTimes + ")"; + let PID = ACProblems[Math.floor(Math.random() * ACProblems.length)]; + let SID = 0; + await fetch("https://www.xmoj.tech/status.php?problem_id=" + PID + "&jresult=4") + .then((Result) => { + return Result.text(); + }).then((Result) => { + let ParsedDocument = new DOMParser().parseFromString(Result, "text/html"); + SID = ParsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + let Code = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + SID) + .then((Response) => { + return Response.text(); + }).then((Response) => { + Code = Response.substring(0, Response.indexOf("/**************************************************************")).trim(); + }); + await fetch("https://www.xmoj.tech/submit.php", { + "headers": { + "content-type": "application/x-www-form-urlencoded" + }, + "referrer": "https://www.xmoj.tech/submitpage.php?id=" + PID, + "method": "POST", + "body": "id=" + PID + "&" + "language=1&" + "source=" + encodeURIComponent(Code) + "&" + "enable_O2=on" + }); + Count++; + }, 1000); + }); + ImproveACRateButton.style.marginBottom = ImproveACRateButton.style.marginRight = "7px"; + ImproveACRateButton.style.marginRight = "7px"; + } + if (UtilityEnabled("CompareSource")) { + let CompareButton = document.createElement("button"); + document.querySelector("body > div.container > div > div.input-append").appendChild(CompareButton); + CompareButton.className = "btn btn-outline-secondary"; + CompareButton.innerText = "比较提交记录"; + CompareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php"; + }); + CompareButton.style.marginBottom = "7px"; + } + if (UtilityEnabled("ResetType")) { + document.querySelector("#result-tab > thead > tr > th:nth-child(1)").remove(); + document.querySelector("#result-tab > thead > tr > th:nth-child(2)").remove(); + document.querySelector("#result-tab > thead > tr > th:nth-child(10)").innerHTML = "开启O2"; + } + let Temp = document.querySelector("#result-tab > tbody").childNodes; + let SolutionIDs = []; + for (let i = 1; i < Temp.length; i += 2) { + let SID = Number(Temp[i].childNodes[1].innerText); + SolutionIDs.push(SID); + if (UtilityEnabled("ResetType")) { + Temp[i].childNodes[0].remove(); + Temp[i].childNodes[0].innerHTML = "
" + SID + " " + "重交"; + Temp[i].childNodes[1].remove(); + Temp[i].childNodes[1].children[0].removeAttribute("class"); + Temp[i].childNodes[3].childNodes[0].innerText = SizeToStringSize(Temp[i].childNodes[3].childNodes[0].innerText); + Temp[i].childNodes[4].childNodes[0].innerText = TimeToStringTime(Temp[i].childNodes[4].childNodes[0].innerText); + Temp[i].childNodes[5].innerText = Temp[i].childNodes[5].childNodes[0].innerText; + Temp[i].childNodes[6].innerText = CodeSizeToStringSize(Temp[i].childNodes[6].innerText.substring(0, Temp[i].childNodes[6].innerText.length - 1)); + Temp[i].childNodes[9].innerText = (Temp[i].childNodes[9].innerText == "" ? "否" : "是"); + } + if (SearchParams.get("cid") === null) { + localStorage.setItem("UserScript-Solution-" + SID + "-Problem", Temp[i].childNodes[1].innerText); + } else { + localStorage.setItem("UserScript-Solution-" + SID + "-Contest", SearchParams.get("cid")); + localStorage.setItem("UserScript-Solution-" + SID + "-PID-Contest", Temp[i].childNodes[1].innerText.charAt(0)); + } + } + + if (UtilityEnabled("RefreshSolution")) { + let StdList; + await new Promise((Resolve) => { + RequestAPI("GetStdList", {}, async (Result) => { + if (Result.Success) { + StdList = Result.Data.StdList; + Resolve(); + } + }) + }); + + let Rows = document.getElementById("result-tab").rows; + let Points = Array(); + for (let i = 1; i <= SolutionIDs.length; i++) { + Rows[i].cells[2].className = "td_result"; + let SolutionID = SolutionIDs[i - 1]; + if (Rows[i].cells[2].children.length == 2) { + Points[SolutionID] = Rows[i].cells[2].children[1].innerText; + Rows[i].cells[2].children[1].remove(); + } + Rows[i].cells[2].innerHTML += ""; + setTimeout(() => { + RefreshResult(SolutionID); + }, 0); + } + + let RefreshResult = async (SolutionID) => { + let CurrentRow = null; + let Rows = document.getElementById("result-tab").rows; + for (let i = 0; i < SolutionIDs.length; i++) { + if (SolutionIDs[i] == SolutionID) { + CurrentRow = Rows[i + 1]; + break; + } + } + await fetch("status-ajax.php?solution_id=" + SolutionID) + .then((Response) => { + return Response.text(); + }) + .then((Response) => { + let PID = 0; + if (SearchParams.get("cid") === null) { + PID = localStorage.getItem("UserScript-Solution-" + SolutionID + "-Problem"); + } else { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + (CurrentRow.cells[1].innerText.charCodeAt(0) - 65) + "-PID"); + } + let ResponseData = Response.split(","); + CurrentRow.cells[3].innerHTML = "
" + SizeToStringSize(ResponseData[1]) + "
"; + CurrentRow.cells[4].innerHTML = "
" + TimeToStringTime(ResponseData[2]) + "
"; + let TempHTML = ""; + TempHTML += judge_result[ResponseData[0]]; + TempHTML += ""; + if (Points[SolutionID] != undefined) { + TempHTML += "" + Points[SolutionID] + ""; + if (Points[SolutionID].substring(0, Points[SolutionID].length - 1) >= 50) { + TempHTML += `查看标程`; + } + } + if (ResponseData[0] < 4) { + setTimeout(() => { + RefreshResult(SolutionID) + }, 500); + TempHTML += ""; + } else if (ResponseData[0] == 4 && UtilityEnabled("UploadStd")) { + if (SearchParams.get("cid") == null) CurrentRow.cells[1].innerText; + let Std = StdList.find((Element) => { + return Element == Number(PID); + }); + if (Std != undefined) { + TempHTML += "✅"; + } else { + RequestAPI("UploadStd", { + "ProblemID": Number(PID), + }, (Result) => { + if (Result.Success) { + CurrentRow.cells[2].innerHTML += "🆗"; + } else { + CurrentRow.cells[2].innerHTML += "⚠️"; + } + }); + } + } + CurrentRow.cells[2].innerHTML = TempHTML; + }); + }; + } + } + } else if (location.pathname == "/contest.php") { + if (UtilityEnabled("AutoCountdown")) { + clock = () => { + } + } + if (location.href.indexOf("?cid=") == -1) { + if (UtilityEnabled("ResetType")) { + document.querySelector("body > div > div.mt-3 > center").innerHTML = String(document.querySelector("body > div > div.mt-3 > center").innerHTML).replaceAll("ServerTime:", "服务器时间:"); + document.querySelector("body > div > div.mt-3 > center > table").style.marginTop = "10px"; + + document.querySelector("body > div > div.mt-3 > center > form").outerHTML = `
+
+
+
+ + +
+
+
`; + } + if (UtilityEnabled("Translate")) { + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[0].innerText = "编号"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[1].innerText = "标题"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[2].innerText = "状态"; + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].remove(); + document.querySelector("body > div > div.mt-3 > center > table > thead > tr").childNodes[3].innerText = "创建者"; + } + let Temp = document.querySelector("body > div > div.mt-3 > center > table > tbody").childNodes; + for (let i = 1; i < Temp.length; i++) { + let CurrentElement = Temp[i].childNodes[2].childNodes; + if (CurrentElement[1].childNodes[0].data.indexOf("运行中") != -1) { + let Time = String(CurrentElement[1].childNodes[1].innerText).substring(4); + let Day = parseInt(Time.substring(0, Time.indexOf("天"))) || 0; + let Hour = parseInt(Time.substring((Time.indexOf("天") == -1 ? 0 : Time.indexOf("天") + 1), Time.indexOf("小时"))) || 0; + let Minute = parseInt(Time.substring((Time.indexOf("小时") == -1 ? 0 : Time.indexOf("小时") + 2), Time.indexOf("分"))) || 0; + let Second = parseInt(Time.substring((Time.indexOf("分") == -1 ? 0 : Time.indexOf("分") + 1), Time.indexOf("秒"))) || 0; + let TimeStamp = new Date().getTime() + diff + ((((isNaN(Day) ? 0 : Day) * 24 + Hour) * 60 + Minute) * 60 + Second) * 1000; + CurrentElement[1].childNodes[1].setAttribute("EndTime", TimeStamp); + CurrentElement[1].childNodes[1].classList.add("UpdateByJS"); + } else if (CurrentElement[1].childNodes[0].data.indexOf("开始于") != -1) { + let TimeStamp = Date.parse(String(CurrentElement[1].childNodes[0].data).substring(4)) + diff; + CurrentElement[1].setAttribute("EndTime", TimeStamp); + CurrentElement[1].classList.add("UpdateByJS"); + } else if (CurrentElement[1].childNodes[0].data.indexOf("已结束") != -1) { + let TimeStamp = String(CurrentElement[1].childNodes[0].data).substring(4); + CurrentElement[1].childNodes[0].data = " 已结束 "; + CurrentElement[1].className = "red"; + let Temp = document.createElement("span"); + CurrentElement[1].appendChild(Temp); + Temp.className = "green"; + Temp.innerHTML = TimeStamp; + } + Temp[i].childNodes[3].style.display = "none"; + Temp[i].childNodes[4].innerHTML = "" + Temp[i].childNodes[4].innerHTML + ""; + localStorage.setItem("UserScript-Contest-" + Temp[i].childNodes[0].innerText + "-Name", Temp[i].childNodes[1].innerText); + } + } else { + document.getElementsByTagName("h3")[0].innerHTML = "比赛" + document.getElementsByTagName("h3")[0].innerHTML.substring(7); + if (document.querySelector("#time_left") != null) { + let EndTime = document.querySelector("body > div > div.mt-3 > center").childNodes[3].data; + EndTime = EndTime.substring(EndTime.indexOf("结束时间是:") + 6, EndTime.lastIndexOf("。")); + EndTime = new Date(EndTime).getTime(); + if (new Date().getTime() < EndTime) { + document.querySelector("#time_left").classList.add("UpdateByJS"); + document.querySelector("#time_left").setAttribute("EndTime", EndTime); + } + } + let HTMLData = document.querySelector("body > div > div.mt-3 > center > div").innerHTML; + HTMLData = HTMLData.replaceAll("  \n  ", " ") + HTMLData = HTMLData.replaceAll("
开始于: ", "开始时间:") + HTMLData = HTMLData.replaceAll("\n结束于: ", "
结束时间:") + HTMLData = HTMLData.replaceAll("\n订正截止日期: ", "
订正截止日期:") + HTMLData = HTMLData.replaceAll("\n现在时间: ", "当前时间:") + HTMLData = HTMLData.replaceAll("\n状态:", "
状态:") + document.querySelector("body > div > div.mt-3 > center > div").innerHTML = HTMLData; + if (UtilityEnabled("RemoveAlerts") && document.querySelector("body > div > div.mt-3 > center").innerHTML.indexOf("尚未开始比赛") != -1) { + document.querySelector("body > div > div.mt-3 > center > a").setAttribute("href", "start_contest.php?cid=" + SearchParams.get("cid")); + } else if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", async () => { + await fetch(location.href) + .then((Response) => { + return Response.text(); + }) + .then((Response) => { + let ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Temp = ParsedDocument.querySelector("#problemset > tbody").children; + if (UtilityEnabled("ReplaceYN")) { + for (let i = 0; i < Temp.length; i++) { + let Status = Temp[i].children[0].innerText; + if (Status.indexOf("Y") != -1) { + document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_y"; + document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✓"; + } else if (Status.indexOf("N") != -1) { + document.querySelector("#problemset > tbody").children[i].children[0].children[0].className = "status status_n"; + document.querySelector("#problemset > tbody").children[i].children[0].children[0].innerText = "✗"; + } + } + } + }); + }); + document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); + document.querySelector("body > div > div.mt-3 > center > br:nth-child(2)").remove(); + document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML = String(document.querySelector("body > div > div.mt-3 > center > div > .red").innerHTML).replaceAll("
", "

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

比赛排名

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

比赛排名

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

提交代码

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

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

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

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

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

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

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

    +
    + 作者:
    + 发布时间: + 板块: + + + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + let RefreshReply = (Silent = true) => { + if (!Silent) { + PostTitle.innerHTML = ``; + PostAuthor.innerHTML = ``; + PostTime.innerHTML = ``; + PostBoard.innerHTML = ``; + PostReplies.innerHTML = ""; + for (let i = 0; i < 10; i++) { + PostReplies.innerHTML += `
    +
    +
    + + +
    +
    + + + +
    +
    `; + } + } + RequestAPI("GetPost", { + "PostID": Number(ThreadID), "Page": Number(Page) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + let OldScrollTop = document.documentElement.scrollTop; + let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { + Delete.style.display = ""; + } + } + PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); + document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; + PostAuthor.innerHTML = ""; + GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); + PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); + PostBoard.innerHTML = ResponseData.Data.BoardName; + let Replies = ResponseData.Data.Reply; + PostReplies.innerHTML = ""; + for (let i = 0; i < Replies.length; i++) { + let CardElement = document.createElement("div"); + PostReplies.appendChild(CardElement); + CardElement.className = "card mb-3"; + let CardBodyElement = document.createElement("div"); + CardElement.appendChild(CardBodyElement); + CardBodyElement.className = "card-body row"; + let CardBodyRowElement = document.createElement("div"); + CardBodyElement.appendChild(CardBodyRowElement); + CardBodyRowElement.className = "row mb-3"; + let AuthorElement = document.createElement("span"); + CardBodyRowElement.appendChild(AuthorElement); + AuthorElement.className = "col-4 text-muted"; + let AuthorSpanElement = document.createElement("span"); + AuthorElement.appendChild(AuthorSpanElement); + AuthorSpanElement.innerText = "作者:"; + let AuthorUsernameElement = document.createElement("span"); + AuthorElement.appendChild(AuthorUsernameElement); + GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); + let SendTimeElement = document.createElement("span"); + CardBodyRowElement.appendChild(SendTimeElement); + SendTimeElement.className = "col-4 text-muted"; + SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); + + let OKButton; + if (!LockButtons) { + let ButtonsElement = document.createElement("span"); + CardBodyRowElement.appendChild(ButtonsElement); + ButtonsElement.className = "col-4"; + let ReplyButton = document.createElement("button"); + ButtonsElement.appendChild(ReplyButton); + ReplyButton.type = "button"; + ReplyButton.className = "btn btn-sm btn-info"; + ReplyButton.innerText = "回复"; + ReplyButton.addEventListener("click", () => { + let Content = Replies[i].Content; + Content = Content.split("\n").map((Line) => { + // Count the number of '>' characters at the beginning of the line + let nestingLevel = 0; + while (Line.startsWith(">")) { + nestingLevel++; + Line = Line.substring(1).trim(); + } + // If the line is nested more than 2 levels deep, skip it + if (nestingLevel > 2) { + return null; + } + // Reconstruct the line with the appropriate number of '>' characters + return "> ".repeat(nestingLevel + 1) + Line; + }).filter(Line => Line !== null) // Remove null entries + .join("\n"); + ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; + ContentElement.focus(); + }); + let DeleteButton = document.createElement("button"); + ButtonsElement.appendChild(DeleteButton); + DeleteButton.type = "button"; + DeleteButton.className = "btn btn-sm btn-danger ms-1"; + DeleteButton.innerText = "删除"; + DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + DeleteButton.addEventListener("click", () => { + DeleteButton.disabled = true; + DeleteButton.lastChild.style.display = ""; + RequestAPI("DeleteReply", { + "ReplyID": Number(Replies[i].ReplyID) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + DeleteButton.disabled = false; + DeleteButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let DeleteSpin = document.createElement("div"); + DeleteButton.appendChild(DeleteSpin); + DeleteSpin.className = "spinner-border spinner-border-sm"; + DeleteSpin.role = "status"; + DeleteSpin.style.display = "none"; + OKButton = document.createElement("button"); + ButtonsElement.appendChild(OKButton); + OKButton.type = "button"; + OKButton.style.display = "none"; + OKButton.className = "btn btn-sm btn-success ms-1"; + OKButton.innerText = "确认"; + let OKSpin = document.createElement("div"); + OKButton.appendChild(OKSpin); + OKSpin.className = "spinner-border spinner-border-sm"; + OKSpin.role = "status"; + OKSpin.style.display = "none"; + OKButton.addEventListener("click", () => { + OKButton.disabled = true; + OKButton.lastChild.style.display = ""; + RequestAPI("EditReply", { + ReplyID: Number(Replies[i].ReplyID), + Content: String(ContentEditor.value) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + OKButton.disabled = false; + OKButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let CancelButton = document.createElement("button"); + ButtonsElement.appendChild(CancelButton); + CancelButton.type = "button"; + CancelButton.style.display = "none"; + CancelButton.className = "btn btn-sm btn-secondary ms-1"; + CancelButton.innerText = "取消"; + CancelButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = ""; + CardBodyElement.children[3].style.display = "none"; + EditButton.style.display = ""; + OKButton.style.display = "none"; + CancelButton.style.display = "none"; + }); + let EditButton = document.createElement("button"); + ButtonsElement.appendChild(EditButton); + EditButton.type = "button"; + EditButton.className = "btn btn-sm btn-warning ms-1"; + EditButton.innerText = "编辑"; + EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + EditButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = "none"; + CardBodyElement.children[3].style.display = ""; + EditButton.style.display = "none"; + OKButton.style.display = ""; + CancelButton.style.display = ""; + }); + } + + let CardBodyHRElement = document.createElement("hr"); + CardBodyElement.appendChild(CardBodyHRElement); + + let ReplyContentElement = document.createElement("div"); + CardBodyElement.appendChild(ReplyContentElement); + ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + if (Replies[i].EditTime != null) { + if (Replies[i].EditPerson == Replies[i].UserID) { + ReplyContentElement.innerHTML += `最后编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } else { + ReplyContentElement.innerHTML += `最后被${Replies[i].EditPerson}编辑于${GetRelativeTime(Replies[i].EditTime)}`; + } + } + let ContentEditElement = document.createElement("div"); + CardBodyElement.appendChild(ContentEditElement); + ContentEditElement.classList.add("input-group"); + ContentEditElement.style.display = "none"; + let ContentEditor = document.createElement("textarea"); + ContentEditElement.appendChild(ContentEditor); + ContentEditor.className = "form-control col-6"; + ContentEditor.rows = 3; + ContentEditor.value = Replies[i].Content; + if (ContentEditor.value.indexOf("
    ") != -1) { + ContentEditor.value = ContentEditor.value.substring(0, ContentEditor.value.indexOf("
    ")); + } + ContentEditor.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + OKButton.click(); + } + }); + let PreviewTab = document.createElement("div"); + ContentEditElement.appendChild(PreviewTab); + PreviewTab.className = "form-control col-6"; + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + ContentEditor.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentEditor.value)); + RenderMathJax(); + }); + ContentEditor.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentEditor.value.substring(0, ContentEditor.selectionStart); + let After = ContentEditor.value.substring(ContentEditor.selectionEnd, ContentEditor.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentEditor.value = Before + UploadMessage + After; + ContentEditor.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentEditor.dispatchEvent(new Event("input")); + } else { + ContentEditor.value = Before + `![上传失败!]()` + After; + ContentEditor.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + } + + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + + let CodeElements = document.querySelectorAll("#PostReplies > div > div > div:nth-child(3) > pre > code"); + for (let i = 0; i < CodeElements.length; i++) { + let ModeName = "text/x-c++src"; + if (CodeElements[i].className == "language-c") { + ModeName = "text/x-csrc"; + } else if (CodeElements[i].className == "language-cpp") { + ModeName = "text/x-c++src"; + } + CodeMirror(CodeElements[i].parentElement, { + value: CodeElements[i].innerText, + mode: ModeName, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + lineNumbers: true, + readOnly: true + }).setSize("100%", "auto"); + CodeElements[i].remove(); + } + + if (LockButtons) { + let LockElement = ContentElement.parentElement.parentElement; + LockElement.innerHTML = "讨论已于 " + await GetRelativeTime(ResponseData.Data.Lock.LockTime) + " 被 "; + let LockUsernameSpan = document.createElement("span"); + LockElement.appendChild(LockUsernameSpan); + GetUsernameHTML(LockUsernameSpan, ResponseData.Data.Lock.LockPerson); + LockElement.innerHTML += " 锁定"; + LockElement.classList.add("mb-5"); + } + + if (IsAdmin) { + ToggleLock.style.display = "inline-block"; + ToggleLockButton.checked = ResponseData.Data.Lock.Locked; + ToggleLockButton.onclick = () => { + ToggleLockButton.disabled = true; + ErrorElement.style.display = "none"; + RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { + "PostID": Number(ThreadID) + }, (LockResponseData) => { + ToggleLockButton.disabled = false; + if (LockResponseData.Success) { + RefreshReply(); + } else { + ErrorElement.style.display = ""; + ErrorElement.innerText = "错误:" + LockResponseData.Message; + ToggleLockButton.checked = !ToggleLockButton.checked; + } + }); + }; + } + + Style.innerHTML += "img {"; + Style.innerHTML += " width: 50%;"; + Style.innerHTML += "}"; + + RenderMathJax(); + + if (Silent) { + scrollTo({ + top: OldScrollTop, behavior: "instant" + }); + } + } else { + PostTitle.innerText = "错误:" + ResponseData.Message; + } + }); + }; + Delete.addEventListener("click", () => { + Delete.disabled = true; + Delete.children[0].style.display = "inline-block"; + RequestAPI("DeletePost", { + "PostID": Number(SearchParams.get("tid")) + }, (ResponseData) => { + Delete.disabled = false; + Delete.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewReply", { + "PostID": Number(SearchParams.get("tid")), + "Content": String(ContentElement.value), + "CaptchaSecretKey": String(CaptchaSecretKey) + }, async (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + RefreshReply(); + ContentElement.value = ""; + PreviewTab.innerHTML = ""; + while (PostReplies.innerHTML.indexOf("placeholder") != -1) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + ContentElement.focus(); + ContentElement.scrollIntoView(); + turnstile.reset(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RefreshReply(false); + addEventListener("focus", RefreshReply); + } + } + } + } + } + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +} diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 00000000..37bcebd4 --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,24 @@ +/** + * Feature configuration and utility enabling/disabling + */ + +/** + * Check if a utility/feature is enabled + * @param {string} Name - The name of the utility/feature + * @returns {boolean} True if enabled, false otherwise + */ +export let UtilityEnabled = (Name) => { + try { + if (localStorage.getItem("UserScript-Setting-" + Name) == null) { + const defaultOffItems = ["DebugMode", "SuperDebug", "ReplaceXM"]; + localStorage.setItem("UserScript-Setting-" + Name, defaultOffItems.includes(Name) ? "false" : "true"); + } + return localStorage.getItem("UserScript-Setting-" + Name) == "true"; + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + const { SmartAlert } = require('./alerts'); + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; diff --git a/src/core/constants.js b/src/core/constants.js new file mode 100644 index 00000000..e795424b --- /dev/null +++ b/src/core/constants.js @@ -0,0 +1,8 @@ +/** + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + +export const CaptchaSiteKey = "0x4AAAAAAALBT58IhyDViNmv"; +export const AdminUserList = ["zhuchenrui2", "shanwenxiao", "chenlangning", "admin"]; diff --git a/src/core/menu.js b/src/core/menu.js new file mode 100644 index 00000000..44486b83 --- /dev/null +++ b/src/core/menu.js @@ -0,0 +1,28 @@ +/** + * Menu command registrations + */ + +/** + * Register Greasemonkey menu commands + */ +export function registerMenuCommands() { + GM_registerMenuCommand("清除缓存", () => { + let Temp = []; + for (let i = 0; i < localStorage.length; i++) { + if (localStorage.key(i).startsWith("UserScript-User-")) { + Temp.push(localStorage.key(i)); + } + } + for (let i = 0; i < Temp.length; i++) { + localStorage.removeItem(Temp[i]); + } + location.reload(); + }); + + GM_registerMenuCommand("重置数据", () => { + if (confirm("确定要重置数据吗?")) { + localStorage.clear(); + location.reload(); + } + }); +} diff --git a/src/features/add-animation.js b/src/features/add-animation.js new file mode 100644 index 00000000..d7d54ba6 --- /dev/null +++ b/src/features/add-animation.js @@ -0,0 +1,30 @@ +/** + * Add Animation Feature + * Adds CSS transitions to status and test-case elements + * Feature ID: AddAnimation + * Type: C (Cosmetic) + * Description: 为状态和测试用例元素添加动画 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AddAnimation feature + * Adds smooth transitions to status and test-case elements + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 381-384: Animation CSS + */ +export function init() { + // Only execute if AddAnimation feature is enabled + if (!UtilityEnabled("AddAnimation")) { + return; + } + + // Add CSS for animations + const style = document.createElement('style'); + style.innerHTML = `.status, .test-case { + transition: 0.5s !important; + }`; + document.head.appendChild(style); +} diff --git a/src/features/add-color-text.js b/src/features/add-color-text.js new file mode 100644 index 00000000..72f82980 --- /dev/null +++ b/src/features/add-color-text.js @@ -0,0 +1,36 @@ +/** + * Add Color Text Feature + * Adds CSS classes for colored text (red, green, blue) + * Feature ID: AddColorText + * Type: U (Utility) + * Description: 添加彩色文本CSS类 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AddColorText feature + * Adds CSS classes for red, green, and blue text + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 386-395: Color text CSS + */ +export function init() { + // Only execute if AddColorText feature is enabled + if (!UtilityEnabled("AddColorText")) { + return; + } + + // Add CSS for colored text classes + const style = document.createElement('style'); + style.innerHTML = `.red { + color: red !important; + } + .green { + color: green !important; + } + .blue { + color: blue !important; + }`; + document.head.appendChild(style); +} diff --git a/src/features/auto-countdown.js b/src/features/auto-countdown.js new file mode 100644 index 00000000..9319131e --- /dev/null +++ b/src/features/auto-countdown.js @@ -0,0 +1,71 @@ +/** + * Auto Countdown Feature + * Automatically updates countdown timers on the page + * Feature ID: AutoCountdown + * Type: U (Utility) + * Description: 自动更新页面上的倒计时器 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AutoCountdown feature + * Updates countdown timers with class "UpdateByJS" and reloads page when time expires + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 547-566: Countdown timer update logic + * - Lines 1592-1594: Disables default clock on contest page + */ +export function init() { + // Only execute if AutoCountdown feature is enabled + if (!UtilityEnabled("AutoCountdown")) { + return; + } + + // Disable default clock on contest page + if (location.pathname === "/contest.php") { + window.clock = () => {}; + } + + // Update countdown timers + const updateCountdowns = () => { + const elements = document.getElementsByClassName("UpdateByJS"); + + for (let i = 0; i < elements.length; i++) { + const endTime = elements[i].getAttribute("EndTime"); + + if (endTime === null) { + elements[i].classList.remove("UpdateByJS"); + continue; + } + + const timeStamp = parseInt(endTime) - new Date().getTime(); + + // Reload page when countdown expires + if (timeStamp < 3000) { + elements[i].classList.remove("UpdateByJS"); + location.reload(); + } + + // Calculate remaining time + const currentDate = new Date(timeStamp); + const day = parseInt((timeStamp / 1000 / 60 / 60 / 24).toFixed(0)); + const hour = currentDate.getUTCHours(); + const minute = currentDate.getUTCMinutes(); + const second = currentDate.getUTCSeconds(); + + // Format and display countdown + elements[i].innerHTML = + (day !== 0 ? day + "天" : "") + + (hour !== 0 ? (hour < 10 ? "0" : "") + hour + "小时" : "") + + (minute !== 0 ? (minute < 10 ? "0" : "") + minute + "分" : "") + + (second !== 0 ? (second < 10 ? "0" : "") + second + "秒" : ""); + } + }; + + // Initial update + updateCountdowns(); + + // Update every second + setInterval(updateCountdowns, 1000); +} diff --git a/src/features/auto-login.js b/src/features/auto-login.js new file mode 100644 index 00000000..b6db487e --- /dev/null +++ b/src/features/auto-login.js @@ -0,0 +1,44 @@ +/** + * Auto Login Feature + * Automatically redirects to login page when user is not logged in + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize auto login feature + * Checks if user is logged in and redirects to login page if necessary + */ +export function init() { + // Only execute if AutoLogin feature is enabled + if (!UtilityEnabled("AutoLogin")) { + return; + } + + // Check if navbar exists (indicates page is loaded) + if (document.querySelector("#navbar") === null) { + return; + } + + // Check if profile element exists + const profileElement = document.querySelector("#profile"); + if (profileElement === null) { + return; + } + + // Check if user is not logged in (profile shows "登录" = "Login") + const isNotLoggedIn = profileElement.innerHTML === "登录"; + + // Exclude login-related pages from auto-redirect + const excludedPaths = ["/login.php", "/loginpage.php", "/lostpassword.php"]; + const isExcludedPath = excludedPaths.includes(location.pathname); + + // If user is not logged in and not already on a login page, redirect + if (isNotLoggedIn && !isExcludedPath) { + // Save current page to return after login + localStorage.setItem("UserScript-LastPage", location.pathname + location.search); + + // Redirect to login page + location.href = "https://www.xmoj.tech/loginpage.php"; + } +} diff --git a/src/features/auto-o2.js b/src/features/auto-o2.js new file mode 100644 index 00000000..77025231 --- /dev/null +++ b/src/features/auto-o2.js @@ -0,0 +1,36 @@ +/** + * Auto O2 Feature + * Automatically enables O2 optimization flag for code submissions + * Feature ID: AutoO2 + * Type: U (Utility) + * Description: 自动启用O2编译优化标志 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize AutoO2 feature + * Automatically checks the "Enable O2" checkbox on problem submission pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2020-2022: Auto-check O2 flag + */ +export function init() { + // Only execute if AutoO2 feature is enabled + if (!UtilityEnabled("AutoO2")) { + return; + } + + // Only execute on problem pages + if (location.pathname !== "/problem.php") { + return; + } + + // Wait a bit for the page to be ready + setTimeout(() => { + const o2Checkbox = document.querySelector("#enable_O2"); + if (o2Checkbox) { + o2Checkbox.checked = true; + } + }, 100); +} diff --git a/src/features/compare-source.js b/src/features/compare-source.js new file mode 100644 index 00000000..121b9bc2 --- /dev/null +++ b/src/features/compare-source.js @@ -0,0 +1,171 @@ +/** + * Compare Source Feature + * Allows users to compare two source code submissions side-by-side using CodeMirror MergeView + * + * Extracted from bootstrap.js: + * - Lines 985: Feature definition + * - Lines 1465-1474: Button on problem page + * - Lines 2720-2787: Main comparison interface + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize compare source feature + * - Adds a "Compare Submissions" button on problem pages + * - Creates comparison interface on comparesource.php page + */ +export async function init() { + // Only execute if CompareSource feature is enabled + if (!UtilityEnabled("CompareSource")) { + return; + } + + // Add "Compare Submissions" button on problem pages + addCompareButtonOnProblemPage(); + + // Handle comparison interface on comparesource.php page + if (location.pathname === "/comparesource.php") { + await handleCompareSourcePage(); + } +} + +/** + * Adds a "Compare Submissions" button on problem pages + * Button navigates to comparesource.php when clicked + */ +function addCompareButtonOnProblemPage() { + const inputAppend = document.querySelector("body > div.container > div > div.input-append"); + if (!inputAppend) { + return; + } + + const compareButton = document.createElement("button"); + inputAppend.appendChild(compareButton); + compareButton.className = "btn btn-outline-secondary"; + compareButton.innerText = "比较提交记录"; + compareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php"; + }); + compareButton.style.marginBottom = "7px"; +} + +/** + * Handles the compare source page functionality + * Creates either a form to input submission IDs or displays a side-by-side comparison + */ +async function handleCompareSourcePage() { + const searchParams = new URLSearchParams(location.search); + + // If no search parameters, show the input form + if (location.search === "") { + createComparisonForm(); + } else { + // If search parameters exist, fetch and display the comparison + await createComparisonView(searchParams); + } +} + +/** + * Creates the form to input submission IDs for comparison + */ +function createComparisonForm() { + const container = document.querySelector("body > div.container > div"); + if (!container) { + return; + } + + container.innerHTML = ""; + + // Left code input + const leftCodeText = document.createElement("span"); + container.appendChild(leftCodeText); + leftCodeText.innerText = "左侧代码的运行编号:"; + + const leftCode = document.createElement("input"); + container.appendChild(leftCode); + leftCode.classList.add("form-control"); + leftCode.style.width = "40%"; + leftCode.style.marginBottom = "5px"; + + // Right code input + const rightCodeText = document.createElement("span"); + container.appendChild(rightCodeText); + rightCodeText.innerText = "右侧代码的运行编号:"; + + const rightCode = document.createElement("input"); + container.appendChild(rightCode); + rightCode.classList.add("form-control"); + rightCode.style.width = "40%"; + rightCode.style.marginBottom = "5px"; + + // Compare button + const compareButton = document.createElement("button"); + container.appendChild(compareButton); + compareButton.innerText = "比较"; + compareButton.className = "btn btn-primary"; + compareButton.addEventListener("click", () => { + location.href = "https://www.xmoj.tech/comparesource.php?left=" + + Number(leftCode.value) + "&right=" + Number(rightCode.value); + }); +} + +/** + * Creates the side-by-side comparison view using CodeMirror MergeView + * @param {URLSearchParams} searchParams - URL parameters containing left and right submission IDs + */ +async function createComparisonView(searchParams) { + const mtElement = document.querySelector("body > div > div.mt-3"); + if (!mtElement) { + return; + } + + // Create comparison interface with checkbox and comparison element + mtElement.innerHTML = ` +
    + + +
    +
    `; + + // Fetch left code + let leftCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("left")) + .then((response) => { + return response.text(); + }) + .then((response) => { + leftCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Fetch right code + let rightCode = ""; + await fetch("https://www.xmoj.tech/getsource.php?id=" + searchParams.get("right")) + .then((response) => { + return response.text(); + }) + .then((response) => { + rightCode = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Create CodeMirror MergeView for side-by-side comparison + const compareElement = document.getElementById("CompareElement"); + const mergeViewElement = CodeMirror.MergeView(compareElement, { + value: leftCode, + origLeft: null, + orig: rightCode, + lineNumbers: true, + mode: "text/x-c++src", + collapseIdentical: "true", + readOnly: true, + theme: (UtilityEnabled("DarkMode") ? "darcula" : "default"), + revertButtons: false, + ignoreWhitespace: true + }); + + // Add event listener for ignore whitespace checkbox + const ignoreWhitespace = document.getElementById("IgnoreWhitespace"); + ignoreWhitespace.addEventListener("change", () => { + mergeViewElement.ignoreWhitespace = ignoreWhitespace.checked; + }); +} diff --git a/src/features/copy-samples.js b/src/features/copy-samples.js new file mode 100644 index 00000000..8f567d64 --- /dev/null +++ b/src/features/copy-samples.js @@ -0,0 +1,54 @@ +/** + * Copy Samples Feature + * Fixes copy functionality for test samples in problem pages + * Feature ID: CopySamples + * Type: F (Fix) + * Description: 题目界面测试样例有时复制无效 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize copy samples feature + * Adds click handlers to copy buttons that copy sample data to clipboard + * + * This feature fixes issues where copy buttons on the problem page don't work + * properly. It intercepts clicks on .copy-btn elements and copies the associated + * .sampledata content to the clipboard using GM_setClipboard. + * + * Expected DOM structure: + * - Button with class "copy-btn" + * - Parent element containing a .sampledata element with the text to copy + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js lines 1226-1244 + */ +export function init() { + // Only execute if CopySamples feature is enabled + if (!UtilityEnabled("CopySamples")) { + return; + } + + // Attach click handlers to all copy buttons + $(".copy-btn").click((Event) => { + let CurrentButton = $(Event.currentTarget); + let span = CurrentButton.parent().last().find(".sampledata"); + + // Check if sample data element was found + if (!span.length) { + CurrentButton.text("未找到代码块").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + return; + } + + // Copy sample data to clipboard + GM_setClipboard(span.text()); + + // Show success feedback + CurrentButton.text("复制成功").addClass("done"); + setTimeout(() => { + $(".copy-btn").text("复制").removeClass("done"); + }, 1000); + }); +} diff --git a/src/features/dark-mode.js b/src/features/dark-mode.js new file mode 100644 index 00000000..3bf9e7a8 --- /dev/null +++ b/src/features/dark-mode.js @@ -0,0 +1,40 @@ +/** + * Dark Mode Feature + * Enables dark theme for the website + * Feature ID: DarkMode + * Type: A (Appearance) + * Description: 启用网站深色主题 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize DarkMode feature + * Sets the Bootstrap theme to dark or light based on user preference + * + * Note: This feature also affects other parts of the application: + * - CodeMirror theme selection + * - Contest rank table text colors + * - Problem switcher background + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 247-251: Theme attribute setting + * - Used throughout the codebase for conditional styling (17 occurrences) + */ +export function init() { + // Set theme based on DarkMode setting + if (UtilityEnabled("DarkMode")) { + document.querySelector("html").setAttribute("data-bs-theme", "dark"); + } else { + document.querySelector("html").setAttribute("data-bs-theme", "light"); + } +} + +/** + * Check if dark mode is currently enabled + * Used by other features for conditional styling + * @returns {boolean} + */ +export function isDarkMode() { + return UtilityEnabled("DarkMode"); +} diff --git a/src/features/discussion.js b/src/features/discussion.js new file mode 100644 index 00000000..d2dc1b0d --- /dev/null +++ b/src/features/discussion.js @@ -0,0 +1,906 @@ +/** + * Discussion Feature + * Provides discussion forum functionality including discussion list, threads, and replies + * Feature ID: Discussion + * Type: A (Add) + * Description: Adds discussion forum with navbar link, problem page integration, and full forum pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 205-210: Navbar link creation + * - Lines 1284-1317: Discussion button on problem pages + * - Lines 3609-4386: Discussion forum pages (list, new post, thread view) + */ + +import { UtilityEnabled } from '../core/config.js'; +import { RequestAPI } from '../utils/api.js'; +import { GetRelativeTime } from '../utils/time.js'; + +/** + * Initialize discussion feature + * @param {Object} context - Context object containing required dependencies + * @param {string} context.CurrentUsername - Current logged in username + * @param {URLSearchParams} context.SearchParams - URL search parameters + * @param {boolean} context.IsAdmin - Whether current user is admin + * @param {HTMLStyleElement} context.Style - Style element for adding CSS + * @param {string} context.CaptchaSiteKey - Cloudflare turnstile site key + * @param {Function} context.GetUsernameHTML - Function to render username with profile link + * @param {Function} context.PurifyHTML - Function to sanitize HTML content + * @param {Function} context.RenderMathJax - Function to render math formulas + */ +export function init(context) { + // Only execute if Discussion feature is enabled + if (!UtilityEnabled("Discussion")) { + return; + } + + const { + CurrentUsername, + SearchParams, + IsAdmin, + Style, + CaptchaSiteKey, + GetUsernameHTML, + PurifyHTML, + RenderMathJax + } = context; + + // Part 1: Create navbar link (lines 205-210) + let Discussion = null; + if (document.querySelector("#navbar > ul:nth-child(1)")) { + Discussion = document.createElement("li"); + document.querySelector("#navbar > ul:nth-child(1)").appendChild(Discussion); + Discussion.innerHTML = "讨论"; + } + + // Part 2: Discussion button on problem pages (lines 1284-1317) + if (location.pathname === "/problem.php") { + const centerElement = document.querySelector("body > div > div.mt-3 > center"); + if (centerElement) { + let PID = null; + if (SearchParams.get("cid") != null) { + PID = localStorage.getItem("UserScript-Contest-" + SearchParams.get("cid") + "-Problem-" + SearchParams.get("pid") + "-PID"); + } else { + PID = SearchParams.get("id"); + } + + if (PID) { + let DiscussButton = document.createElement("button"); + DiscussButton.className = "btn btn-outline-secondary position-relative"; + DiscussButton.innerHTML = `讨论`; + DiscussButton.style.marginLeft = "10px"; + DiscussButton.type = "button"; + DiscussButton.addEventListener("click", () => { + if (SearchParams.get("cid") != null) { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + PID, "_blank"); + } else { + open("https://www.xmoj.tech/discuss3/discuss.php?pid=" + SearchParams.get("id"), "_blank"); + } + }); + centerElement.appendChild(DiscussButton); + + let UnreadBadge = document.createElement("span"); + UnreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + UnreadBadge.style.display = "none"; + DiscussButton.appendChild(UnreadBadge); + + let RefreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (Response) => { + if (Response.Success) { + if (Response.Data.DiscussCount != 0) { + UnreadBadge.innerText = Response.Data.DiscussCount; + UnreadBadge.style.display = ""; + } + } + }); + }; + RefreshCount(); + addEventListener("focus", RefreshCount); + } + } + } + + // Part 3: Discussion forum pages (lines 3609-4386) + if (location.pathname.indexOf("/discuss3") != -1) { + if (Discussion) { + Discussion.classList.add("active"); + } + + // Discussion list page + if (location.pathname == "/discuss3/discuss.php") { + document.title = "讨论列表"; + let ProblemID = parseInt(SearchParams.get("pid")); + let BoardID = parseInt(SearchParams.get("bid")); + let Page = Number(SearchParams.get("page")) || 1; + document.querySelector("body > div > div").innerHTML = `

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

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

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

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

    +
    + 作者:
    + 发布时间: + 板块: + + + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + `; + let CaptchaSecretKey = ""; + unsafeWindow.CaptchaLoadedCallback = () => { + turnstile.render("#CaptchaContainer", { + theme: UtilityEnabled("DarkMode") ? "dark" : "light", language: "zh-cn", + sitekey: CaptchaSiteKey, callback: function (CaptchaSecretKeyValue) { + CaptchaSecretKey = CaptchaSecretKeyValue; + SubmitElement.disabled = false; + }, + }); + }; + let TurnstileScript = document.createElement("script"); + TurnstileScript.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=CaptchaLoadedCallback"; + document.body.appendChild(TurnstileScript); + ContentElement.addEventListener("keydown", (Event) => { + if ((Event.metaKey || Event.ctrlKey) && Event.keyCode == 13) { + SubmitElement.click(); + } + }); + ContentElement.addEventListener("input", () => { + PreviewTab.innerHTML = PurifyHTML(marked.parse(ContentElement.value)); + RenderMathJax(); + }); + ContentElement.addEventListener("paste", (EventData) => { + let Items = EventData.clipboardData.items; + if (Items.length !== 0) { + for (let i = 0; i < Items.length; i++) { + if (Items[i].type.indexOf("image") != -1) { + let Reader = new FileReader(); + Reader.readAsDataURL(Items[i].getAsFile()); + Reader.onload = () => { + let Before = ContentElement.value.substring(0, ContentElement.selectionStart); + let After = ContentElement.value.substring(ContentElement.selectionEnd, ContentElement.value.length); + const UploadMessage = "![正在上传图片...]()"; + ContentElement.value = Before + UploadMessage + After; + ContentElement.dispatchEvent(new Event("input")); + RequestAPI("UploadImage", { + "Image": Reader.result + }, (ResponseData) => { + if (ResponseData.Success) { + ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.dispatchEvent(new Event("input")); + } else { + ContentElement.value = Before + `![上传失败!]()` + After; + ContentElement.dispatchEvent(new Event("input")); + } + }); + }; + } + } + } + }); + let RefreshReply = (Silent = true) => { + if (!Silent) { + PostTitle.innerHTML = ``; + PostAuthor.innerHTML = ``; + PostTime.innerHTML = ``; + PostBoard.innerHTML = ``; + PostReplies.innerHTML = ""; + for (let i = 0; i < 10; i++) { + PostReplies.innerHTML += `
    +
    +
    + + +
    +
    + + + +
    +
    `; + } + } + RequestAPI("GetPost", { + "PostID": Number(ThreadID), "Page": Number(Page) + }, async (ResponseData) => { + if (ResponseData.Success == true) { + let OldScrollTop = document.documentElement.scrollTop; + let LockButtons = !IsAdmin && ResponseData.Data.Lock.Locked; + if (!Silent) { + DiscussPagination.children[0].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=1"; + DiscussPagination.children[1].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page - 1); + DiscussPagination.children[2].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + Page; + DiscussPagination.children[3].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + (Page + 1); + DiscussPagination.children[4].children[0].href = "https://www.xmoj.tech/discuss3/thread.php?tid=" + ThreadID + "&page=" + ResponseData.Data.PageCount; + if (Page <= 1) { + DiscussPagination.children[0].classList.add("disabled"); + DiscussPagination.children[1].remove(); + } + if (Page >= ResponseData.Data.PageCount) { + DiscussPagination.children[DiscussPagination.children.length - 1].classList.add("disabled"); + DiscussPagination.children[DiscussPagination.children.length - 2].remove(); + } + if (IsAdmin || ResponseData.Data.UserID == CurrentUsername) { + Delete.style.display = ""; + } + } + PostTitle.innerHTML = ResponseData.Data.Title + (ResponseData.Data.ProblemID == 0 ? "" : ` - 题目` + ` ` + ResponseData.Data.ProblemID + ``); + document.title = "讨论" + ThreadID + ": " + ResponseData.Data.Title; + PostAuthor.innerHTML = ""; + GetUsernameHTML(PostAuthor.children[0], ResponseData.Data.UserID); + PostTime.innerHTML = GetRelativeTime(ResponseData.Data.PostTime); + PostBoard.innerHTML = ResponseData.Data.BoardName; + let Replies = ResponseData.Data.Reply; + PostReplies.innerHTML = ""; + for (let i = 0; i < Replies.length; i++) { + let CardElement = document.createElement("div"); + PostReplies.appendChild(CardElement); + CardElement.className = "card mb-3"; + let CardBodyElement = document.createElement("div"); + CardElement.appendChild(CardBodyElement); + CardBodyElement.className = "card-body row"; + let CardBodyRowElement = document.createElement("div"); + CardBodyElement.appendChild(CardBodyRowElement); + CardBodyRowElement.className = "row mb-3"; + let AuthorElement = document.createElement("span"); + CardBodyRowElement.appendChild(AuthorElement); + AuthorElement.className = "col-4 text-muted"; + let AuthorSpanElement = document.createElement("span"); + AuthorElement.appendChild(AuthorSpanElement); + AuthorSpanElement.innerText = "作者:"; + let AuthorUsernameElement = document.createElement("span"); + AuthorElement.appendChild(AuthorUsernameElement); + GetUsernameHTML(AuthorUsernameElement, Replies[i].UserID); + let SendTimeElement = document.createElement("span"); + CardBodyRowElement.appendChild(SendTimeElement); + SendTimeElement.className = "col-4 text-muted"; + SendTimeElement.innerHTML = "发布时间:" + GetRelativeTime(Replies[i].ReplyTime); + + let OKButton; + if (!LockButtons) { + let ButtonsElement = document.createElement("span"); + CardBodyRowElement.appendChild(ButtonsElement); + ButtonsElement.className = "col-4"; + let ReplyButton = document.createElement("button"); + ButtonsElement.appendChild(ReplyButton); + ReplyButton.type = "button"; + ReplyButton.className = "btn btn-sm btn-info"; + ReplyButton.innerText = "回复"; + ReplyButton.addEventListener("click", () => { + let Content = Replies[i].Content; + Content = Content.split("\n").map((Line) => { + // Count the number of '>' characters at the beginning of the line + let nestingLevel = 0; + while (Line.startsWith(">")) { + nestingLevel++; + Line = Line.substring(1).trim(); + } + // If the line is nested more than 2 levels deep, skip it + if (nestingLevel > 2) { + return null; + } + // Reconstruct the line with the appropriate number of '>' characters + return "> ".repeat(nestingLevel + 1) + Line; + }).filter(Line => Line !== null) // Remove null entries + .join("\n"); + ContentElement.value += Content + `\n\n@${Replies[i].UserID} `; + ContentElement.focus(); + }); + let DeleteButton = document.createElement("button"); + ButtonsElement.appendChild(DeleteButton); + DeleteButton.type = "button"; + DeleteButton.className = "btn btn-sm btn-danger ms-1"; + DeleteButton.innerText = "删除"; + DeleteButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + DeleteButton.addEventListener("click", () => { + DeleteButton.disabled = true; + DeleteButton.lastChild.style.display = ""; + RequestAPI("DeleteReply", { + "ReplyID": Number(Replies[i].ReplyID) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + DeleteButton.disabled = false; + DeleteButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let DeleteSpin = document.createElement("div"); + DeleteButton.appendChild(DeleteSpin); + DeleteSpin.className = "spinner-border spinner-border-sm"; + DeleteSpin.role = "status"; + DeleteSpin.style.display = "none"; + OKButton = document.createElement("button"); + ButtonsElement.appendChild(OKButton); + OKButton.type = "button"; + OKButton.style.display = "none"; + OKButton.className = "btn btn-sm btn-success ms-1"; + OKButton.innerText = "确认"; + let OKSpin = document.createElement("div"); + OKButton.appendChild(OKSpin); + OKSpin.className = "spinner-border spinner-border-sm"; + OKSpin.role = "status"; + OKSpin.style.display = "none"; + OKButton.addEventListener("click", () => { + OKButton.disabled = true; + OKButton.lastChild.style.display = ""; + RequestAPI("EditReply", { + ReplyID: Number(Replies[i].ReplyID), + Content: String(ContentEditor.value) + }, (ResponseData) => { + if (ResponseData.Success == true) { + RefreshReply(); + } else { + OKButton.disabled = false; + OKButton.lastChild.style.display = "none"; + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = ""; + } + }); + }); + let CancelButton = document.createElement("button"); + ButtonsElement.appendChild(CancelButton); + CancelButton.type = "button"; + CancelButton.style.display = "none"; + CancelButton.className = "btn btn-sm btn-secondary ms-1"; + CancelButton.innerText = "取消"; + CancelButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = ""; + CardBodyElement.children[3].style.display = "none"; + EditButton.style.display = ""; + OKButton.style.display = "none"; + CancelButton.style.display = "none"; + }); + let EditButton = document.createElement("button"); + ButtonsElement.appendChild(EditButton); + EditButton.type = "button"; + EditButton.className = "btn btn-sm btn-warning ms-1"; + EditButton.innerText = "编辑"; + EditButton.style.display = (IsAdmin || Replies[i].UserID == CurrentUsername ? "" : "none"); + EditButton.addEventListener("click", () => { + CardBodyElement.children[2].style.display = "none"; + CardBodyElement.children[3].style.display = ""; + EditButton.style.display = "none"; + OKButton.style.display = ""; + CancelButton.style.display = ""; + }); + } + + let CardBodyHRElement = document.createElement("hr"); + CardBodyElement.appendChild(CardBodyHRElement); + + let ReplyContentElement = document.createElement("div"); + CardBodyElement.appendChild(ReplyContentElement); + ReplyContentElement.innerHTML = PurifyHTML(marked.parse(Replies[i].Content)).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + if (Replies[i].EditTime != null) { + if (Replies[i].EditPerson == Replies[i].UserID) { + { + const span = document.createElement('span'); + span.className = 'text-muted'; + span.style.fontSize = '12px'; + span.appendChild(document.createTextNode(`最后编辑于${GetRelativeTime(Replies[i].EditTime)}`)); + ReplyContentElement.appendChild(span); + } + } else { + { + const outer = document.createElement('span'); + outer.className = 'text-muted'; + outer.style.fontSize = '12px'; + outer.appendChild(document.createTextNode('最后被')); + const userSpan = document.createElement('span'); + userSpan.className = 'Usernames'; + userSpan.appendChild(document.createTextNode(Replies[i].EditPerson)); + outer.appendChild(userSpan); + outer.appendChild(document.createTextNode(`编辑于${GetRelativeTime(Replies[i].EditTime)}`)); + ReplyContentElement.appendChild(outer); + } + } + } + let ContentEditElement = document.createElement("div"); + 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.appendChild(document.createTextNode(" 锁定")); + LockElement.classList.add("mb-5"); + } + + if (IsAdmin) { + ToggleLock.style.display = "inline-block"; + ToggleLockButton.checked = ResponseData.Data.Lock.Locked; + ToggleLockButton.onclick = () => { + ToggleLockButton.disabled = true; + ErrorElement.style.display = "none"; + RequestAPI((ToggleLockButton.checked ? "LockPost" : "UnlockPost"), { + "PostID": Number(ThreadID) + }, (LockResponseData) => { + ToggleLockButton.disabled = false; + if (LockResponseData.Success) { + RefreshReply(); + } else { + ErrorElement.style.display = ""; + ErrorElement.innerText = "错误:" + LockResponseData.Message; + ToggleLockButton.checked = !ToggleLockButton.checked; + } + }); + }; + } + + Style.innerHTML += "img {"; + Style.innerHTML += " width: 50%;"; + Style.innerHTML += "}"; + + RenderMathJax(); + + if (Silent) { + scrollTo({ + top: OldScrollTop, behavior: "instant" + }); + } + } else { + PostTitle.innerText = "错误:" + ResponseData.Message; + } + }); + }; + Delete.addEventListener("click", () => { + Delete.disabled = true; + Delete.children[0].style.display = "inline-block"; + RequestAPI("DeletePost", { + "PostID": Number(SearchParams.get("tid")) + }, (ResponseData) => { + Delete.disabled = false; + Delete.children[0].style.display = "none"; + if (ResponseData.Success == true) { + location.href = "https://www.xmoj.tech/discuss3/discuss.php"; + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + SubmitElement.addEventListener("click", async () => { + ErrorElement.style.display = "none"; + SubmitElement.disabled = true; + SubmitElement.children[0].style.display = "inline-block"; + RequestAPI("NewReply", { + "PostID": Number(SearchParams.get("tid")), + "Content": String(ContentElement.value), + "CaptchaSecretKey": String(CaptchaSecretKey) + }, async (ResponseData) => { + SubmitElement.disabled = false; + SubmitElement.children[0].style.display = "none"; + if (ResponseData.Success == true) { + RefreshReply(); + ContentElement.value = ""; + PreviewTab.innerHTML = ""; + while (PostReplies.innerHTML.indexOf("placeholder") != -1) { + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + ContentElement.focus(); + ContentElement.scrollIntoView(); + turnstile.reset(); + } else { + ErrorElement.innerText = ResponseData.Message; + ErrorElement.style.display = "block"; + } + }); + }); + RefreshReply(false); + addEventListener("focus", RefreshReply); + } + } + } +} diff --git a/src/features/export-ac-code.js b/src/features/export-ac-code.js new file mode 100644 index 00000000..5523cdcd --- /dev/null +++ b/src/features/export-ac-code.js @@ -0,0 +1,106 @@ +/** + * Export AC Code Feature + * Exports all accepted code solutions as a ZIP file + * Feature ID: ExportACCode + * Type: U (Utility) + * Description: 导出所有AC代码为ZIP文件 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ExportACCode feature + * Adds a button to export all accepted code solutions + * + * This feature: + * 1. Adds an "导出AC代码" button to the user page + * 2. Fetches all AC code from export_ac_code.php + * 3. Creates a ZIP file with all accepted solutions + * 4. Uses JSZip library for compression + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2445-2495: Export AC code button and logic + */ +export function init() { + // Only execute if ExportACCode feature is enabled + if (!UtilityEnabled("ExportACCode")) { + return; + } + + // Only execute on user information page + if (location.pathname !== "/userinfo.php") { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + const container = document.querySelector("body > div.container > div"); + if (!container) return; + + // Create export button + const exportButton = document.createElement("button"); + container.appendChild(exportButton); + exportButton.innerText = "导出AC代码"; + exportButton.className = "btn btn-outline-secondary"; + + // Add click handler + exportButton.addEventListener("click", () => { + exportButton.disabled = true; + exportButton.innerText = "正在导出..."; + + const request = new XMLHttpRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState === 4) { + if (request.status === 200) { + const response = request.responseText; + const acCode = response.split("------------------------------------------------------\r\n"); + + // Load JSZip library + const scriptElement = document.createElement("script"); + scriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; + document.head.appendChild(scriptElement); + + scriptElement.onload = () => { + const zip = new JSZip(); + + // Add each AC code file to ZIP + for (let i = 0; i < acCode.length; i++) { + let currentCode = acCode[i]; + if (currentCode !== "") { + const currentQuestionID = currentCode.substring(7, 11); + currentCode = currentCode.substring(14); + currentCode = currentCode.replaceAll("\r", ""); + zip.file(currentQuestionID + ".cpp", currentCode); + } + } + + exportButton.innerText = "正在生成压缩包……"; + zip.generateAsync({ type: "blob" }) + .then((content) => { + saveAs(content, "ACCodes.zip"); + exportButton.innerText = "AC代码导出成功"; + exportButton.disabled = false; + setTimeout(() => { + exportButton.innerText = "导出AC代码"; + }, 1000); + }); + }; + } else { + exportButton.disabled = false; + exportButton.innerText = "AC代码导出失败"; + setTimeout(() => { + exportButton.innerText = "导出AC代码"; + }, 1000); + } + } + }); + + request.open("GET", "https://www.xmoj.tech/export_ac_code.php", true); + request.send(); + }); + } catch (error) { + console.error('[ExportACCode] Error initializing export button:', error); + } + }, 100); +} diff --git a/src/features/improve-ac-rate.js b/src/features/improve-ac-rate.js new file mode 100644 index 00000000..a3f02ffc --- /dev/null +++ b/src/features/improve-ac-rate.js @@ -0,0 +1,135 @@ +/** + * Improve AC Rate Feature + * Adds a button to resubmit already-AC'd problems to improve submission statistics + * Feature ID: ImproveACRate + * Type: U (Utility) + * Description: 添加按钮来重新提交已AC的题目以提高正确率 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ImproveACRate feature + * Adds a "提高正确率" button that resubmits already-AC'd problems + * + * This feature: + * 1. Fetches user's AC problems from userinfo page + * 2. Displays current AC rate percentage + * 3. On click, randomly selects 3 AC'd problems and resubmits them + * 4. Uses existing AC code from status page + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1405-1463: Improve AC rate button and logic + */ +export function init() { + // Only execute if ImproveACRate feature is enabled + if (!UtilityEnabled("ImproveACRate")) { + return; + } + + // Only execute on status page + if (location.pathname !== "/status.php") { + return; + } + + // Need current username + const currentUsername = document.querySelector("#profile")?.innerText; + if (!currentUsername || currentUsername === "登录") { + return; + } + + // Wait for page to be ready + setTimeout(async () => { + try { + const container = document.querySelector("body > div.container > div > div.input-append"); + if (!container) return; + + // Create improve AC rate button + const improveACRateButton = document.createElement("button"); + container.appendChild(improveACRateButton); + improveACRateButton.className = "btn btn-outline-secondary"; + improveACRateButton.innerText = "提高正确率"; + improveACRateButton.disabled = true; + + // Fetch user's AC problems + let acProblems = []; + await fetch(`https://www.xmoj.tech/userinfo.php?user=${currentUsername}`) + .then((response) => response.text()) + .then((response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + + // Calculate and display AC rate + const acCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText); + const submitCount = parseInt(parsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText); + const acRate = (acCount / submitCount * 100).toFixed(2); + improveACRateButton.innerText += ` (${acRate}%)`; + + // Extract AC problem IDs + const scriptContent = parsedDocument.querySelector("#statics > tbody > tr:nth-child(2) > td:nth-child(3) > script").innerText.split("\n")[5].split(";"); + for (let i = 0; i < scriptContent.length; i++) { + const problemId = Number(scriptContent[i].substring(2, scriptContent[i].indexOf(","))); + if (!isNaN(problemId)) { + acProblems.push(problemId); + } + } + + improveACRateButton.disabled = false; + }); + + // Add click handler + improveACRateButton.addEventListener("click", async () => { + improveACRateButton.disabled = true; + const submitTimes = 3; + let count = 0; + + const submitInterval = setInterval(async () => { + if (count >= submitTimes) { + clearInterval(submitInterval); + location.reload(); + return; + } + + improveACRateButton.innerText = `正在提交 (${count + 1}/${submitTimes})`; + + // Randomly select an AC'd problem + const pid = acProblems[Math.floor(Math.random() * acProblems.length)]; + + // Get a solution ID for this problem + let sid = 0; + await fetch(`https://www.xmoj.tech/status.php?problem_id=${pid}&jresult=4`) + .then((result) => result.text()) + .then((result) => { + const parsedDocument = new DOMParser().parseFromString(result, "text/html"); + sid = parsedDocument.querySelector("#result-tab > tbody > tr:nth-child(1) > td:nth-child(2)").innerText; + }); + + // Get the source code + let code = ""; + await fetch(`https://www.xmoj.tech/getsource.php?id=${sid}`) + .then((response) => response.text()) + .then((response) => { + code = response.substring(0, response.indexOf("/**************************************************************")).trim(); + }); + + // Resubmit the code + await fetch("https://www.xmoj.tech/submit.php", { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + referrer: `https://www.xmoj.tech/submitpage.php?id=${pid}`, + method: "POST", + body: `id=${pid}&language=1&source=${encodeURIComponent(code)}&enable_O2=on` + }); + + count++; + }, 1000); + }); + + // Style the button + improveACRateButton.style.marginBottom = "7px"; + improveACRateButton.style.marginRight = "7px"; + } catch (error) { + console.error('[ImproveACRate] Error initializing button:', error); + } + }, 100); +} diff --git a/src/features/index-page.js b/src/features/index-page.js new file mode 100644 index 00000000..79042108 --- /dev/null +++ b/src/features/index-page.js @@ -0,0 +1,256 @@ +// Index page handler +import { UtilityEnabled } from '../core/config.js'; +import { RequestAPI } from '../utils/api.js'; +import { TidyTable } from '../utils/table.js'; +import { RenderMathJax } from '../utils/mathjax.js'; +import { GetUsernameHTML } from '../utils/user.js'; + +/** + * Handle index page (/index.php or /) + * @param {Object} context - Execution context + */ +export async function handleIndexPage(context) { + const { location, SearchParams, initTheme, marked } = context; + + if (location.pathname == "/index.php" || location.pathname == "/") { + if (new URL(location.href).searchParams.get("ByUserScript") != null) { + // Script settings page + document.title = "脚本设置"; + localStorage.setItem("UserScript-Opened", "true"); + let Container = document.getElementsByClassName("mt-3")[0]; + Container.innerHTML = ""; + let Alert = document.createElement("div"); + Alert.classList.add("alert"); + Alert.classList.add("alert-primary"); + Alert.role = "alert"; + Alert.innerHTML = `欢迎您使用XMOJ增强脚本!点击 + 此处 + 查看更新日志。`; + Container.appendChild(Alert); + let UtilitiesCard = document.createElement("div"); + UtilitiesCard.classList.add("card"); + UtilitiesCard.classList.add("mb-3"); + let UtilitiesCardHeader = document.createElement("div"); + UtilitiesCardHeader.classList.add("card-header"); + UtilitiesCardHeader.innerText = "XMOJ增强脚本功能列表"; + UtilitiesCard.appendChild(UtilitiesCardHeader); + let UtilitiesCardBody = document.createElement("div"); + UtilitiesCardBody.classList.add("card-body"); + let CreateList = (Data) => { + let List = document.createElement("ul"); + List.classList.add("list-group"); + for (let i = 0; i < Data.length; i++) { + let Row = document.createElement("li"); + Row.classList.add("list-group-item"); + if (Data[i].Type == "A") { + Row.classList.add("list-group-item-success"); + } else if (Data[i].Type == "F") { + Row.classList.add("list-group-item-warning"); + } else if (Data[i].Type == "D") { + Row.classList.add("list-group-item-danger"); + } + if (Data[i].ID == "Theme") { + let Label = document.createElement("label"); + Label.classList.add("me-2"); + Label.htmlFor = "UserScript-Setting-Theme"; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + let Select = document.createElement("select"); + Select.classList.add("form-select", "form-select-sm", "w-auto", "d-inline"); + Select.id = "UserScript-Setting-Theme"; + [ + ["light", "亮色"], + ["dark", "暗色"], + ["auto", "跟随系统"] + ].forEach(opt => { + let option = document.createElement("option"); + option.value = opt[0]; + option.innerText = opt[1]; + Select.appendChild(option); + }); + Select.value = localStorage.getItem("UserScript-Setting-Theme") || "auto"; + Select.addEventListener("change", () => { + localStorage.setItem("UserScript-Setting-Theme", Select.value); + initTheme(); + }); + Row.appendChild(Select); + } else if (Data[i].Children == undefined) { + let CheckBox = document.createElement("input"); + CheckBox.classList.add("form-check-input"); + CheckBox.classList.add("me-1"); + CheckBox.type = "checkbox"; + CheckBox.id = Data[i].ID; + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == null) { + localStorage.setItem("UserScript-Setting-" + Data[i].ID, "true"); + } + if (localStorage.getItem("UserScript-Setting-" + Data[i].ID) == "false") { + CheckBox.checked = false; + } else { + CheckBox.checked = true; + } + CheckBox.addEventListener("change", () => { + return localStorage.setItem("UserScript-Setting-" + Data[i].ID, CheckBox.checked); + }); + + Row.appendChild(CheckBox); + let Label = document.createElement("label"); + Label.classList.add("form-check-label"); + Label.htmlFor = Data[i].ID; + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } else { + let Label = document.createElement("label"); + Label.innerText = Data[i].Name; + Row.appendChild(Label); + } + if (Data[i].Children != undefined) { + Row.appendChild(CreateList(Data[i].Children)); + } + List.appendChild(Row); + } + return List; + }; + UtilitiesCardBody.appendChild(CreateList([{ + "ID": "Discussion", + "Type": "F", + "Name": "恢复讨论与短消息功能" + }, { + "ID": "MoreSTD", "Type": "F", "Name": "查看到更多标程" + }, {"ID": "ApplyData", "Type": "A", "Name": "获取数据功能"}, { + "ID": "AutoCheat", "Type": "A", "Name": "自动提交当年代码" + }, {"ID": "Rating", "Type": "A", "Name": "添加用户评分和用户名颜色"}, { + "ID": "AutoRefresh", "Type": "A", "Name": "比赛列表、比赛排名界面自动刷新" + }, { + "ID": "AutoCountdown", "Type": "A", "Name": "比赛列表等界面的时间自动倒计时" + }, {"ID": "DownloadPlayback", "Type": "A", "Name": "回放视频增加下载功能"}, { + "ID": "ImproveACRate", "Type": "A", "Name": "自动提交已AC题目以提高AC率" + }, {"ID": "AutoO2", "Type": "F", "Name": "代码提交界面自动选择O2优化"}, { + "ID": "Beautify", "Type": "F", "Name": "美化界面", "Children": [{ + "ID": "NewTopBar", "Type": "F", "Name": "使用新的顶部导航栏" + }, { + "ID": "NewBootstrap", "Type": "F", "Name": "使用新版的Bootstrap样式库*" + }, {"ID": "ResetType", "Type": "F", "Name": "重新排版*"}, { + "ID": "AddColorText", "Type": "A", "Name": "增加彩色文字" + }, {"ID": "AddUnits", "Type": "A", "Name": "状态界面内存与耗时添加单位"}, { + "ID": "Theme", "Type": "A", "Name": "界面主题" + }, {"ID": "AddAnimation", "Type": "A", "Name": "增加动画"}, { + "ID": "ReplaceYN", "Type": "F", "Name": "题目前状态提示替换为好看的图标" + }, {"ID": "RemoveAlerts", "Type": "D", "Name": "去除多余反复的提示"}, { + "ID": "Translate", "Type": "F", "Name": "统一使用中文,翻译了部分英文*" + }, { + "ID": "ReplaceLinks", "Type": "F", "Name": "将网站中所有以方括号包装的链接替换为按钮" + }, {"ID": "RemoveUseless", "Type": "D", "Name": "删去无法使用的功能*"}, { + "ID": "ReplaceXM", + "Type": "F", + "Name": "将网站中所有\"小明\"和\"我\"关键字替换为\"高老师\",所有\"小红\"替换为\"徐师娘\",所有\"小粉\"替换为\"彩虹\",所有\"下海\"、\"海上\"替换为\"上海\" (此功能默认关闭)" + }] + }, { + "ID": "AutoLogin", "Type": "A", "Name": "在需要登录的界面自动跳转到登录界面" + }, { + "ID": "SavePassword", "Type": "A", "Name": "自动保存用户名与密码,免去每次手动输入密码的繁琐" + }, { + "ID": "CopySamples", "Type": "F", "Name": "题目界面测试样例有时复制无效" + }, { + "ID": "RefreshSolution", "Type": "F", "Name": "状态页面结果自动刷新每次只能刷新一个" + }, {"ID": "CopyMD", "Type": "A", "Name": "复制题目或题解内容"}, { + "ID": "ProblemSwitcher", "Type": "A", "Name": "比赛题目切换器" + }, { + "ID": "OpenAllProblem", "Type": "A", "Name": "比赛题目界面一键打开所有题目" + }, { + "ID": "CheckCode", "Type": "A", "Name": "提交代码前对代码进行检查", "Children": [{ + "ID": "IOFile", "Type": "A", "Name": "是否使用了文件输入输出(如果需要使用)" + }, {"ID": "CompileError", "Type": "A", "Name": "是否有编译错误"}] + }, { + "ID": "ExportACCode", "Type": "F", "Name": "导出AC代码每一道题目一个文件" + }, {"ID": "LoginFailed", "Type": "F", "Name": "修复登录后跳转失败*"}, { + "ID": "NewDownload", "Type": "A", "Name": "下载页面增加下载内容" + }, {"ID": "CompareSource", "Type": "A", "Name": "比较代码"}, { + "ID": "BBSPopup", "Type": "A", "Name": "讨论提醒" + }, {"ID": "MessagePopup", "Type": "A", "Name": "短消息提醒"}, { + "ID": "DebugMode", "Type": "A", "Name": "调试模式(仅供开发者使用)" + }, { + "ID": "SuperDebug", "Type": "A", "Name": "本地调试模式(仅供开发者使用) (未经授权的擅自开启将导致大部分功能不可用!)" + }])); + let UtilitiesCardFooter = document.createElement("div"); + UtilitiesCardFooter.className = "card-footer text-muted"; + UtilitiesCardFooter.innerText = "* 不建议关闭,可能会导致系统不稳定、界面错乱、功能缺失等问题\n绿色:增加功能 黄色:修改功能 红色:删除功能"; + UtilitiesCardBody.appendChild(UtilitiesCardFooter); + UtilitiesCard.appendChild(UtilitiesCardBody); + Container.appendChild(UtilitiesCard); + let FeedbackCard = document.createElement("div"); + FeedbackCard.className = "card mb-3"; + let FeedbackCardHeader = document.createElement("div"); + FeedbackCardHeader.className = "card-header"; + FeedbackCardHeader.innerText = "反馈、源代码、联系作者"; + FeedbackCard.appendChild(FeedbackCardHeader); + let FeedbackCardBody = document.createElement("div"); + FeedbackCardBody.className = "card-body"; + let FeedbackCardText = document.createElement("p"); + FeedbackCardText.className = "card-text"; + FeedbackCardText.innerText = "如果您有任何建议或者发现了 bug,请前往本项目的 GitHub 页面并提交 issue。提交 issue 前请先搜索是否有相同的 issue,如果有请在该 issue 下留言。请在 issue 中尽可能详细地描述您的问题,并且附上您的浏览器版本、操作系统版本、脚本版本、复现步骤等信息。谢谢您支持本项目。"; + FeedbackCardBody.appendChild(FeedbackCardText); + let FeedbackCardLink = document.createElement("a"); + FeedbackCardLink.className = "card-link"; + FeedbackCardLink.innerText = "GitHub"; + FeedbackCardLink.href = "https://github.com/XMOJ-Script-dev/XMOJ-Script"; + FeedbackCardBody.appendChild(FeedbackCardLink); + FeedbackCard.appendChild(FeedbackCardBody); + Container.appendChild(FeedbackCard); + } else { + // Normal index page + let Temp = document.querySelector("body > div > div.mt-3 > div > div.col-md-8").children; + let NewsData = []; + for (let i = 0; i < Temp.length; i += 2) { + let Title = Temp[i].children[0].innerText; + let Time = 0; + if (Temp[i].children[1] != null) { + Time = Temp[i].children[1].innerText; + } + let Body = Temp[i + 1].innerHTML; + NewsData.push({"Title": Title, "Time": new Date(Time), "Body": Body}); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").innerHTML = ""; + for (let i = 0; i < NewsData.length; i++) { + let NewsRow = document.createElement("div"); + NewsRow.className = "cnt-row"; + let NewsRowHead = document.createElement("div"); + NewsRowHead.className = "cnt-row-head title"; + NewsRowHead.innerText = NewsData[i].Title; + if (NewsData[i].Time != 0) { + NewsRowHead.innerHTML += "" + NewsData[i].Time.toLocaleDateString() + ""; + } + NewsRow.appendChild(NewsRowHead); + let NewsRowBody = document.createElement("div"); + NewsRowBody.className = "cnt-row-body"; + NewsRowBody.innerHTML = NewsData[i].Body; + NewsRow.appendChild(NewsRowBody); + document.querySelector("body > div > div.mt-3 > div > div.col-md-8").appendChild(NewsRow); + } + let CountDownData = document.querySelector("#countdown_list").innerHTML; + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML = `
    +
    倒计时
    +
    ${CountDownData}
    +
    `; + let Tables = document.getElementsByTagName("table"); + for (let i = 0; i < Tables.length; i++) { + TidyTable(Tables[i]); + } + document.querySelector("body > div > div.mt-3 > div > div.col-md-4").innerHTML += `
    +
    公告
    +
    加载中...
    +
    `; + RequestAPI("GetNotice", {}, (Response) => { + if (Response.Success) { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = marked.parse(Response.Data["Notice"]).replaceAll(/@([a-zA-Z0-9]+)/g, `@$1`); + RenderMathJax(); + let UsernameElements = document.getElementsByClassName("Usernames"); + for (let i = 0; i < UsernameElements.length; i++) { + GetUsernameHTML(UsernameElements[i], UsernameElements[i].innerText, true); + } + } else { + document.querySelector("body > div.container > div > div > div.col-md-4 > div:nth-child(2) > div.cnt-row-body").innerHTML = "加载失败: " + Response.Message; + } + }); + } + } +} diff --git a/src/features/index.js b/src/features/index.js new file mode 100644 index 00000000..5d8e6224 --- /dev/null +++ b/src/features/index.js @@ -0,0 +1,117 @@ +/** + * Feature loader - Initializes all extracted feature modules + * + * This module provides a centralized way to initialize all feature modules. + * Features are loaded conditionally based on UtilityEnabled settings and + * current page URL. + */ + +import { init as initAutoLogin } from './auto-login.js'; +import { init as initDiscussion } from './discussion.js'; +import { init as initCopySamples } from './copy-samples.js'; +import { init as initCompareSource } from './compare-source.js'; +import { init as initRemoveUseless } from './remove-useless.js'; +import { init as initReplaceXM } from './replace-xm.js'; +import { init as initReplaceYN } from './replace-yn.js'; +import { init as initAddAnimation } from './add-animation.js'; +import { init as initAddColorText } from './add-color-text.js'; +import { init as initSavePassword } from './save-password.js'; +import { init as initRemoveAlerts } from './remove-alerts.js'; +import { init as initReplaceLinks } from './replace-links.js'; +import { init as initAutoO2 } from './auto-o2.js'; +import { init as initTranslate } from './translate.js'; +import { init as initAutoCountdown } from './auto-countdown.js'; +import { init as initMoreSTD } from './more-std.js'; +import { init as initExportACCode } from './export-ac-code.js'; +import { init as initOpenAllProblem } from './open-all-problem.js'; +import { init as initDarkMode } from './dark-mode.js'; +import { init as initImproveACRate } from './improve-ac-rate.js'; + +/** + * Initialize all feature modules + * Features will self-check if they should be active based on UtilityEnabled + * + * @param {Object} context - Shared context object with dependencies + * @param {string} context.CurrentUsername - Current logged-in username + * @param {URLSearchParams} context.SearchParams - URL search parameters + * @param {boolean} context.IsAdmin - Whether user is admin + * @param {HTMLStyleElement} context.Style - Style element for adding CSS + * @param {string} context.CaptchaSiteKey - Cloudflare Turnstile site key + * @param {Function} context.GetUsernameHTML - Function to render usernames + * @param {Function} context.PurifyHTML - Function to sanitize HTML + * @param {Function} context.RenderMathJax - Function to render MathJax + */ +export async function initializeFeatures(context) { + try { + // Initialize features that need to run early (before main page load) + initAutoLogin(); + + // Initialize theme (must run early) + initDarkMode(); + + // Initialize features that clean up/modify the page + initRemoveUseless(); + initRemoveAlerts(); + + // Initialize cosmetic/styling features + initAddAnimation(); + initAddColorText(); + + // Initialize text replacement features + initReplaceXM(); + initReplaceYN(); + initReplaceLinks(); + initTranslate(); + + // Initialize utility features + initAutoCountdown(); + initMoreSTD(); + initOpenAllProblem(); + initImproveACRate(); + + // Initialize page-specific features + initCopySamples(); + initSavePassword(); + initAutoO2(); + initExportACCode(); + await initCompareSource(); + + // Initialize complex features that need context + if (context) { + await initDiscussion(context); + } + + console.log('[XMOJ-Script] Feature modules initialized'); + } catch (error) { + console.error('[XMOJ-Script] Error initializing features:', error); + } +} + +/** + * Get list of all extracted features + * Useful for debugging and feature management + */ +export function getExtractedFeatures() { + return [ + 'AutoLogin', + 'Discussion', + 'CopySamples', + 'CompareSource', + 'RemoveUseless', + 'ReplaceXM', + 'ReplaceYN', + 'AddAnimation', + 'AddColorText', + 'SavePassword', + 'RemoveAlerts', + 'ReplaceLinks', + 'AutoO2', + 'Translate', + 'AutoCountdown', + 'MoreSTD', + 'ExportACCode', + 'OpenAllProblem', + 'DarkMode', + 'ImproveACRate', + ]; +} diff --git a/src/features/more-std.js b/src/features/more-std.js new file mode 100644 index 00000000..bc02f28d --- /dev/null +++ b/src/features/more-std.js @@ -0,0 +1,81 @@ +/** + * More STD Feature + * Adds standard solution links to contest problem tables + * Feature ID: MoreSTD + * Type: U (Utility) + * Description: 在比赛题目表格中添加标程链接 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize MoreSTD feature + * Reorganizes contest problem table to add standard solution links + * + * This feature: + * 1. Removes any existing "标程" column + * 2. Adds a new "标程" column header + * 3. Adds links to standard solutions for each problem + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1699-1717: Standard solution column management + */ +export function init() { + // Only execute if MoreSTD feature is enabled + if (!UtilityEnabled("MoreSTD")) { + return; + } + + // Only execute on contest pages + if (location.pathname !== "/contest.php") { + return; + } + + // Check if we're in a contest with problem list + const searchParams = new URLSearchParams(location.search); + if (!searchParams.get("cid")) { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + const tableHeader = document.querySelector("#problemset > thead > tr"); + + // Only proceed if table exists and has "标程" column + if (!tableHeader || tableHeader.innerHTML.indexOf("标程") === -1) { + return; + } + + // Remove existing "标程" column + let headerCells = tableHeader.children; + for (let i = 0; i < headerCells.length; i++) { + if (headerCells[i].innerText === "标程") { + headerCells[i].remove(); + + // Remove corresponding cells from each row + const bodyRows = document.querySelector("#problemset > tbody").children; + for (let j = 0; j < bodyRows.length; j++) { + if (bodyRows[j].children[i] !== undefined) { + bodyRows[j].children[i].remove(); + } + } + } + } + + // Add new "标程" column header + tableHeader.innerHTML += '标程'; + + // Add standard solution links for each problem + const bodyRows = document.querySelector("#problemset > tbody").children; + const cid = Number(searchParams.get("cid")); + + for (let i = 0; i < bodyRows.length; i++) { + bodyRows[i].innerHTML += + `打开`; + } + } catch (error) { + console.error('[MoreSTD] Error adding standard solution links:', error); + } + }, 100); +} diff --git a/src/features/open-all-problem.js b/src/features/open-all-problem.js new file mode 100644 index 00000000..e2b131fa --- /dev/null +++ b/src/features/open-all-problem.js @@ -0,0 +1,78 @@ +/** + * Open All Problem Feature + * Adds buttons to open all problems or only unsolved problems in new tabs + * Feature ID: OpenAllProblem + * Type: U (Utility) + * Description: 添加按钮以在新标签页中打开所有题目或仅未解决的题目 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize OpenAllProblem feature + * Adds two buttons to contest pages: + * 1. "打开全部题目" - Opens all contest problems in new tabs + * 2. "打开未解决题目" - Opens only unsolved problems in new tabs + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1817-1841: Open all problem buttons + */ +export function init() { + // Only execute if OpenAllProblem feature is enabled + if (!UtilityEnabled("OpenAllProblem")) { + return; + } + + // Only execute on contest pages with problem list + if (location.pathname !== "/contest.php") { + return; + } + + const searchParams = new URLSearchParams(location.search); + if (!searchParams.get("cid")) { + return; + } + + // Wait for page to be ready + setTimeout(() => { + try { + // Find or create container for buttons + let cheatDiv = document.querySelector("#CheatDiv"); + if (!cheatDiv) { + return; + } + + // Create "Open All Problems" button + const openAllButton = document.createElement("button"); + openAllButton.className = "btn btn-outline-secondary"; + openAllButton.innerText = "打开全部题目"; + openAllButton.style.marginRight = "5px"; + cheatDiv.appendChild(openAllButton); + + openAllButton.addEventListener("click", () => { + const rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < rows.length; i++) { + open(rows[i].children[2].children[0].href, "_blank"); + } + }); + + // Create "Open Unsolved Problems" button + const openUnsolvedButton = document.createElement("button"); + openUnsolvedButton.className = "btn btn-outline-secondary"; + openUnsolvedButton.innerText = "打开未解决题目"; + cheatDiv.appendChild(openUnsolvedButton); + + openUnsolvedButton.addEventListener("click", () => { + const rows = document.querySelector("#problemset > tbody").rows; + for (let i = 0; i < rows.length; i++) { + // Only open problems that are not marked as solved (status_y) + if (!rows[i].children[0].children[0].classList.contains("status_y")) { + open(rows[i].children[2].children[0].href, "_blank"); + } + } + }); + } catch (error) { + console.error('[OpenAllProblem] Error initializing buttons:', error); + } + }, 100); +} diff --git a/src/features/remove-alerts.js b/src/features/remove-alerts.js new file mode 100644 index 00000000..d6fd9154 --- /dev/null +++ b/src/features/remove-alerts.js @@ -0,0 +1,45 @@ +/** + * Remove Alerts Feature + * Removes redundant alerts and warnings + * Feature ID: RemoveAlerts + * Type: D (Debug/Development) + * Description: 去除多余反复的提示 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize RemoveAlerts feature + * Modifies contest links to bypass "contest not started" alerts + * + * On contest pages, when the contest hasn't started yet, this feature + * changes the link to point directly to start_contest.php, bypassing + * the alert that would normally prevent access. + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 1666-1667: Modify contest start link + */ +export function init() { + // Only execute if RemoveAlerts feature is enabled + if (!UtilityEnabled("RemoveAlerts")) { + return; + } + + // Only execute on contest pages + if (location.pathname !== "/contest.php") { + return; + } + + // Check if contest hasn't started yet + const centerElement = document.querySelector("body > div > div.mt-3 > center"); + if (centerElement && centerElement.innerHTML.indexOf("尚未开始比赛") !== -1) { + const contestLink = document.querySelector("body > div > div.mt-3 > center > a"); + const searchParams = new URLSearchParams(location.search); + const cid = searchParams.get("cid"); + + if (contestLink && cid) { + // Modify link to bypass alert + contestLink.setAttribute("href", `start_contest.php?cid=${cid}`); + } + } +} diff --git a/src/features/remove-useless.js b/src/features/remove-useless.js new file mode 100644 index 00000000..fec3e571 --- /dev/null +++ b/src/features/remove-useless.js @@ -0,0 +1,85 @@ +/** + * Remove Useless Feature + * Removes unwanted elements from various pages + * Feature ID: RemoveUseless + * Type: U (Utility) + * Description: 移除页面中的无用元素 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize remove useless feature + * Removes various unwanted elements from the page based on current location + * + * This feature removes several unnecessary or distracting elements: + * - Marquee elements (scrolling text banners) + * - Footer elements + * - English title headers (h2.lang_en) on problem and solution pages + * - Submission nodes on user info pages + * - Center tags on problem pages + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Line 320-322: Remove marquee elements + * - Line 398-402: Remove footer + * - Line 1222-1225: Remove h2.lang_en and second center tag on problem pages + * - Line 2500-2505: Remove submission child nodes on userinfo page + * - Line 3209-3211: Remove h2.lang_en on problem_solution page + */ +export function init() { + // Only execute if RemoveUseless feature is enabled + if (!UtilityEnabled("RemoveUseless")) { + return; + } + + // Remove marquee elements (scrolling banners) - Line 320-322 + const marquee = document.getElementsByTagName("marquee")[0]; + if (marquee !== undefined && marquee !== null) { + marquee.remove(); + } + + // Remove footer - Line 398-402 + const footer = document.getElementsByClassName("footer")[0]; + if (footer !== undefined && footer !== null) { + footer.remove(); + } + + // Page-specific removals based on current pathname + const pathname = location.pathname; + + // Problem page specific removals - Line 1222-1225 + if (pathname === "/problem.php") { + const langEnHeader = document.querySelector("h2.lang_en"); + if (langEnHeader !== null) { + langEnHeader.remove(); + } + + const centerElements = document.getElementsByTagName("center"); + if (centerElements.length > 1 && centerElements[1] !== null) { + centerElements[1].remove(); + } + } + + // User info page specific removals - Line 2500-2505 + if (pathname === "/userinfo.php") { + const searchParams = new URLSearchParams(location.search); + if (searchParams.get("ByUserScript") === null) { + const submissionElement = document.getElementById("submission"); + if (submissionElement) { + const childNodes = submissionElement.childNodes; + // Remove all child nodes + for (let i = childNodes.length - 1; i >= 0; i--) { + childNodes[i].remove(); + } + } + } + } + + // Problem solution page specific removals - Line 3209-3211 + if (pathname === "/problem_solution.php") { + const langEnHeader = document.querySelector("h2.lang_en"); + if (langEnHeader !== null) { + langEnHeader.remove(); + } + } +} diff --git a/src/features/replace-links.js b/src/features/replace-links.js new file mode 100644 index 00000000..f6074638 --- /dev/null +++ b/src/features/replace-links.js @@ -0,0 +1,33 @@ +/** + * Replace Links Feature + * Replaces bracketed links with styled buttons + * Feature ID: ReplaceLinks + * Type: F (Format/UI) + * Description: 将网站中所有以方括号包装的链接替换为按钮 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ReplaceLinks feature + * Replaces all links in format [text] with styled buttons + * + * Example transformation: + * [Problem 1001] + * -> + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 216-218: Link to button replacement + */ +export function init() { + // Only execute if ReplaceLinks feature is enabled + if (!UtilityEnabled("ReplaceLinks")) { + return; + } + + // Replace all bracketed links with buttons + document.body.innerHTML = String(document.body.innerHTML).replaceAll( + /\[([^<]*)<\/a>\]/g, + '' + ); +} diff --git a/src/features/replace-xm.js b/src/features/replace-xm.js new file mode 100644 index 00000000..01cce47e --- /dev/null +++ b/src/features/replace-xm.js @@ -0,0 +1,41 @@ +/** + * Replace XM Feature + * Replaces "小明" references with "高老师" + * Feature ID: ReplaceXM + * Type: C (Cosmetic/Fun) + * Description: 将"小明"替换为"高老师" + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ReplaceXM feature + * Replaces text content throughout the page: + * - "我" -> "高老师" + * - "小明" -> "高老师" + * - "下海" -> "上海" + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 219-222: Text replacement + * - Line 304: Navbar brand text + */ +export function init() { + // Only execute if ReplaceXM feature is enabled + if (!UtilityEnabled("ReplaceXM")) { + return; + } + + // Replace text content throughout the page + document.body.innerHTML = String(document.body.innerHTML).replaceAll("我", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("小明", "高老师"); + document.body.innerHTML = String(document.body.innerHTML).replaceAll("下海", "上海"); +} + +/** + * Get the site name based on ReplaceXM setting + * Used by navbar and other UI elements + * @returns {string} Site name + */ +export function getSiteName() { + return UtilityEnabled("ReplaceXM") ? "高老师" : "小明"; +} diff --git a/src/features/replace-yn.js b/src/features/replace-yn.js new file mode 100644 index 00000000..dd2e16a0 --- /dev/null +++ b/src/features/replace-yn.js @@ -0,0 +1,53 @@ +/** + * Replace YN Feature + * Replaces Y/N status indicators with symbols + * Feature ID: ReplaceYN + * Type: U (Utility) + * Description: 将Y/N状态替换为符号 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize ReplaceYN feature + * Replaces status text with symbols: + * - "Y" (AC/Accepted) -> "✓" + * - "N" (WA/Wrong Answer) -> "✗" + * - "W" (Waiting) -> "⏳" + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 404-417: Status text replacement + */ +export function init() { + // Only execute if ReplaceYN feature is enabled + if (!UtilityEnabled("ReplaceYN")) { + return; + } + + // Replace AC (Accepted) status + let elements = document.getElementsByClassName("status_y"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "✓"; + } + + // Replace WA (Wrong Answer) status + elements = document.getElementsByClassName("status_n"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "✗"; + } + + // Replace Waiting status + elements = document.getElementsByClassName("status_w"); + for (let i = 0; i < elements.length; i++) { + elements[i].innerText = "⏳"; + } +} + +/** + * Check if ReplaceYN is enabled + * Used by other features that need to apply Y/N replacement + * @returns {boolean} + */ +export function isEnabled() { + return UtilityEnabled("ReplaceYN"); +} diff --git a/src/features/save-password.js b/src/features/save-password.js new file mode 100644 index 00000000..9f5c2797 --- /dev/null +++ b/src/features/save-password.js @@ -0,0 +1,86 @@ +/** + * Save Password Feature + * Automatically saves and fills login credentials + * Feature ID: SavePassword + * Type: U (Utility) + * Description: 自动保存和填充登录凭据 + */ + +import { UtilityEnabled } from '../core/config.js'; +import { storeCredential, getCredential, clearCredential } from '../utils/credentials.js'; + +/** + * Initialize SavePassword feature + * Sets up auto-fill on login page when credentials are available + * + * Note: This feature also integrates with the login handler to: + * - Save credentials after successful login + * - Clear credentials after failed login + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 2841-2843: Save credentials on success + * - Lines 2850-2852: Clear credentials on failure + * - Lines 2867-2876: Auto-fill and auto-submit login form + */ +export function init() { + // Only execute on login page + if (location.pathname !== "/loginpage.php") { + return; + } + + // Only execute if SavePassword feature is enabled + if (!UtilityEnabled("SavePassword")) { + return; + } + + // Auto-fill login form with saved credentials + (async () => { + // Wait a bit for the page to be ready + await new Promise(resolve => setTimeout(resolve, 100)); + + const credential = await getCredential(); + if (credential) { + const usernameInput = document.querySelector("#login > div:nth-child(1) > div > input"); + const passwordInput = document.querySelector("#login > div:nth-child(2) > div > input"); + const loginButton = document.getElementsByName("submit")[0]; + + if (usernameInput && passwordInput && loginButton) { + usernameInput.value = credential.id; + passwordInput.value = credential.password; + loginButton.click(); + } + } + })(); +} + +/** + * Save credentials after successful login + * Called by login handler after successful authentication + * @param {string} username - Username to save + * @param {string} password - Password to save + */ +export async function saveOnSuccess(username, password) { + if (!UtilityEnabled("SavePassword")) { + return; + } + await storeCredential(username, password); +} + +/** + * Clear credentials after failed login + * Called by login handler after failed authentication + */ +export async function clearOnFailure() { + if (!UtilityEnabled("SavePassword")) { + return; + } + await clearCredential(); +} + +/** + * Check if SavePassword feature is enabled + * @returns {boolean} + */ +export function isEnabled() { + return UtilityEnabled("SavePassword"); +} diff --git a/src/features/translate.js b/src/features/translate.js new file mode 100644 index 00000000..d82b404b --- /dev/null +++ b/src/features/translate.js @@ -0,0 +1,107 @@ +/** + * Translate Feature + * Translates English text to Chinese throughout the site + * Feature ID: Translate + * Type: F (Format/UI) + * Description: 统一使用中文,翻译了部分英文 + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize Translate feature + * Translates various English UI elements to Chinese based on current page + * + * Translations include: + * - Navbar: "Problems" -> "题库" + * - Problem set page: Form placeholders and table headers + * - Contest page: Table headers + * + * Extracted from: /home/user/XMOJ-Script/src/core/bootstrap.js + * - Lines 211-213: Navbar translation + * - Lines 1073-1078: Problemset page translations + * - Lines 1611-1617: Contest page translations + */ +export function init() { + // Only execute if Translate feature is enabled + if (!UtilityEnabled("Translate")) { + return; + } + + const pathname = location.pathname; + + // Translate navbar (on all pages) + translateNavbar(); + + // Page-specific translations + if (pathname === "/problemset.php") { + translateProblemsetPage(); + } else if (pathname === "/contest.php") { + translateContestPage(); + } +} + +/** + * Translate navbar elements + */ +function translateNavbar() { + try { + const problemsLink = document.querySelector("#navbar > ul:nth-child(1) > li:nth-child(2) > a"); + if (problemsLink) { + problemsLink.innerText = "题库"; + } + } catch (e) { + console.error('[Translate] Error translating navbar:', e); + } +} + +/** + * Translate problemset page elements + */ +function translateProblemsetPage() { + try { + // Translate search form placeholders and buttons + const problemIdInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > input"); + if (problemIdInput) { + problemIdInput.placeholder = "题目编号"; + } + + const confirmButton = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(2) > form > button"); + if (confirmButton) { + confirmButton.innerText = "确认"; + } + + const searchInput = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2) > tbody > tr > td:nth-child(3) > form > input"); + if (searchInput) { + searchInput.placeholder = "标题或内容"; + } + + // Translate table header + const statusHeader = document.querySelector("#problemset > thead > tr > th:nth-child(1)"); + if (statusHeader) { + statusHeader.innerText = "状态"; + } + } catch (e) { + console.error('[Translate] Error translating problemset page:', e); + } +} + +/** + * Translate contest page table headers + */ +function translateContestPage() { + try { + const tableHeader = document.querySelector("body > div > div.mt-3 > center > table > thead > tr"); + if (tableHeader && tableHeader.childNodes.length >= 4) { + tableHeader.childNodes[0].innerText = "编号"; + tableHeader.childNodes[1].innerText = "标题"; + tableHeader.childNodes[2].innerText = "状态"; + tableHeader.childNodes[3].remove(); + if (tableHeader.childNodes[3]) { + tableHeader.childNodes[3].innerText = "创建者"; + } + } + } catch (e) { + console.error('[Translate] Error translating contest page:', e); + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..0edf848f --- /dev/null +++ b/src/main.js @@ -0,0 +1,95 @@ +/** + * Main entry point for XMOJ Script + * This file imports all utilities and features, then initializes the application + */ + +// Core imports +import { UtilityEnabled } from './core/config.js'; +import { AdminUserList } from './core/constants.js'; + +// Utility imports +import { escapeHTML, PurifyHTML } from './utils/html.js'; +import { SmartAlert } from './utils/alerts.js'; +import { GetRelativeTime, SecondsToString, StringToSeconds, TimeToStringTime } from './utils/time.js'; +import { SizeToStringSize, CodeSizeToStringSize } from './utils/format.js'; +import { compareVersions } from './utils/version.js'; +import { RequestAPI } from './utils/api.js'; +import { storeCredential, getCredential, clearCredential } from './utils/credentials.js'; +import { RenderMathJax } from './utils/mathjax.js'; +import { TidyTable } from './utils/table.js'; +import { GetUserInfo, GetUserBadge, GetUsernameHTML } from './utils/user.js'; + +// Core application imports +import { initTheme, main } from './core/bootstrap.js'; +import { registerMenuCommands } from './core/menu.js'; + +// Feature modules imports +import { initializeFeatures, getExtractedFeatures } from './features/index.js'; + +// Page modules imports +import { initializePage, getImplementedPages } from './pages/index.js'; + +// Make utilities globally available (for compatibility with inline code) +window.escapeHTML = escapeHTML; +window.PurifyHTML = PurifyHTML; +window.SmartAlert = SmartAlert; +window.GetRelativeTime = GetRelativeTime; +window.SecondsToString = SecondsToString; +window.StringToSeconds = StringToSeconds; +window.TimeToStringTime = TimeToStringTime; +window.SizeToStringSize = SizeToStringSize; +window.CodeSizeToStringSize = CodeSizeToStringSize; +window.compareVersions = compareVersions; +window.RequestAPI = RequestAPI; +window.storeCredential = storeCredential; +window.getCredential = getCredential; +window.clearCredential = clearCredential; +window.RenderMathJax = RenderMathJax; +window.TidyTable = TidyTable; +window.GetUserInfo = GetUserInfo; +window.GetUserBadge = GetUserBadge; +window.GetUsernameHTML = GetUsernameHTML; +window.UtilityEnabled = UtilityEnabled; +window.AdminUserList = AdminUserList; + +// Register menu commands +registerMenuCommands(); + +// Initialize theme +initTheme(); + +// Initialize extracted feature modules +// These run before main() to allow early initialization (like AutoLogin) +initializeFeatures().then(() => { + console.log('[XMOJ-Script] Extracted features loaded:', getExtractedFeatures()); +}); + +// Start the main application +// Note: bootstrap.js still contains all original code for compatibility +// Extracted features in src/features/ provide the same functionality +// in a more maintainable way +main(); + +// Initialize page-specific modules after main() runs +// Page modules handle page-specific styling and DOM manipulations +// This needs to run after bootstrap.js sets up the basic structure +window.addEventListener('load', () => { + // Create context object with commonly used utilities + const pageContext = { + SearchParams: new URLSearchParams(location.search), + RenderMathJax, + RequestAPI, + TidyTable, + GetUserInfo, + GetUserBadge, + GetUsernameHTML, + GetRelativeTime, + SmartAlert, + Style: document.querySelector('style#UserScript-Style'), + IsAdmin: window.IsAdmin || false, // Set by bootstrap.js + }; + + initializePage(pageContext).then(() => { + console.log('[XMOJ-Script] Page modules available for:', getImplementedPages()); + }); +}); diff --git a/src/pages/contest.js b/src/pages/contest.js new file mode 100644 index 00000000..7c5185c6 --- /dev/null +++ b/src/pages/contest.js @@ -0,0 +1,187 @@ +/** + * Contest Page Module + * Handles all styling and functionality for /contest.php + */ + +/** + * Initialize contest page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Check if viewing specific contest or contest list + if (location.href.indexOf("?cid=") === -1) { + // Contest list page + initContestList(); + } else { + // Specific contest page + initContestView(SearchParams); + } +} + +/** + * Initialize contest list view + */ +function initContestList() { + // Style contest list table rows + const contestRows = document.querySelector("body > div > div.mt-3 > center > table > tbody")?.childNodes; + if (!contestRows) return; + + for (let i = 1; i < contestRows.length; i++) { + const currentElement = contestRows[i].childNodes[2]?.childNodes; + if (!currentElement) continue; + + // Handle different contest states + if (currentElement[1]?.childNodes[0]?.data?.indexOf("运行中") !== -1) { + handleRunningContest(currentElement); + } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("开始于") !== -1) { + handleUpcomingContest(currentElement); + } else if (currentElement[1]?.childNodes[0]?.data?.indexOf("已结束") !== -1) { + handleFinishedContest(currentElement); + } + + // Hide column and add user link + contestRows[i].childNodes[3].style.display = "none"; + const creator = contestRows[i].childNodes[4].innerHTML; + contestRows[i].childNodes[4].innerHTML = `${creator}`; + + // Store contest name + const contestId = contestRows[i].childNodes[0].innerText; + const contestName = contestRows[i].childNodes[1].innerText; + localStorage.setItem(`UserScript-Contest-${contestId}-Name`, contestName); + } +} + +/** + * Handle running contest countdown + * @param {NodeList} element - Contest row element + */ +function handleRunningContest(element) { + const time = String(element[1].childNodes[1].innerText).substring(4); + + // Parse time components + const day = parseInt(time.substring(0, time.indexOf("天"))) || 0; + const hourStart = time.indexOf("天") === -1 ? 0 : time.indexOf("天") + 1; + const hour = parseInt(time.substring(hourStart, time.indexOf("小时"))) || 0; + const minuteStart = time.indexOf("小时") === -1 ? 0 : time.indexOf("小时") + 2; + const minute = parseInt(time.substring(minuteStart, time.indexOf("分"))) || 0; + const secondStart = time.indexOf("分") === -1 ? 0 : time.indexOf("分") + 1; + const second = parseInt(time.substring(secondStart, time.indexOf("秒"))) || 0; + + // Calculate timestamp + const diff = window.diff || 0; // Global time diff + const timeStamp = new Date().getTime() + diff + ((((day * 24 + hour) * 60 + minute) * 60 + second) * 1000); + + element[1].childNodes[1].setAttribute("EndTime", timeStamp); + element[1].childNodes[1].classList.add("UpdateByJS"); +} + +/** + * Handle upcoming contest + * @param {NodeList} element - Contest row element + */ +function handleUpcomingContest(element) { + const diff = window.diff || 0; + const timeStamp = Date.parse(String(element[1].childNodes[0].data).substring(4)) + diff; + element[1].setAttribute("EndTime", timeStamp); + element[1].classList.add("UpdateByJS"); +} + +/** + * Handle finished contest + * @param {NodeList} element - Contest row element + */ +function handleFinishedContest(element) { + const timeStamp = String(element[1].childNodes[0].data).substring(4); + element[1].childNodes[0].data = " 已结束 "; + element[1].className = "red"; + + const span = document.createElement("span"); + span.className = "green"; + span.innerHTML = timeStamp; + element[1].appendChild(span); +} + +/** + * Initialize specific contest view + * @param {URLSearchParams} SearchParams - URL search parameters + */ +function initContestView(SearchParams) { + // Update title + const title = document.getElementsByTagName("h3")[0]; + if (title) { + title.innerHTML = "比赛" + title.innerHTML.substring(7); + } + + // Handle countdown timer + const timeLeft = document.querySelector("#time_left"); + if (timeLeft) { + const centerNode = document.querySelector("body > div > div.mt-3 > center"); + let endTimeText = centerNode?.childNodes[3]?.data; + + if (endTimeText) { + endTimeText = endTimeText.substring(endTimeText.indexOf("结束时间是:") + 6, endTimeText.lastIndexOf("。")); + const endTime = new Date(endTimeText).getTime(); + + if (new Date().getTime() < endTime) { + timeLeft.classList.add("UpdateByJS"); + timeLeft.setAttribute("EndTime", endTime); + } + } + } + + // Format contest information + const infoDiv = document.querySelector("body > div > div.mt-3 > center > div"); + if (infoDiv) { + let htmlData = infoDiv.innerHTML; + htmlData = htmlData.replaceAll("  \n  ", " "); + htmlData = htmlData.replaceAll("
    开始于: ", "开始时间:"); + htmlData = htmlData.replaceAll("\n结束于: ", "
    结束时间:"); + htmlData = htmlData.replaceAll("\n订正截止日期: ", "
    订正截止日期:"); + htmlData = htmlData.replaceAll("\n现在时间: ", "当前时间:"); + htmlData = htmlData.replaceAll("\n状态:", "
    状态:"); + infoDiv.innerHTML = htmlData; + } + + // Format problem list + formatProblemList(); + + // Store problem count + const problemCount = document.querySelector("#problemset > tbody")?.rows.length; + if (problemCount) { + localStorage.setItem(`UserScript-Contest-${SearchParams.get("cid")}-ProblemCount`, problemCount); + } +} + +/** + * Format problem list in contest + */ +function formatProblemList() { + const tbody = document.querySelector("#problemset > tbody"); + if (!tbody) return; + + // Format problem names + tbody.innerHTML = tbody.innerHTML.replaceAll( + /\t ([0-9]*)      问题  ([^<]*)/g, + "$2. $1" + ); + tbody.innerHTML = tbody.innerHTML.replaceAll( + /\t\*([0-9]*)      问题  ([^<]*)/g, + "拓展$2. $1" + ); + + // Ensure status divs exist + const rows = tbody.rows; + for (let i = 0; i < rows.length; i++) { + if (rows[i].childNodes[0]?.children.length === 0) { + rows[i].childNodes[0].innerHTML = '
    '; + } + + // Make problem title link open in new tab + const titleLink = rows[i].children[2]?.children[0]; + if (titleLink) { + titleLink.target = "_blank"; + } + } +} diff --git a/src/pages/contestrank.js b/src/pages/contestrank.js new file mode 100644 index 00000000..3421b76e --- /dev/null +++ b/src/pages/contestrank.js @@ -0,0 +1,224 @@ +/** + * Contest Rank Pages Module + * Handles all styling and functionality for /contestrank-oi.php and /contestrank-correct.php + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize contest rank page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams, TidyTable, GetUsernameHTML, Style } = context; + + const pathname = location.pathname; + const isOI = pathname === "/contestrank-oi.php"; + const isCorrect = pathname === "/contestrank-correct.php"; + + // Create rank table if doesn't exist + if (document.querySelector("#rank") === null) { + document.querySelector("body > div > div.mt-3").innerHTML = + '

    比赛排名

    '; + } + + // Check if in UserScript mode + const byUserScript = SearchParams.get("ByUserScript") !== null; + + // Handle title and headers + const titleElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (titleElement.innerText === "比赛排名") { + document.querySelector("#rank").innerText = "比赛暂时还没有排名"; + } else { + if (isOI && !byUserScript) { + await initOIRanking(titleElement, TidyTable, GetUsernameHTML); + } else if (isCorrect) { + initCorrectRanking(titleElement, TidyTable, GetUsernameHTML); + } + } + + // Add page styles + addPageStyles(Style); + + // Hide link and set title + const linkElement = document.querySelector("body > div.container > div > center > a"); + if (linkElement) { + linkElement.style.display = "none"; + } + + const centerDiv = document.querySelector("body > div.container > div > center"); + if (centerDiv) { + centerDiv.style.paddingBottom = "10px"; + } + + document.title = titleElement.innerText; +} + +/** + * Initialize OI ranking table + */ +async function initOIRanking(titleElement, TidyTable, GetUsernameHTML) { + // Update title + const originalTitle = titleElement.innerText; + titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(OI排名)"; + + // Translate headers + translateRankHeaders(); + + // Refresh ranking function + const refreshOIRank = async () => { + await fetch(location.href) + .then((response) => response.text()) + .then(async (response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + TidyTable(parsedDocument.getElementById("rank")); + + const rows = parsedDocument.getElementById("rank").rows; + for (let i = 1; i < rows.length; i++) { + // Add medal badge + const metalCell = rows[i].cells[0]; + const metal = document.createElement("span"); + metal.innerText = metalCell.innerText; + metal.className = "badge text-bg-primary"; + metalCell.innerText = ""; + metalCell.appendChild(metal); + + // Format username + GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); + + // Style problem cells + for (let j = 5; j < rows[i].cells.length; j++) { + styleProblemCell(rows[i].cells[j]); + } + } + + // Update DOM + document.querySelector("#rank > tbody").innerHTML = parsedDocument.querySelector("#rank > tbody").innerHTML; + }); + }; + + // Initial refresh + await refreshOIRank(); + + // Auto-refresh on focus if enabled + if (UtilityEnabled("AutoRefresh")) { + addEventListener("focus", refreshOIRank); + } +} + +/** + * Initialize correct ranking table + */ +function initCorrectRanking(titleElement, TidyTable, GetUsernameHTML) { + // Update title + if (UtilityEnabled("ResetType")) { + const originalTitle = titleElement.innerText; + titleElement.innerText = originalTitle.substring(originalTitle.indexOf(" -- ") + 4) + "(订正排名)"; + + const linkElement = document.querySelector("body > div > div.mt-3 > center > a"); + if (linkElement) { + linkElement.remove(); + } + } + + // Translate headers + translateRankHeaders(); + + // Style rows + TidyTable(document.getElementById("rank")); + const rows = document.getElementById("rank").rows; + + for (let i = 1; i < rows.length; i++) { + // Add medal badge + const metalCell = rows[i].cells[0]; + const metal = document.createElement("span"); + metal.innerText = metalCell.innerText; + metal.className = "badge text-bg-primary"; + metalCell.innerText = ""; + metalCell.appendChild(metal); + + // Format username + GetUsernameHTML(rows[i].cells[1], rows[i].cells[1].innerText); + + // Style problem cells + for (let j = 5; j < rows[i].cells.length; j++) { + styleProblemCell(rows[i].cells[j]); + } + } +} + +/** + * Translate rank table headers + */ +function translateRankHeaders() { + try { + const headers = document.querySelectorAll("#rank > thead > tr > th"); + if (headers.length >= 5) { + headers[0].innerText = "排名"; + headers[1].innerText = "用户"; + headers[2].innerText = "昵称"; + headers[3].innerText = "AC数"; + headers[4].innerText = "得分"; + } + } catch (error) { + console.error('[ContestRank] Error translating headers:', error); + } +} + +/** + * Style problem cell based on status + * @param {HTMLTableCellElement} cell - Table cell to style + */ +function styleProblemCell(cell) { + let innerText = cell.innerText; + let backgroundColor = cell.style.backgroundColor; + + // Parse RGB values + const red = parseInt(backgroundColor.substring(4, backgroundColor.indexOf(","))); + const green = parseInt(backgroundColor.substring(backgroundColor.indexOf(",") + 2, backgroundColor.lastIndexOf(","))); + const blue = parseInt(backgroundColor.substring(backgroundColor.lastIndexOf(",") + 2, backgroundColor.lastIndexOf(")"))); + + const noData = (red === 238 && green === 238 && blue === 238); + const firstBlood = (red === 170 && green === 170 && blue === 255); + const solved = (green === 255); + + let errorCount = 0; + if (solved) { + errorCount = (blue === 170 ? 5 : (blue - 51) / 32); + } else { + errorCount = (blue === 22 ? 15 : (170 - blue) / 10); + } + + // Apply styling + if (noData) { + backgroundColor = ""; + } else if (firstBlood) { + backgroundColor = "rgb(127, 127, 255)"; + } else if (solved) { + backgroundColor = `rgb(0, 255, 0, ${Math.max(1 / 10 * (10 - errorCount), 0.2)})`; + if (errorCount !== 0) { + innerText += ` (${errorCount === 5 ? "4+" : errorCount})`; + } + } else { + backgroundColor = `rgba(255, 0, 0, ${Math.min(errorCount / 10 + 0.2, 1)})`; + if (errorCount !== 0) { + innerText += ` (${errorCount === 15 ? "14+" : errorCount})`; + } + } + + // Set cell text safely without re-parsing HTML + cell.textContent = innerText; + cell.style.backgroundColor = backgroundColor; + cell.style.color = UtilityEnabled("DarkMode") ? "white" : "black"; +} + +/** + * Add page-specific styles + * @param {HTMLStyleElement} Style - Style element + */ +function addPageStyles(Style) { + Style.innerHTML += ` +td { + white-space: nowrap; +}`; +} diff --git a/src/pages/index.js b/src/pages/index.js new file mode 100644 index 00000000..440e3d6a --- /dev/null +++ b/src/pages/index.js @@ -0,0 +1,57 @@ +/** + * Page loader - Initializes page-specific modules based on current URL + * + * This module provides a centralized way to load page-specific styling and functionality. + * Each page module handles its own initialization and styling. + */ + +import { init as initProblemPage } from './problem.js'; +import { init as initContestPage } from './contest.js'; +import { init as initStatusPage } from './status.js'; +import { init as initSubmitPage } from './submit.js'; +import { init as initProblemsetPage } from './problemset.js'; +import { init as initUserinfoPage } from './userinfo.js'; +import { init as initLoginPage } from './login.js'; +import { init as initContestRankPage } from './contestrank.js'; + +/** + * Page route mapping + */ +const PAGE_ROUTES = { + '/problem.php': initProblemPage, + '/contest.php': initContestPage, + '/status.php': initStatusPage, + '/submitpage.php': initSubmitPage, + '/problemset.php': initProblemsetPage, + '/userinfo.php': initUserinfoPage, + '/loginpage.php': initLoginPage, + '/contestrank-oi.php': initContestRankPage, + '/contestrank-correct.php': initContestRankPage, +}; + +/** + * Initialize page-specific module based on current pathname + * @param {Object} context - Shared context object with dependencies + */ +export async function initializePage(context) { + const pathname = location.pathname; + + const pageInit = PAGE_ROUTES[pathname]; + + if (pageInit) { + try { + await pageInit(context); + console.log(`[XMOJ-Script] Initialized page module: ${pathname}`); + } catch (error) { + console.error(`[XMOJ-Script] Error initializing page ${pathname}:`, error); + } + } +} + +/** + * Get list of all implemented page modules + * @returns {string[]} List of page pathnames with modules + */ +export function getImplementedPages() { + return Object.keys(PAGE_ROUTES); +} diff --git a/src/pages/login.js b/src/pages/login.js new file mode 100644 index 00000000..acc5e565 --- /dev/null +++ b/src/pages/login.js @@ -0,0 +1,142 @@ +/** + * Login Page Module + * Handles all styling and functionality for /loginpage.php + */ + +import { UtilityEnabled } from '../core/config.js'; +import { storeCredential, clearCredential } from '../utils/credentials.js'; + +/** + * Initialize login page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + // Replace login form with Bootstrap-styled version + if (UtilityEnabled("NewBootstrap")) { + replaceLoginForm(); + } + + // Attach login button handler + attachLoginHandler(); +} + +/** + * Replace login form with modern Bootstrap styling + */ +function replaceLoginForm() { + try { + const loginForm = document.querySelector("#login"); + if (!loginForm) return; + + loginForm.innerHTML = `
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    `; + } catch (error) { + console.error('[Login] Error replacing login form:', error); + } +} + +/** + * Attach login button click handler + */ +function attachLoginHandler() { + try { + // Add error message div + const errorText = document.createElement("div"); + errorText.style.color = "red"; + errorText.style.marginBottom = "5px"; + const loginForm = document.querySelector("#login"); + if (!loginForm) return; + loginForm.appendChild(errorText); + + // Get login button + const loginButton = document.getElementsByName("submit")[0]; + if (!loginButton) { + console.warn('[Login] Login button not found'); + return; + } + + // Attach click handler + loginButton.addEventListener("click", async () => { + const username = document.getElementsByName("user_id")[0].value; + const password = document.getElementsByName("password")[0].value; + + if (username === "" || password === "") { + errorText.innerText = "用户名或密码不能为空"; + return; + } + + try { + const response = await fetch("https://www.xmoj.tech/login.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: "user_id=" + encodeURIComponent(username) + "&password=" + hex_md5(password) + }); + + const responseText = await response.text(); + + if (UtilityEnabled("LoginFailed")) { + if (responseText.indexOf("history.go(-2);") !== -1) { + // Login successful + if (UtilityEnabled("SavePassword")) { + await storeCredential(username, password); + } + + let newPage = localStorage.getItem("UserScript-LastPage"); + if (newPage === null) { + newPage = "https://www.xmoj.tech/index.php"; + } + location.href = newPage; + } else { + // Login failed + if (UtilityEnabled("SavePassword")) { + await clearCredential(); + } + + let errorMsg = responseText.substring(responseText.indexOf("alert('") + 7); + errorMsg = errorMsg.substring(0, errorMsg.indexOf("');")); + + if (errorMsg === "UserName or Password Wrong!") { + errorText.innerText = "用户名或密码错误!"; + } else { + errorText.innerText = errorMsg; + } + } + } else { + // LoginFailed feature disabled, use default behavior + document.innerHTML = responseText; + } + } catch (error) { + console.error('[Login] Error during login:', error); + errorText.innerText = "登录请求失败,请重试"; + } + }); + } catch (error) { + console.error('[Login] Error attaching login handler:', error); + } +} diff --git a/src/pages/problem.js b/src/pages/problem.js new file mode 100644 index 00000000..fcdfb783 --- /dev/null +++ b/src/pages/problem.js @@ -0,0 +1,208 @@ +/** + * Problem Page Module + * Handles all styling and functionality for /problem.php + */ + +import { UtilityEnabled } from '../core/config.js'; +import { TidyTable } from '../utils/table.js'; + +/** + * Initialize problem page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams, RenderMathJax, RequestAPI, Style } = context; + + // Render MathJax + await RenderMathJax(); + + // Check if problem doesn't exist + if (document.querySelector("body > div > div.mt-3 > h2") != null) { + document.querySelector("body > div > div.mt-3").innerHTML = "没有此题目或题目对你不可见"; + setTimeout(() => { + location.href = "https://www.xmoj.tech/problemset.php"; + }, 1000); + return; + } + + let PID = SearchParams.get("cid") + ? localStorage.getItem(`UserScript-Contest-${SearchParams.get("cid")}-Problem-${SearchParams.get("pid")}-PID`) + : SearchParams.get("id"); + + // Handle null PID for contest problems (fetch from page) + if (PID === null && SearchParams.get("cid")) { + await fetch(location.href) + .then((response) => response.text()) + .then((response) => { + const parsedDocument = new DOMParser().parseFromString(response, "text/html"); + const allAnchors = parsedDocument.querySelectorAll('.mt-3 > center:nth-child(1) > a'); + const SubmitLink = Array.from(allAnchors).find(a => a.textContent.trim() === '提交'); + if (SubmitLink && SubmitLink.href) { + const url = new URL(SubmitLink.href); + PID = url.searchParams.get("id"); + localStorage.setItem(`UserScript-Contest-${SearchParams.get("cid")}-Problem-${SearchParams.get("pid")}-PID`, PID); + } + }); + } + + // Fix spacing + if (document.querySelector("body > div > div.mt-3 > center").lastElementChild !== null) { + document.querySelector("body > div > div.mt-3 > center").lastElementChild.style.marginLeft = "10px"; + } + + // Fix submit button + fixSubmitButton(); + + // Style sample data cards + const sampleDataElements = document.querySelectorAll(".sampledata"); + for (let i = 0; i < sampleDataElements.length; i++) { + sampleDataElements[i].parentElement.className = "card"; + } + + // Handle IO file information + handleIOFile(PID); + + // Add discussion button (if Discussion feature is enabled) + if (UtilityEnabled("Discussion")) { + addDiscussionButton(PID, SearchParams, RequestAPI); + } + + // Tidy tables + const tables = document.getElementsByTagName("table"); + for (let i = 0; i < tables.length; i++) { + TidyTable(tables[i]); + } + + // Add custom styles + addPageStyles(Style); +} + +/** + * Fix submit button styling and behavior + */ +function fixSubmitButton() { + // Find submit link by text content (more reliable than nth-child selectors) + const links = document.querySelectorAll('.mt-3 > center:nth-child(1) > a'); + const submitLink = Array.from(links).find(a => a.textContent.trim() === '提交'); + + if (!submitLink) return; + + // Create submit button + const submitButton = document.createElement('button'); + submitButton.id = 'SubmitButton'; + submitButton.className = 'btn btn-outline-secondary'; + submitButton.textContent = '提交'; + submitButton.onclick = function () { + window.location.href = submitLink.href; + }; + + // Replace the link with the button + submitLink.parentNode.replaceChild(submitButton, submitLink); + + // Remove the button's outer brackets + const container = document.querySelector('.mt-3 > center:nth-child(1)'); + if (container) { + let str = container.innerHTML; + let target = submitButton.outerHTML; + let result = str.replace(new RegExp(`(.?)${target}(.?)`, 'g'), target); + container.innerHTML = result; + + // Re-attach click handler after innerHTML replacement + const newButton = document.querySelector('html body.placeholder-glow div.container div.mt-3 center button#SubmitButton.btn.btn-outline-secondary'); + if (newButton) { + newButton.onclick = function () { + window.location.href = submitLink.href; + }; + } + } +} + +/** + * Handle IO file information display + * @param {string} PID - Problem ID + */ +function handleIOFile(PID) { + const ioFileElement = document.querySelector("body > div > div.mt-3 > center > h3"); + if (!ioFileElement) return; + + // Move child nodes out of h3 + while (ioFileElement.childNodes.length >= 1) { + ioFileElement.parentNode.insertBefore(ioFileElement.childNodes[0], ioFileElement); + } + ioFileElement.parentNode.insertBefore(document.createElement("br"), ioFileElement); + ioFileElement.remove(); + + // Extract and store IO filename + const centerNode = document.querySelector("body > div > div.mt-3 > center"); + if (centerNode && centerNode.childNodes[2]) { + const temp = centerNode.childNodes[2].data.trim(); + const ioFilename = temp.substring(0, temp.length - 3); + localStorage.setItem(`UserScript-Problem-${PID}-IOFilename`, ioFilename); + } +} + +/** + * Add discussion button with unread badge + * @param {string} PID - Problem ID + * @param {URLSearchParams} SearchParams - URL search parameters + * @param {Function} RequestAPI - API request function + */ +function addDiscussionButton(PID, SearchParams, RequestAPI) { + const discussButton = document.createElement("button"); + discussButton.className = "btn btn-outline-secondary position-relative"; + discussButton.innerHTML = `讨论`; + discussButton.style.marginLeft = "10px"; + discussButton.type = "button"; + + discussButton.addEventListener("click", () => { + const problemId = SearchParams.get("cid") ? PID : SearchParams.get("id"); + open(`https://www.xmoj.tech/discuss3/discuss.php?pid=${problemId}`, "_blank"); + }); + + document.querySelector("body > div > div.mt-3 > center").appendChild(discussButton); + + // Add unread badge + const unreadBadge = document.createElement("span"); + unreadBadge.className = "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"; + unreadBadge.style.display = "none"; + discussButton.appendChild(unreadBadge); + + // Refresh unread count + const refreshCount = () => { + RequestAPI("GetPostCount", { + "ProblemID": Number(PID) + }, (response) => { + if (response.Success && response.Data.DiscussCount != 0) { + unreadBadge.innerText = response.Data.DiscussCount; + unreadBadge.style.display = ""; + } + }); + }; + + refreshCount(); + addEventListener("focus", refreshCount); +} + +/** + * Add custom page styles + * @param {HTMLStyleElement} Style - Style element to append to + */ +function addPageStyles(Style) { + Style.innerHTML += ` +code, kbd, pre, samp { + font-family: monospace, Consolas, 'Courier New'; + font-size: 1rem; +} +pre { + padding: 0.3em 0.5em; + margin: 0.5em 0; +} +.in-out { + overflow: hidden; + display: flex; + padding: 0.5em 0; +} +.in-out .in-out-item { + flex: 1; +}`; +} diff --git a/src/pages/problemset.js b/src/pages/problemset.js new file mode 100644 index 00000000..15819c71 --- /dev/null +++ b/src/pages/problemset.js @@ -0,0 +1,103 @@ +/** + * Problemset Page Module + * Handles all styling and functionality for /problemset.php + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize problemset page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Set column widths + if (UtilityEnabled("ResetType")) { + setColumnWidths(); + } + + // Replace search forms with improved layout + replaceSearchForms(SearchParams); + + // Store problem names in localStorage + storeProblemNames(); +} + +/** + * Set column widths for problemset table + */ +function setColumnWidths() { + try { + const headers = document.querySelectorAll("#problemset > thead > tr > th"); + if (headers.length >= 5) { + headers[0].style.width = "5%"; // Status + headers[1].style.width = "10%"; // ID + headers[2].style.width = "75%"; // Title + headers[3].style.width = "5%"; // AC ratio + headers[4].style.width = "5%"; // Difficulty + } + } catch (error) { + console.error('[Problemset] Error setting column widths:', error); + } +} + +/** + * Replace search forms with improved layout + * @param {URLSearchParams} SearchParams - URL search parameters + */ +function replaceSearchForms(SearchParams) { + try { + const oldTable = document.querySelector("body > div > div.mt-3 > center > table:nth-child(2)"); + if (!oldTable) return; + + oldTable.outerHTML = ` +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    `; + + // Restore search value if present + const searchParam = SearchParams.get("search"); + if (searchParam) { + const searchInput = document.querySelector("body > div > div.mt-3 > center > div > div:nth-child(3) > form > input"); + if (searchInput) { + searchInput.value = searchParam; + } + } + } catch (error) { + console.error('[Problemset] Error replacing search forms:', error); + } +} + +/** + * Store problem names in localStorage for quick access + */ +function storeProblemNames() { + try { + const rows = document.querySelector("#problemset")?.rows; + if (!rows) return; + + for (let i = 1; i < rows.length; i++) { + const problemId = rows[i].children[1]?.innerText; + const problemName = rows[i].children[2]?.innerText; + + if (problemId && problemName) { + localStorage.setItem(`UserScript-Problem-${problemId}-Name`, problemName); + } + } + } catch (error) { + console.error('[Problemset] Error storing problem names:', error); + } +} diff --git a/src/pages/status.js b/src/pages/status.js new file mode 100644 index 00000000..5431ad91 --- /dev/null +++ b/src/pages/status.js @@ -0,0 +1,29 @@ +/** + * Status Page Module + * Handles all styling and functionality for /status.php + */ + +/** + * Initialize status page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Only proceed if not in special UserScript mode + if (SearchParams.get("ByUserScript") !== null) { + return; + } + + // Set page title + document.title = "提交状态"; + + // Remove old script tags + const oldScript = document.querySelector("body > script:nth-child(5)"); + if (oldScript) { + oldScript.remove(); + } + + // Additional status page initialization can go here + // Most status page features are handled by feature modules +} diff --git a/src/pages/submit.js b/src/pages/submit.js new file mode 100644 index 00000000..1a53005e --- /dev/null +++ b/src/pages/submit.js @@ -0,0 +1,25 @@ +/** + * Submit Page Module + * Handles all styling and functionality for /submitpage.php + */ + +/** + * Initialize submit page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams } = context; + + // Set page title + const problemId = SearchParams.get("id"); + const contestId = SearchParams.get("cid"); + + if (problemId) { + document.title = `提交代码: 题目${Number(problemId)}`; + } else if (contestId) { + document.title = `提交代码: 比赛${Number(contestId)}`; + } + + // Additional submit page initialization can go here + // Most submit page features are handled by feature modules and CodeMirror initialization +} diff --git a/src/pages/userinfo.js b/src/pages/userinfo.js new file mode 100644 index 00000000..ab342067 --- /dev/null +++ b/src/pages/userinfo.js @@ -0,0 +1,304 @@ +/** + * User Info Page Module + * Handles all styling and functionality for /userinfo.php + */ + +import { UtilityEnabled } from '../core/config.js'; + +/** + * Initialize user info page + * @param {Object} context - Page context with utilities + */ +export async function init(context) { + const { SearchParams, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin } = context; + + // Check if in ByUserScript mode (upload standard solution) + if (SearchParams.get("ByUserScript") !== null) { + document.title = "上传标程"; + // Upload standard solution UI is handled in bootstrap.js + return; + } + + // Clean up submission section if RemoveUseless is enabled + if (UtilityEnabled("RemoveUseless")) { + cleanupSubmissionSection(); + } + + // Execute embedded script (chart initialization) + executeEmbeddedScript(); + + // Translate table headers + translateTableHeaders(); + + // Extract and display user information + await displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); +} + +/** + * Clean up submission section + */ +function cleanupSubmissionSection() { + try { + const submissionElement = document.getElementById("submission"); + if (submissionElement) { + const childNodes = submissionElement.childNodes; + for (let i = childNodes.length - 1; i >= 0; i--) { + childNodes[i].remove(); + } + } + } catch (error) { + console.error('[UserInfo] Error cleaning up submission section:', error); + } +} + +/** + * Execute embedded chart script + */ +function executeEmbeddedScript() { + try { + const scriptElement = document.querySelector("body > script:nth-child(5)"); + if (scriptElement) { + eval(scriptElement.innerHTML); + } + } catch (error) { + console.error('[UserInfo] Error executing embedded script:', error); + } +} + +/** + * Translate table headers + */ +function translateTableHeaders() { + try { + // Remove first row + const firstRow = document.querySelector("#statics > tbody > tr:nth-child(1)"); + if (firstRow) { + firstRow.remove(); + } + + // Translate remaining headers + const rows = document.querySelector("#statics > tbody")?.children; + if (!rows) return; + + for (let i = 0; i < rows.length; i++) { + if (rows[i].children[0]) { + const headerText = rows[i].children[0].innerText; + if (headerText === "Statistics") { + rows[i].children[0].innerText = "统计"; + } else if (headerText === "Email:") { + rows[i].children[0].innerText = "电子邮箱"; + } + rows[i].children[1].removeAttribute("align"); + } + } + } catch (error) { + console.error('[UserInfo] Error translating table headers:', error); + } +} + +/** + * Display user profile with avatar and solved problems + * @param {Function} GetUserInfo - Function to get user info + * @param {Function} GetUserBadge - Function to get user badge + * @param {Function} GetRelativeTime - Function to format relative time + * @param {Function} RequestAPI - Function to make API requests + * @param {Function} SmartAlert - Function to show alerts + * @param {boolean} IsAdmin - Whether current user is admin + */ +async function displayUserProfile(GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { + try { + // Extract AC problems + const acCell = document.querySelector("#statics > tbody > tr:nth-child(1) > td:nth-child(3)"); + const acProblems = []; + + if (acCell) { + const childNodes = acCell.childNodes; + for (let i = 0; i < childNodes.length; i++) { + if (childNodes[i].tagName === "A" && childNodes[i].href.indexOf("problem.php?id=") !== -1) { + acProblems.push(Number(childNodes[i].innerText.trim())); + } + } + acCell.remove(); + } + + // Extract user info from caption + const caption = document.querySelector("#statics > caption"); + if (!caption) return; + + const captionText = caption.childNodes[0].data.trim(); + const [userId, userNick] = captionText.split("--"); + caption.remove(); + + // Set page title + document.title = `用户 ${userId} 的个人中心`; + + // Create new layout + await createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin); + } catch (error) { + console.error('[UserInfo] Error displaying user profile:', error); + } +} + +/** + * Create user profile layout with avatar, info, and solved problems + */ +async function createUserLayout(userId, userNick, acProblems, GetUserInfo, GetUserBadge, GetRelativeTime, RequestAPI, SmartAlert, IsAdmin) { + // Create main row + const row = document.createElement("div"); + row.className = "row"; + + // Left column + const leftDiv = document.createElement("div"); + leftDiv.className = "col-md-5"; + row.appendChild(leftDiv); + + // Avatar and user info + const leftTopDiv = document.createElement("div"); + leftTopDiv.className = "row mb-2"; + leftDiv.appendChild(leftTopDiv); + + // Avatar + const avatarContainer = document.createElement("div"); + avatarContainer.classList.add("col-auto"); + const avatarElement = document.createElement("img"); + + const userInfo = await GetUserInfo(userId); + const emailHash = userInfo?.EmailHash; + + if (!emailHash) { + avatarElement.src = `https://cravatar.cn/avatar/00000000000000000000000000000000?d=mp&f=y`; + } else { + avatarElement.src = `https://cravatar.cn/avatar/${emailHash}?d=retro`; + } + + avatarElement.classList.add("rounded", "me-2"); + avatarElement.style.height = "120px"; + avatarContainer.appendChild(avatarElement); + leftTopDiv.appendChild(avatarContainer); + + // User info + const userInfoElement = document.createElement("div"); + userInfoElement.classList.add("col-auto"); + userInfoElement.style.lineHeight = "40px"; + // Safer text insertion without re-parsing HTML + userInfoElement.appendChild(document.createTextNode(`用户名:${userId}`)); + userInfoElement.appendChild(document.createElement('br')); + userInfoElement.appendChild(document.createTextNode(`昵称:${userNick}`)); + userInfoElement.appendChild(document.createElement('br')); + + if (UtilityEnabled("Rating")) { + userInfoElement.appendChild(document.createTextNode(`评分:${userInfo?.Rating || 'N/A'}`)); + userInfoElement.appendChild(document.createElement('br')); + } + + // Last online time (async) + const lastOnlineElement = document.createElement('div'); + lastOnlineElement.innerHTML = "最后在线:加载中...
    "; + userInfoElement.appendChild(lastOnlineElement); + + RequestAPI("LastOnline", { "Username": userId }, (result) => { + if (result.Success) { + lastOnlineElement.innerHTML = `最后在线:${GetRelativeTime(result.Data.logintime)}
    `; + } else { + lastOnlineElement.innerHTML = "最后在线:近三个月内从未
    "; + } + }); + + // Badge management buttons (admin only) + if (IsAdmin) { + await addBadgeManagement(userId, userInfoElement, GetUserBadge, RequestAPI, SmartAlert); + } + + leftTopDiv.appendChild(userInfoElement); + + // Move statistics table to left column + const leftTable = document.querySelector("body > div > div > center > table"); + if (leftTable) { + leftDiv.appendChild(leftTable); + } + + // Right column - AC problems + const rightDiv = document.createElement("div"); + rightDiv.className = "col-md-7"; + row.appendChild(rightDiv); + // Heading + const heading = document.createElement('h5'); + heading.appendChild(document.createTextNode('已解决题目')); + rightDiv.appendChild(heading); + + for (const problemId of acProblems) { + const link = document.createElement('a'); + link.href = `https://www.xmoj.tech/problem.php?id=${problemId}`; + link.target = '_blank'; + link.appendChild(document.createTextNode(String(problemId))); + rightDiv.appendChild(link); + rightDiv.appendChild(document.createTextNode(' ')); + } + + // Replace page content + const contentDiv = document.querySelector("body > div > div"); + if (contentDiv) { + contentDiv.innerHTML = ""; + contentDiv.appendChild(row); + } +} + +/** + * Add badge management buttons for admins + */ +async function addBadgeManagement(userId, container, GetUserBadge, RequestAPI, SmartAlert) { + const badgeInfo = await GetUserBadge(userId); + + if (badgeInfo.Content !== "") { + // Delete badge button + const deleteBadgeButton = document.createElement("button"); + deleteBadgeButton.className = "btn btn-outline-danger btn-sm"; + deleteBadgeButton.innerText = "删除标签"; + deleteBadgeButton.addEventListener("click", async () => { + if (confirm("您确定要删除此标签吗?")) { + RequestAPI("DeleteBadge", { "UserID": userId }, (response) => { + if (response.Success) { + clearBadgeCache(userId); + window.location.reload(); + } else { + SmartAlert(response.Message); + } + }); + } + }); + container.appendChild(deleteBadgeButton); + } else { + // Add badge button + const addBadgeButton = document.createElement("button"); + addBadgeButton.className = "btn btn-outline-primary btn-sm"; + addBadgeButton.innerText = "添加标签"; + addBadgeButton.addEventListener("click", async () => { + RequestAPI("NewBadge", { "UserID": userId }, (response) => { + if (response.Success) { + clearBadgeCache(userId); + window.location.reload(); + } else { + SmartAlert(response.Message); + } + }); + }); + container.appendChild(addBadgeButton); + } +} + +/** + * Clear badge cache for a user + * @param {string} userId - User ID + */ +function clearBadgeCache(userId) { + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(`UserScript-User-${userId}-Badge-`)) { + keysToRemove.push(key); + } + } + for (const key of keysToRemove) { + localStorage.removeItem(key); + } +} diff --git a/src/utils/alerts.js b/src/utils/alerts.js new file mode 100644 index 00000000..0b939faf --- /dev/null +++ b/src/utils/alerts.js @@ -0,0 +1,14 @@ +/** + * Alert utilities + */ + +/** + * Shows an alert only if the message has changed + * @param {string} Message - The message to display + */ +export let SmartAlert = (Message) => { + if (localStorage.getItem("UserScript-Alert") !== Message) { + alert(Message); + } + localStorage.setItem("UserScript-Alert", Message); +}; diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 00000000..36694a73 --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,82 @@ +/** + * API request utilities + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Make an API request to the backend + * @param {string} Action - The API action + * @param {Object} Data - The data to send + * @param {Function} CallBack - Callback function to handle response + */ +export let RequestAPI = (Action, Data, CallBack) => { + try { + let Session = ""; + let Temp = document.cookie.split(";"); + for (let i = 0; i < Temp.length; i++) { + if (Temp[i].includes("PHPSESSID")) { + Session = Temp[i].split("=")[1]; + } + } + if (Session === "") { //The cookie is httpOnly + GM.cookie.set({ + name: 'PHPSESSID', + value: (Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2)).substring(0, 28), + path: "/" + }) + .then(() => { + console.log('Reset PHPSESSID successfully.'); + location.reload(); //Refresh the page to auth with the new PHPSESSID + }) + .catch((error) => { + console.error(error); + }); + } + + // Get current username from profile + let CurrentUsername = ""; + if (document.querySelector("#profile") !== null) { + CurrentUsername = document.querySelector("#profile").innerText; + CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); + } + + let PostData = { + "Authentication": { + "SessionID": Session, "Username": CurrentUsername, + }, "Data": Data, "Version": GM_info.script.version, "DebugMode": UtilityEnabled("DebugMode") + }; + let DataString = JSON.stringify(PostData); + if (UtilityEnabled("DebugMode")) { + console.log("Sent for", Action + ":", DataString); + } + GM_xmlhttpRequest({ + method: "POST", + url: (UtilityEnabled("SuperDebug") ? "http://127.0.0.1:8787/" : "https://api.xmoj-bbs.me/") + Action, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "XMOJ-UserID": CurrentUsername, + "XMOJ-Script-Version": GM_info.script.version, + "DebugMode": UtilityEnabled("DebugMode") + }, + data: DataString, + onload: (Response) => { + if (UtilityEnabled("DebugMode")) { + console.log("Received for", Action + ":", Response.responseText); + } + try { + CallBack(JSON.parse(Response.responseText)); + } catch (Error) { + console.log(Response.responseText); + } + } + }); + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; diff --git a/src/utils/credentials.js b/src/utils/credentials.js new file mode 100644 index 00000000..a45542a0 --- /dev/null +++ b/src/utils/credentials.js @@ -0,0 +1,47 @@ +/** + * Credential storage utilities using the Credentials API + */ + +/** + * Store user credentials + * @param {string} username - Username + * @param {string} password - Password + */ +export let storeCredential = async (username, password) => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + const credential = new PasswordCredential({id: username, password: password}); + await navigator.credentials.store(credential); + } catch (e) { + console.error(e); + } + } +}; + +/** + * Get stored credentials + * @returns {Promise} The stored credentials or null + */ +export let getCredential = async () => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + return await navigator.credentials.get({password: true, mediation: 'optional'}); + } catch (e) { + console.error(e); + } + } + return null; +}; + +/** + * Clear stored credentials + */ +export let clearCredential = async () => { + if ('credentials' in navigator && window.PasswordCredential) { + try { + await navigator.credentials.preventSilentAccess(); + } catch (e) { + console.error(e); + } + } +}; diff --git a/src/utils/format.js b/src/utils/format.js new file mode 100644 index 00000000..b43a0cb9 --- /dev/null +++ b/src/utils/format.js @@ -0,0 +1,62 @@ +/** + * Formatting utilities for sizes and other values + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Converts a memory size in bytes to a human-readable string representation. + * @param {number} Memory - The memory size in bytes. + * @returns {string} The human-readable string representation of the memory size. + */ +export let SizeToStringSize = (Memory) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Memory < 1024) { + return Memory + "KB"; + } else if (Memory < 1024 * 1024) { + return (Memory / 1024).toFixed(2) + "MB"; + } else if (Memory < 1024 * 1024 * 1024) { + return (Memory / 1024 / 1024).toFixed(2) + "GB"; + } else { + return (Memory / 1024 / 1024 / 1024).toFixed(2) + "TB"; + } + } else { + return Memory; + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; + +/** + * Converts a code size in bytes to a human-readable string representation. + * @param {number} Memory - The code size in bytes. + * @returns {string} The human-readable string representation of the code size. + */ +export let CodeSizeToStringSize = (Memory) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Memory < 1024) { + return Memory + "B"; + } else if (Memory < 1024 * 1024) { + return (Memory / 1024).toFixed(2) + "KB"; + } else if (Memory < 1024 * 1024 * 1024) { + return (Memory / 1024 / 1024).toFixed(2) + "MB"; + } else { + return (Memory / 1024 / 1024 / 1024).toFixed(2) + "GB"; + } + } else { + return Memory; + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; diff --git a/src/utils/html.js b/src/utils/html.js new file mode 100644 index 00000000..c8db56aa --- /dev/null +++ b/src/utils/html.js @@ -0,0 +1,43 @@ +/** + * HTML utilities for escaping and purifying HTML content + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Escapes HTML special characters + * @param {string} str - The string to escape + * @returns {string} The escaped string + */ +export let escapeHTML = (str) => { + return str.replace(/[&<>"']/g, function (match) { + const escape = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return escape[match]; + }); +}; + +/** + * Purifies HTML content using DOMPurify + * @param {string} Input - The HTML content to purify + * @returns {string} The purified HTML content + */ +export let PurifyHTML = (Input) => { + try { + return DOMPurify.sanitize(Input, { + "ALLOWED_TAGS": ["a", "b", "big", "blockquote", "br", "code", "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "hr", "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "ul", "var"], + "ALLOWED_ATTR": ["abbr", "accept", "accept-charset", "accesskey", "action", "align", "alt", "axis", "border", "cellpadding", "cellspacing", "char", "charoff", "charset", "checked", "cite", "clear", "color", "cols", "colspan", "compact", "coords", "datetime", "dir", "disabled", "enctype", "for", "frame", "headers", "height", "href", "hreflang", "hspace", "ismap", "itemprop", "label", "lang", "longdesc", "maxlength", "media", "method", "multiple", "name", "nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev", "rows", "rowspan", "rules", "scope", "selected", "shape", "size", "span", "src", "start", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "vspace", "width"] + }); + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; diff --git a/src/utils/mathjax.js b/src/utils/mathjax.js new file mode 100644 index 00000000..e90aa65e --- /dev/null +++ b/src/utils/mathjax.js @@ -0,0 +1,30 @@ +/** + * MathJax rendering utilities + */ + +/** + * Render MathJax on the page + */ +export let RenderMathJax = async () => { + try { + if (document.getElementById("MathJax-script") === null) { + var ScriptElement = document.createElement("script"); + ScriptElement.id = "MathJax-script"; + ScriptElement.type = "text/javascript"; + ScriptElement.src = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.0.5/es5/tex-chtml.js"; + document.body.appendChild(ScriptElement); + await new Promise((Resolve) => { + ScriptElement.onload = () => { + Resolve(); + }; + }); + } + if (typeof MathJax !== 'undefined') { //If there is a Math expression + MathJax.startup.input[0].findTeX.options.inlineMath.push(["$", "$"]); + MathJax.startup.input[0].findTeX.getPatterns(); + MathJax.typeset(); + } + } catch (e) { + console.error(e); + } +}; diff --git a/src/utils/table.js b/src/utils/table.js new file mode 100644 index 00000000..c4970810 --- /dev/null +++ b/src/utils/table.js @@ -0,0 +1,24 @@ +/** + * Table utilities for styling and tidying up tables + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Tidies up the given table by applying Bootstrap styling and removing unnecessary attributes. + * + * @param {HTMLElement} Table - The table element to be tidied up. + */ +export let TidyTable = (Table) => { + try { + if (UtilityEnabled("NewBootstrap") && Table != null) { + Table.className = "table table-hover"; + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; diff --git a/src/utils/time.js b/src/utils/time.js new file mode 100644 index 00000000..18665024 --- /dev/null +++ b/src/utils/time.js @@ -0,0 +1,101 @@ +/** + * Time utilities for formatting and converting time values + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; + +/** + * Calculates the relative time based on the input date. + * @param {string|Date} Input - The input date. + * @returns {string} The relative time in a formatted string. + */ +export let GetRelativeTime = (Input) => { + try { + Input = new Date(parseInt(Input)); + let Now = new Date().getTime(); + let Delta = Now - Input.getTime(); + let RelativeName = ""; + if (Delta < 0) { + RelativeName = "未来"; + } else if (Delta <= 1000 * 60) { + RelativeName = "刚刚"; + } else if (Delta <= 1000 * 60 * 60) { + RelativeName = Math.floor((Now - Input) / 1000 / 60) + "分钟前"; + } else if (Delta <= 1000 * 60 * 60 * 24) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60) + "小时前"; + } else if (Delta <= 1000 * 60 * 60 * 24 * 31) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24) + "天前"; + } else if (Delta <= 1000 * 60 * 60 * 24 * 365) { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 31) + "个月前"; + } else { + RelativeName = Math.floor((Now - Input) / 1000 / 60 / 60 / 24 / 365) + "年前"; + } + return "" + RelativeName + ""; + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; + +/** + * Converts the given number of seconds to a formatted string representation of hours, minutes, and seconds. + * @param {number} InputSeconds - The number of seconds to convert. + * @returns {string} The formatted string representation of the input seconds. + */ +export let SecondsToString = (InputSeconds) => { + try { + let Hours = Math.floor(InputSeconds / 3600); + let Minutes = Math.floor((InputSeconds % 3600) / 60); + let Seconds = InputSeconds % 60; + return (Hours < 10 ? "0" : "") + Hours + ":" + (Minutes < 10 ? "0" : "") + Minutes + ":" + (Seconds < 10 ? "0" : "") + Seconds; + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; + +/** + * Converts a string in the format "hh:mm:ss" to the equivalent number of seconds. + * @param {string} InputString - The input string to convert. + * @returns {number} The number of seconds equivalent to the input string. + */ +export let StringToSeconds = (InputString) => { + try { + let SplittedString = InputString.split(":"); + return parseInt(SplittedString[0]) * 60 * 60 + parseInt(SplittedString[1]) * 60 + parseInt(SplittedString[2]); + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; + +/** + * Converts a time value to a string representation. + * @param {number} Time - The time value to convert. + * @returns {string|number} - The converted time value as a string, or the original value if UtilityEnabled("AddUnits") is false. + */ +export let TimeToStringTime = (Time) => { + try { + if (UtilityEnabled("AddUnits")) { + if (Time < 1000) { + return Time + "ms"; + } else if (Time < 1000 * 60) { + return (Time / 1000).toFixed(2) + "s"; + } + } else { + return Time; + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; diff --git a/src/utils/user.js b/src/utils/user.js new file mode 100644 index 00000000..4418d367 --- /dev/null +++ b/src/utils/user.js @@ -0,0 +1,172 @@ +/** + * User information utilities + */ + +import { UtilityEnabled } from '../core/config.js'; +import { SmartAlert } from './alerts.js'; +import { RequestAPI } from './api.js'; +import { AdminUserList } from '../core/constants.js'; + +/** + * Get user information + * @param {string} Username - The username + * @returns {Promise} User info object with Rating and EmailHash + */ +export let GetUserInfo = async (Username) => { + try { + if (localStorage.getItem("UserScript-User-" + Username + "-UserRating") != null && new Date().getTime() - parseInt(localStorage.getItem("UserScript-User-" + Username + "-LastUpdateTime")) < 1000 * 60 * 60 * 24) { + return { + "Rating": localStorage.getItem("UserScript-User-" + Username + "-UserRating"), + "EmailHash": localStorage.getItem("UserScript-User-" + Username + "-EmailHash") + } + } + return await fetch("https://www.xmoj.tech/userinfo.php?user=" + Username).then((Response) => { + return Response.text(); + }).then((Response) => { + if (Response.indexOf("No such User!") !== -1) { + return null; + } + const ParsedDocument = new DOMParser().parseFromString(Response, "text/html"); + let Rating = (parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(4) > td:nth-child(2)").innerText.trim()) / parseInt(ParsedDocument.querySelector("#statics > tbody > tr:nth-child(3) > td:nth-child(2)").innerText.trim())).toFixed(3) * 1000; + let Temp = ParsedDocument.querySelector("#statics > tbody").children; + let Email = Temp[Temp.length - 1].children[1].innerText.trim(); + let EmailHash = CryptoJS.MD5(Email).toString(); + localStorage.setItem("UserScript-User-" + Username + "-UserRating", Rating); + if (Email === "") { + EmailHash = undefined; + } else { + localStorage.setItem("UserScript-User-" + Username + "-EmailHash", EmailHash); + } + localStorage.setItem("UserScript-User-" + Username + "-LastUpdateTime", new Date().getTime()); + return { + "Rating": Rating, "EmailHash": EmailHash + } + }); + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; + +/** + * Retrieves the badge information for a given user. + * + * @param {string} Username - The username of the user. + * @returns {Promise} - A promise that resolves to an object containing the badge information. + * @property {string} BackgroundColor - The background color of the badge. + * @property {string} Color - The color of the badge. + * @property {string} Content - The content of the badge. + */ +export let GetUserBadge = async (Username) => { + try { + if (localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime") != null && new Date().getTime() - parseInt(localStorage.getItem("UserScript-User-" + Username + "-Badge-LastUpdateTime")) < 1000 * 60 * 60 * 24) { + return { + "BackgroundColor": localStorage.getItem("UserScript-User-" + Username + "-Badge-BackgroundColor"), + "Color": localStorage.getItem("UserScript-User-" + Username + "-Badge-Color"), + "Content": localStorage.getItem("UserScript-User-" + Username + "-Badge-Content") + } + } else { + let BackgroundColor = ""; + let Color = ""; + let Content = ""; + await new Promise((Resolve) => { + RequestAPI("GetBadge", { + "UserID": String(Username) + }, (Response) => { + if (Response.Success) { + BackgroundColor = Response.Data.BackgroundColor; + Color = Response.Data.Color; + Content = Response.Data.Content; + } + Resolve(); + }); + }); + localStorage.setItem("UserScript-User-" + Username + "-Badge-BackgroundColor", BackgroundColor); + localStorage.setItem("UserScript-User-" + Username + "-Badge-Color", Color); + localStorage.setItem("UserScript-User-" + Username + "-Badge-Content", Content); + localStorage.setItem("UserScript-User-" + Username + "-Badge-LastUpdateTime", String(new Date().getTime())); + return { + "BackgroundColor": BackgroundColor, "Color": Color, "Content": Content + } + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; + +/** + * Sets the HTML content of an element to display a username with optional additional information. + * @param {HTMLElement} Element - The element to set the HTML content. + * @param {string} Username - The username to display. + * @param {boolean} [Simple=false] - Indicates whether to display additional information or not. + * @param {string} [Href="https://www.xmoj.tech/userinfo.php?user="] - The URL to link the username to. + * @returns {Promise} - A promise that resolves when the HTML content is set. + */ +export let GetUsernameHTML = async (Element, Username, Simple = false, Href = "https://www.xmoj.tech/userinfo.php?user=") => { + try { + //Username = Username.replaceAll(/[^a-zA-Z0-9]/g, ""); + let ID = "Username-" + Username + "-" + Math.random(); + Element.id = ID; + Element.innerHTML = `
    `; + Element.appendChild(document.createTextNode(Username)); + let UserInfo = await GetUserInfo(Username); + if (UserInfo === null) { + document.getElementById(ID).innerHTML = ""; + document.getElementById(ID).appendChild(document.createTextNode(Username)); + return; + } + let HTMLData = ""; + if (!Simple) { + HTMLData += ``; + } + HTMLData += ` 500) { + HTMLData += "link-danger"; + } else if (Rating >= 400) { + HTMLData += "link-warning"; + } else if (Rating >= 300) { + HTMLData += "link-success"; + } else { + HTMLData += "link-info"; + } + } else { + HTMLData += "link-info"; + } + HTMLData += `">`; + if (!Simple) { + if (AdminUserList.includes(Username)) { + HTMLData += `脚本管理员`; + } + let BadgeInfo = await GetUserBadge(Username); + if (BadgeInfo.Content !== "") { + HTMLData += `${BadgeInfo.Content}`; + } + } + if (document.getElementById(ID) !== null) { + document.getElementById(ID).innerHTML = HTMLData; + document.getElementById(ID).getElementsByTagName("a")[0].appendChild(document.createTextNode(Username)); + } + } catch (e) { + console.error(e); + if (UtilityEnabled("DebugMode")) { + SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); + } + } +}; diff --git a/src/utils/version.js b/src/utils/version.js new file mode 100644 index 00000000..d86aa324 --- /dev/null +++ b/src/utils/version.js @@ -0,0 +1,26 @@ +/** + * Version comparison utilities + */ + +/** + * Compares two version strings + * @param {string} currVer - Current version + * @param {string} remoteVer - Remote version + * @returns {boolean} True if update is needed + */ +export function compareVersions(currVer, remoteVer) { + const currParts = currVer.split('.').map(Number); + const remoteParts = remoteVer.split('.').map(Number); + + const maxLen = Math.max(currParts.length, remoteParts.length); + for (let i = 0; i < maxLen; i++) { + const curr = currParts[i] !== undefined ? currParts[i] : 0; + const remote = remoteParts[i] !== undefined ? remoteParts[i] : 0; + if (remote > curr) { + return true; // update needed + } else if (remote < curr) { + return false; // no update needed + } + } + return false; // versions are equal +}