Codeforces Better!

一个适用于 Codeforces 的 Tampermonkey 脚本,增强功能与界面。

当前为 2024-03-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Codeforces Better!
  3. // @namespace https://greasyfork.org/users/747162
  4. // @version 1.74.0
  5. // @author 北极小狐
  6. // @match *://*.codeforces.com/*
  7. // @match *://*.codeforc.es/*
  8. // @run-at document-start
  9. // @connect www2.deepl.com
  10. // @connect api-free.deepl.com
  11. // @connect api.deepl.com
  12. // @connect api.deeplx.org
  13. // @connect www.iflyrec.com
  14. // @connect m.youdao.com
  15. // @connect api.interpreter.caiyunai.com
  16. // @connect translate.google.com
  17. // @connect openai.api2d.net
  18. // @connect api.openai.com
  19. // @connect www.luogu.com.cn
  20. // @connect vjudge.net
  21. // @connect clist.by
  22. // @connect greasyfork.org
  23. // @connect rextester.com
  24. // @connect wandbox.org
  25. // @connect staticfile.net
  26. // @connect aowuucdn.oss-cn-beijing.aliyuncs.com
  27. // @connect aowuucdn.oss-accelerate.aliyuncs.com
  28. // @connect 127.0.0.1
  29. // @connect *
  30. // @grant GM_xmlhttpRequest
  31. // @grant GM_info
  32. // @grant GM_setValue
  33. // @grant GM_getValue
  34. // @grant GM_listValues
  35. // @grant GM_deleteValue
  36. // @grant GM_addStyle
  37. // @grant GM_setClipboard
  38. // @grant GM_getResourceText
  39. // @icon https://aowuucdn.oss-accelerate.aliyuncs.com/codeforces.png
  40. // @require https://cdn.staticfile.net/turndown/7.1.2/turndown.min.js
  41. // @require https://cdn.staticfile.net/markdown-it/13.0.1/markdown-it.min.js
  42. // @require https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
  43. // @require https://cdn.staticfile.net/chroma-js/2.4.2/chroma.min.js
  44. // @require https://cdn.staticfile.net/xterm/3.9.2/xterm.min.js
  45. // @require https://cdn.staticfile.net/dexie/3.2.4/dexie.min.js
  46. // @require https://cdn.staticfile.net/i18next/23.5.1/i18next.min.js
  47. // @require https://cdn.staticfile.net/i18next-http-backend/2.2.2/i18nextHttpBackend.min.js
  48. // @require https://cdn.staticfile.net/jquery-i18next/1.2.1/jquery-i18next.min.js
  49. // @require https://cdn.staticfile.net/highlight.js/11.3.1/highlight.min.js
  50. // @require https://update.greasyfork.org/scripts/484742/1311040/i18nextChainedBackendjs.js
  51. // @require https://update.greasyfork.org/scripts/484743/1311041/i18next-localstorage-backendjs.js
  52. // @resource acwing_cpp_code_completer https://aowuucdn.oss-accelerate.aliyuncs.com/acwing_cpp_code_completer-0.0.11.json
  53. // @resource wandboxlist https://wandbox.org/api/list.json
  54. // @resource xtermcss https://cdn.staticfile.net/xterm/3.9.2/xterm.min.css
  55. // @resource selectpagecss https://aowuucdn.oss-accelerate.aliyuncs.com/css/selectpage.css
  56. // @license GPL3
  57. // @compatible Chrome
  58. // @compatible Firefox
  59. // @compatible Edge
  60. // @incompatible safari
  61. // @supportURL https://github.com/beijixiaohu/OJBetter/issues
  62. // @name:zh-TW Codeforces Better!
  63. // @name:en Codeforces Better!
  64. // @name:de Codeforces Better!
  65. // @name:fr Codeforces Better!
  66. // @name:ko Codeforces Better!
  67. // @name:pt Codeforces Better!
  68. // @name:ja Codeforces Better!
  69. // @name:es Codeforces Better!
  70. // @name:it Codeforces Better!
  71. // @name:hi Codeforces Better!
  72. // @description 一个适用于 Codeforces 的 Tampermonkey 脚本,增强功能与界面。
  73. // @description:zh-TW 一個適用於 Codeforces 的 Tampermonkey 腳本,增強功能與界面。
  74. // @description:en A Tampermonkey script for Codeforces that enhances functionality and interface.
  75. // @description:de Ein Tampermonkey-Skript für Codeforces, das Funktionalität und Benutzeroberfläche verbessert.
  76. // @description:fr Un script Tampermonkey pour Codeforces qui améliore les fonctionnalités et l'interface.
  77. // @description:ko Codeforces를 위한 Tampermonkey 스크립트로 기능과 인터페이스를 개선합니다.
  78. // @description:pt Um script Tampermonkey para Codeforces que aprimora a funcionalidade e a interface.
  79. // @description:ja Codeforces用のTampermonkeyスクリプトで機能とインターフェースを強化します。
  80. // @description:es Un script Tampermonkey para Codeforces que mejora la funcionalidad y la interfaz.
  81. // @description:it Uno script Tampermonkey per Codeforces che migliora la funzionalità e l'interfaccia.
  82. // @description:hi Codeforces के लिए एक Tampermonkey स्क्रिप्ट जो कार्यक्षमता और इंटरफ़ेस को बेहतर बनाता है।
  83. // ==/UserScript==
  84.  
  85. /**
  86. * @namespace OJBetter
  87. * @desc 主命名空间
  88. */
  89. const OJBetter = {};
  90.  
  91. /**
  92. * @namespace state
  93. * @desc 描述脚本的当前状态。
  94. * @memberof OJBetter
  95. */
  96. OJBetter.state = {
  97. /** @type {string} 脚本名*/
  98. name: GM_info.script.name,
  99. /** @type {string} 格式化后的脚本名*/
  100. formatName: undefined,
  101. /** @type {string} 版本号*/
  102. version: GM_info.script.version,
  103. /** @type {boolean?} 是否跳过页面加载等待 */
  104. notWaiteLoaded: undefined,
  105. /** @type {string} 最后公告版本,用于标识版本更新完成提示 */
  106. lastAnnounceVer: undefined,
  107. /** @type {string} 最后读取的有效公告版本 */
  108. lastReadAnnounceVer: undefined,
  109. /** @type {number} 当前已打开的模态对话框数量*/
  110. openDialogCount: 0
  111. };
  112.  
  113. /**
  114. * @namespace common
  115. * @desc 通用设置和属性。
  116. * @memberof OJBetter
  117. */
  118. OJBetter.common = {
  119. /** @type {string} 网站的主机地址 */
  120. hostAddress: location.origin,
  121. /** @type {string?} Codeforces的CSRF令牌 */
  122. cf_csrf_token: undefined,
  123. /** @type {Array?} 任务队列 */
  124. taskQueue: undefined,
  125. /** @type {object} OJBetter数据库连接实例*/
  126. database: undefined,
  127. /** @type {object} turndownService实例*/
  128. turndownService: undefined,
  129. };
  130.  
  131. /**
  132. * @namespace basic
  133. * @desc 基本的用户界面设置。
  134. * @memberof OJBetter
  135. */
  136. OJBetter.basic = {
  137. /** @type {string} 黑暗模式设置 */
  138. darkMode: undefined,
  139. /** @type {boolean?} 是否展开折叠块 */
  140. expandFoldingblocks: undefined,
  141. /** @type {boolean?} 是否开启折叠块渲染性能优化 */
  142. renderPerfOpt: undefined,
  143. /** @type {boolean?} 是否开启下拉选择框性能优化 */
  144. selectElementPerfOpt: undefined,
  145. /** @type {boolean?} 评论区分页 */
  146. commentPaging: undefined,
  147. /** @type {boolean?} 显示跳转到Luogu按钮 */
  148. showJumpToLuogu: undefined,
  149. /** @type {boolean?} 显示跳转到Virtual Judge按钮 */
  150. showCF2vjudge: undefined,
  151. /** @type {boolean?} 比赛排行榜重新着色 */
  152. standingsRecolor: undefined
  153. };
  154.  
  155. /**
  156. * @namespace typeOfPage
  157. * @desc 页面类型判断。
  158. * @memberof OJBetter
  159. */
  160. OJBetter.typeOfPage = {
  161. /** @type {boolean?} 是否是轻量站 */
  162. is_mSite: undefined,
  163. /** @type {boolean?} 是否是acmsguru页面 */
  164. is_acmsguru: undefined,
  165. /** @type {boolean?} 是否是旧版LaTeX页面 */
  166. is_oldLatex: undefined,
  167. /** @type {boolean?} 是否是题目集页面 */
  168. is_contest: undefined,
  169. /** @type {boolean?} 是否是题目页面 */
  170. is_problem: undefined,
  171. /** @type {boolean?} 是否是完整的问题集页面 */
  172. is_completeProblemset: undefined,
  173. /** @type {boolean?} 是否是问题集中的问题页面 */
  174. is_problemset_problem: undefined,
  175. /** @type {boolean?} 是否是问题集页面 */
  176. is_problemset: undefined,
  177. /** @type {boolean?} 是否是Codeforces排名页面 */
  178. is_cfStandings: undefined,
  179. /** @type {boolean?} 是否是提交页面 */
  180. is_submitPage: undefined,
  181. /** @type {boolean?} 是否是代码状态页面 */
  182. is_statePage: undefined,
  183. /** @type {boolean?} 是否是提交记录页面 */
  184. is_submissions: undefined,
  185. };
  186.  
  187. /**
  188. * @namespace localization
  189. * @desc 本地化设置。
  190. * @memberof OJBetter
  191. */
  192. OJBetter.localization = {
  193. /** @type {string?} 网站语言 */
  194. websiteLang: undefined,
  195. /** @type {string?} 脚本语言 */
  196. scriptLang: undefined
  197. };
  198.  
  199. /**
  200. * @namespace translation
  201. * @desc 翻译设置。
  202. * @memberof OJBetter
  203. */
  204. OJBetter.translation = {
  205. /** @type {string?} 翻译服务选择 */
  206. choice: undefined,
  207. /** @type {string?} 目标语言 */
  208. targetLang: undefined,
  209. comment: {
  210. /** @type {string?} 评论翻译服务选择 */
  211. choice: undefined,
  212. /** @type {string?} 评论翻译模式 */
  213. transMode: undefined
  214. },
  215. auto: {
  216. /** @type {boolean?} 自动翻译开关 */
  217. enabled: undefined,
  218. /** @type {number?} 短文本长度限制 */
  219. shortTextLength: undefined,
  220. mixTrans: {
  221. /** @type {boolean?} 混合翻译开关 */
  222. enabled: undefined,
  223. /** @type {Array?} 混合翻译服务列表 */
  224. servers: undefined
  225. }
  226. },
  227. memory: {
  228. /** @type {boolean?} 翻译记忆开关 */
  229. enabled: undefined,
  230. /** @type {Object?} 翻译记忆树 */
  231. ttTree: undefined
  232. },
  233. /** @type {string?} 重翻译时的行为 */
  234. retransAction: undefined,
  235. /** @type {number?} 等待时间 */
  236. waitTime: undefined,
  237. /** @type {boolean?} 替换符 */
  238. replaceSymbol: undefined,
  239. /** @type {boolean?} 过滤文本中的*号 */
  240. filterTextWithoutEmphasis: undefined
  241. };
  242.  
  243. /**
  244. * @namespace clist
  245. * @desc Clist相关设置。
  246. * @memberof OJBetter
  247. */
  248. OJBetter.clist = {
  249. enabled: {
  250. /** @type {boolean?} 比赛页面开关 */
  251. contest: undefined,
  252. /** @type {boolean?} 问题页面开关 */
  253. problem: undefined,
  254. /** @type {boolean?} 问题集页面开关 */
  255. problemset: undefined
  256. },
  257. /** @type {boolean?} Rating数据防剧透 */
  258. ratingHidden: undefined,
  259. /** @type {string?} Clist key */
  260. authorization: undefined
  261. };
  262.  
  263. /**
  264. * @namespace monaco
  265. * @desc Monaco编辑器配置。
  266. * @memberof OJBetter
  267. */
  268. OJBetter.monaco = {
  269. /** @type {boolean?} 在问题页面上启用Monaco编辑器 */
  270. enableOnProblemPage: undefined,
  271. /** @type {boolean?} 美化pre代码块 */
  272. beautifyPreBlocks: undefined,
  273. /** @type {boolean} Monaco编辑器加载完成标志 */
  274. loaderOnload: false,
  275. lsp: {
  276. /** @type {Array?} LSP套接字数组 */
  277. socket: [],
  278. /** @type {boolean?} 是否启用LSP */
  279. enabled: undefined,
  280. /** @type {string?} 工作路径 */
  281. workUri: undefined,
  282. /** @type {string?} 套接字URL */
  283. socketUrl: undefined
  284. },
  285. complet: {
  286. /** @type {boolean?} 是否启用C++代码补全模板 */
  287. cppCodeTemplate: undefined,
  288. /** @type {Object?} 自定义配置 */
  289. customConfig: undefined
  290. },
  291. /** @type {Object?} Monaco编辑器实例 */
  292. editor: null,
  293. /** @type {string?} 在线编译器选择 */
  294. onlineCompilerChoice: undefined,
  295. /** @type {string?} 记忆编译器语言选择 */
  296. compilerSelection: undefined,
  297. /** @type {string?} 当前选择的语言 */
  298. nowLangSelect: undefined,
  299. setting: {
  300. /** @type {Array?} 语言设置数组 */
  301. language: [],
  302. /** @type {string?} 位置 */
  303. position: undefined,
  304. /** @type {boolean} 位置初始化标志 */
  305. position_initialized: false,
  306. /** @type {number?} 字体大小 */
  307. fontsize: undefined,
  308. /** @type {boolean?} 鼠标滚动锁定 */
  309. alwaysConsumeMouseWheel: undefined,
  310. /** @type {boolean?} 提交代码二次确认 */
  311. isCodeSubmitDoubleConfirm: undefined,
  312. /** @type {string?} 提交按钮位置 */
  313. submitButtonPosition: undefined
  314. }
  315. };
  316.  
  317. /**
  318. * @namespace deepl
  319. * @desc DeepL翻译服务配置。
  320. * @memberof OJBetter
  321. */
  322. OJBetter.deepl = {
  323. /** @type {Object?} DeepL配置对象 */
  324. configs: undefined,
  325. config: {
  326. /** @type {string?} 类型 */
  327. type: undefined,
  328. /** @type {string?} 名称 */
  329. name: undefined,
  330. /** @type {string?} API类型 */
  331. apiGenre: undefined,
  332. /** @type {string?} API密钥 */
  333. key: undefined,
  334. /** @type {string?} 代理 */
  335. proxy: undefined,
  336. /** @type {Object?} 额外请求头 */
  337. header: undefined,
  338. /** @type {Object?} 额外请求数据 */
  339. data: undefined,
  340. quota: {
  341. /** @type {string?} 余额URL */
  342. url: undefined,
  343. /** @type {string?} 余额请求方法 */
  344. method: undefined,
  345. /** @type {Object?} 余额请求头 */
  346. header: undefined,
  347. /** @type {Object?} 余额请求数据 */
  348. data: undefined,
  349. /** @type {number?} 剩余配额 */
  350. surplus: undefined
  351. }
  352. },
  353. /** @type {boolean?} 启用重点保护 */
  354. enableEmphasisProtection: undefined,
  355. /** @type {boolean?} 启用链接保护 */
  356. enableLinkProtection: undefined
  357. };
  358.  
  359. /**
  360. * @namespace chatgpt
  361. * @desc ChatGPT服务配置。
  362. * @memberof OJBetter
  363. */
  364. OJBetter.chatgpt = {
  365. /** @type {Object?} ChatGPT配置对象 */
  366. configs: undefined,
  367. config: {
  368. /** @type {string?} 名称 */
  369. name: undefined,
  370. /** @type {string?} 模型 */
  371. model: undefined,
  372. /** @type {string?} API密钥 */
  373. key: undefined,
  374. /** @type {string?} 代理 */
  375. proxy: undefined,
  376. /** @type {Object?} 额外请求头 */
  377. header: undefined,
  378. /** @type {Object?} 额外请求数据 */
  379. data: undefined,
  380. quota: {
  381. /** @type {string?} 余额URL */
  382. url: undefined,
  383. /** @type {string?} 余额请求方法 */
  384. method: undefined,
  385. /** @type {Object?} 余额请求头 */
  386. header: undefined,
  387. /** @type {Object?} 余额请求数据 */
  388. data: undefined,
  389. /** @type {number?} 剩余配额 */
  390. surplus: undefined
  391. }
  392. },
  393. /** @type {boolean?} 是否为流式传输 */
  394. isStream: undefined
  395. };
  396.  
  397. /**
  398. * @namespace preference
  399. * @desc 偏好设置
  400. * @memberof OJBetter
  401. */
  402. OJBetter.preference = {
  403. /** @type {boolean?} 是否显示加载动画 */
  404. showLoading: undefined,
  405. /** @type {boolean?} 是否显示悬停目标区域 */
  406. hoverTargetAreaDisplay: undefined,
  407. /** @type {string?} 按钮图标大小 */
  408. iconButtonSize: undefined,
  409. };
  410.  
  411. /**
  412. * @namespace dev
  413. * @desc 维护
  414. * @memberof OJBetter
  415. */
  416. OJBetter.dev = {
  417. /** @type {boolean?} 是否显示规则标记 */
  418. isRuleMarkingEnabled: undefined,
  419. };
  420.  
  421. /**
  422. * @namespace about
  423. * @desc 关于页信息
  424. * @memberof OJBetter
  425. */
  426. OJBetter.about = {
  427. /** @type {string?} 更新通道 */
  428. updateChannel: undefined,
  429. /** @type {string?} 更新源 */
  430. updateSource: undefined
  431. };
  432.  
  433. /**
  434. * @namespace supportList
  435. * @desc 支持列表
  436. * @memberof OJBetter
  437. */
  438. OJBetter.supportList = {
  439. /** @type {object} 翻译支持列表和对应语言代码*/
  440. translationSupport: {
  441. 'deepl': { 'zh': 'ZH', 'de': 'DE', 'fr': 'FR', 'ko': 'KO', 'pt': 'PT', 'ja': 'JA', 'es': 'ES', 'it': 'IT' },
  442. 'iflyrec': { 'zh': '1' },
  443. 'youdao': { 'zh': 'AUTO' },
  444. 'google': { 'zh': 'zh-CN', 'zh-Hant': 'zh-TW', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
  445. 'caiyun': { 'zh': 'auto2zh', 'ja': 'auto2ja', 'ko': 'auto2ko', 'es': 'auto2es', 'fr': 'auto2fr' },
  446. 'openai': { 'zh': 'Chinese', 'zh-Hant': 'Traditional Chinese', 'de': 'German', 'fr': 'French', 'ko': 'Korean', 'pt': 'Portuguese', 'ja': 'Japanese', 'es': 'Spanish', 'it': 'Italian', 'hi': 'Hindi' }
  447. },
  448. /** @type {object} 更新源支持列表*/
  449. updateSourceSupportList: {
  450. 'greasyfork': {
  451. 'release': true,
  452. 'dev': false
  453. },
  454. 'github': {
  455. 'release': true,
  456. 'dev': true
  457. },
  458. 'aliyunoss': {
  459. 'release': true,
  460. 'dev': true
  461. }
  462. }
  463. }
  464.  
  465. // ------------------------------
  466. // 一些工具函数
  467. // ------------------------------
  468.  
  469. /**
  470. * 延迟函数
  471. * @param {number} ms 延迟时间(毫秒)
  472. * @returns {Promise<void>}
  473. */
  474. function OJB_delay(ms) {
  475. return new Promise(resolve => setTimeout(resolve, ms));
  476. }
  477.  
  478. /**
  479. * 等待直到指定的条件函数返回true。
  480. *
  481. * @param {() => boolean} conditionCheck 一个无参数的函数,用于检查条件是否满足。当函数返回true时,表示条件已满足。
  482. * @param {number} [interval=100] 检查条件的间隔时间,单位为毫秒。默认为100毫秒。
  483. * @returns {Promise<void>} 返回一个Promise,在条件满足时解决。
  484. */
  485. async function OJB_waitUntilTrue(conditionCheck, interval = 100) {
  486. return new Promise((resolve) => {
  487. const checkCondition = async () => {
  488. if (conditionCheck()) {
  489. resolve();
  490. } else {
  491. await OJB_delay(interval);
  492. checkCondition();
  493. }
  494. };
  495. checkCondition();
  496. });
  497. }
  498.  
  499. /**
  500. * 动态加载JavaScript库并返回一个Promise,该Promise在脚本加载完成后解决。
  501. *
  502. * @param {string} url - 要加载的JavaScript库的URL地址。
  503. * @returns {Promise<void>} 一个Promise,它在脚本加载并执行完成后解决。
  504. */
  505. function OJB_LoadJS(url) {
  506. return new Promise((resolve, reject) => {
  507. let scriptElement = document.createElement("script");
  508. scriptElement.src = url;
  509. document.head.prepend(scriptElement);
  510. scriptElement.onload = resolve;
  511. scriptElement.onerror = reject;
  512. });
  513. }
  514.  
  515. /**
  516. * 安全地创建JQuery对象
  517. * @description 通过字符串创建JQuery对象时,如果字符串以空格开头,在某些Jquery版本中会发生错误,过滤空格以安全的创建元素。
  518. * @param {string} string - 字符串。
  519. * @returns JQuery对象
  520. */
  521. const OJB_safeCreateJQElement = function (string) {
  522. return $(string.replace(/^\s+/, ""));
  523. }
  524.  
  525. /**
  526. * 将数字或者字符串解析为数字。
  527. * @memberof OJBetter.common
  528. * @param {string} val 要解析的字符串
  529. * @param {boolean} [strict=false] 是否进行严格类型检查
  530. * @returns {number} 解析结果
  531. * @throws {Error} 如果解析失败,则抛出错误
  532. */
  533. const OJB_parseNumber = (val, strict = false) => {
  534. const num = Number(val);
  535. if (isNaN(num) || (strict && val.toString() !== num.toString())) {
  536. throw new Error('Invalid number');
  537. }
  538. return num;
  539. };
  540.  
  541. /**
  542. * 将字符串解析为布尔值
  543. * @param {string} val - 要解析的字符串
  544. * @param {boolean} strict - 是否进行严格类型检查
  545. * @returns {boolean} - 解析结果
  546. * @throws {Error} - 如果解析失败,则抛出错误
  547. */
  548. const OJB_parseBoolean = (val, strict) => {
  549. if (strict) {
  550. if (val === true || val === false) return val;
  551. throw new Error('Invalid boolean');
  552. }
  553. return val === 'true' ? true : val === 'false' ? false : val;
  554. };
  555.  
  556. /**
  557. * 将字符串解析为对象
  558. * @param {string} val - 要解析的字符串
  559. * @returns {Object} - 解析结果
  560. * @throws {Error} - 如果解析失败,则抛出错误
  561. */
  562. const OJB_parseObject = val => {
  563. try {
  564. return JSON.parse(val);
  565. } catch {
  566. throw new Error('Invalid JSON');
  567. }
  568. };
  569.  
  570. /**
  571. * 将字符串解析为键值对数组
  572. * @param {string} val - 要解析的字符串
  573. * @returns {Object[]} - 解析结果
  574. * @throws {Error} - 如果解析失败,则抛出错误
  575. */
  576. const OJB_parseLinePairArray = val => {
  577. if (typeof val !== 'string' || val.trim() === '') return [];
  578. return val.split("\n").filter(line => line.trim() !== '').map(line => {
  579. const indexOfFirstColon = line.indexOf(":");
  580. if (indexOfFirstColon === -1) throw new Error('Invalid LinePairArray format: ":" is missing');
  581. const key = line.substring(0, indexOfFirstColon).trim();
  582. const value = line.substring(indexOfFirstColon + 1).trim();
  583. return { [key]: value };
  584. });
  585. };
  586.  
  587. /**
  588. * 移除文本中的HTML标签
  589. * @param {string} text - 包含HTML标签的文本
  590. * @returns {string} - 移除HTML标签后的文本
  591. */
  592. const OJB_removeHTMLTags = function (text) {
  593. return text.replace(/<\/?[a-zA-Z]+("[^"]*"|'[^']*'|[^'">])*>/g, '');
  594. }
  595.  
  596. /**
  597. * 获取对象中指定路径表达式的值
  598. * @param {Object} obj - 要计算的对象
  599. * @param {string} pathOrExpression - 要计算的路径表达式
  600. * @returns {any} - 计算结果
  601. * @example
  602. * const obj = {
  603. * "a": {
  604. * "b": 1
  605. * },
  606. * "c": 2
  607. * };
  608. * OJB_evaluatePathOrExpression(obj, "a.b"); // 1
  609. * OJB_evaluatePathOrExpression(obj, "a.b + c"); // 3
  610. * OJB_evaluatePathOrExpression(obj, "a.b + a.c"); // 1
  611. */
  612. function OJB_evaluatePathOrExpression(obj, pathOrExpression) {
  613. const hasOperator = /[\+\-\*\/]/.test(pathOrExpression);
  614. const getPathValue = (obj, path) => {
  615. return path.split('.').reduce((acc, part) => {
  616. return acc !== undefined && acc !== null && acc.hasOwnProperty(part) ? acc[part] : undefined;
  617. }, obj);
  618. };
  619. const evaluateExpression = (obj, expression) => {
  620. const tokens = expression.split(/([\+\-\*\/])/).map(token => token.trim());
  621. const values = tokens.map(token => {
  622. if (/[\+\-\*\/]/.test(token)) {
  623. return token;
  624. } else {
  625. const value = getPathValue(obj, token);
  626. return value !== undefined ? value : 0;
  627. }
  628. });
  629. const evaluatedExpression = values.join(' ');
  630. try {
  631. return Function(`'use strict'; return (${evaluatedExpression});`)();
  632. } catch (e) {
  633. console.error('Expression evaluation error:', e);
  634. return undefined;
  635. }
  636. };
  637. return hasOperator ? evaluateExpression(obj, pathOrExpression) : getPathValue(obj, pathOrExpression);
  638. }
  639.  
  640. /**
  641. * 获取 GM 存储的值并根据类型进行处理
  642. * @param {string} key - 要检索的值的键。
  643. * @param {any} defaultValue - 如果值未找到,则返回的默认值。
  644. * @param {Object} [options={}] - 配置选项对象。
  645. * @param {string} [options.type='string'] - 期望的值的类型。可选值:'string', 'number', 'boolean', 'object', 'array', 'linePairArray'。
  646. * @param {boolean} [options.strict=false] - 用于数字和布尔类型,表示是否进行严格类型检查。
  647. * @param {string} [options.pathOrExpression=''] - 用于对象或数组类型,表示路径表达式或获取元素的索引。
  648. * @returns {any} - 检索到的值。
  649. */
  650. const OJB_getGMValue = (key, defaultValue, { type = 'string', strict = false, pathOrExpression = '' } = {}) => {
  651. let value = GM_getValue(key);
  652. if (value === undefined || value === null || value === "") {
  653. GM_setValue?.(key, defaultValue);
  654. return defaultValue;
  655. }
  656.  
  657. const parsers = {
  658. string: val => val,
  659. number: (val) => OJB_parseNumber(val, strict),
  660. boolean: (val) => OJB_parseBoolean(val, strict),
  661. object: OJB_parseObject,
  662. array: OJB_parseObject,
  663. linePairArray: OJB_parseLinePairArray
  664. };
  665.  
  666. if (!(type in parsers)) {
  667. console.error(`Unsupported type: ${type}`);
  668. return defaultValue;
  669. }
  670.  
  671. try {
  672. value = parsers[type](value);
  673. } catch (e) {
  674. console.error('Error:', e.message);
  675. return defaultValue;
  676. }
  677.  
  678. // The pathOrExpression processing is not applicable to linePairArray type
  679. if ((type === 'object' || type === 'array') && pathOrExpression) {
  680. const evaluated = OJB_evaluatePathOrExpression(value, pathOrExpression);
  681. if (evaluated === undefined) {
  682. console.error('Path or expression evaluation returned undefined');
  683. return defaultValue;
  684. }
  685. value = evaluated;
  686. }
  687.  
  688. return value;
  689. };
  690.  
  691. /**
  692. * 版本号比较方法
  693. * @param {string} version1 版本号1
  694. * @param {string} version2 版本号2
  695. * @returns {number} -1: version1 < version2, 0: version1 = version2, 1: version1 > version2
  696. */
  697. const OJB_compareVersions = function (version1 = "0", version2 = "0") {
  698. const v1Array = version1.split(".").map(Number);
  699. const v2Array = version2.split(".").map(Number);
  700. const length = Math.max(v1Array.length, v2Array.length);
  701. for (let i = 0; i < length; i++) {
  702. const diff = (v1Array[i] || 0) - (v2Array[i] || 0);
  703. if (diff) return Math.sign(diff);
  704. }
  705. return 0;
  706. }
  707.  
  708. /**
  709. * 获取上一个主版本号
  710. * @param {string} currentVersion 当前版本号
  711. * @returns {string} 上一个主版本号
  712. */
  713. const OJB_getPreviousVersion = function (currentVersion) {
  714. const versionArray = currentVersion.split(".").map(Number);
  715. let lastNonZeroIndex = versionArray.length - 1;
  716. while (lastNonZeroIndex >= 0 && versionArray[lastNonZeroIndex] === 0) {
  717. lastNonZeroIndex--;
  718. }
  719. if (lastNonZeroIndex >= 0) {
  720. versionArray[lastNonZeroIndex]--;
  721. for (let i = lastNonZeroIndex + 1; i < versionArray.length; i++) {
  722. versionArray[i] = 0;
  723. }
  724. }
  725. return versionArray.join(".");
  726. };
  727.  
  728. /**
  729. * 在指定根节点下观察指定选择器的元素,当元素存在时,执行回调函数
  730. * @param {Object} options - 配置对象
  731. * @param {string} options.selector - CSS选择器文本
  732. * @param {Function} options.callback - 回调函数,接收变动的节点作为参数
  733. * @param {Boolean} [options.triggerOnExist=true] - 如果为true,元素已存在时立即触发一次回调
  734. * @param {Element} [options.root=document.body] - 在哪个根节点下监听变化
  735. * @param {Boolean} [options.subtree=false] - 是否监听子树变化(即非直接子元素)
  736. */
  737. function OJB_observeElement({
  738. selector,
  739. callback,
  740. triggerOnExist = true,
  741. root = document.body,
  742. subtree = false
  743. }) {
  744. // 尝试获取选择器指定的元素
  745. const targetNode = root.querySelector(selector);
  746.  
  747. if (targetNode) {
  748. // 如果元素已存在,直接开始观察
  749. observeAndReport(targetNode, callback);
  750. // 如果triggerOnExist为true,则立即触发一次回调
  751. if (triggerOnExist) {
  752. callback(targetNode);
  753. }
  754. } else {
  755. // 如果元素不存在,监听DOM变化直到该元素被添加
  756. const observer = new MutationObserver((mutations) => {
  757. mutations.forEach((mutation) => {
  758. mutation.addedNodes.forEach((node) => {
  759. if (node.nodeType === Node.ELEMENT_NODE && node.matches(selector)) {
  760. observeAndReport(node, callback);
  761. if (triggerOnExist) {
  762. callback(node);
  763. }
  764. observer.disconnect(); // 停止监听
  765. }
  766. });
  767. });
  768. });
  769.  
  770. observer.observe(root, { childList: true, subtree, attributes: false });
  771. }
  772.  
  773. function observeAndReport(node, callback) {
  774. const childObserver = new MutationObserver((mutations) => {
  775. mutations.forEach((mutation) => {
  776. mutation.addedNodes.forEach((addedNode) => {
  777. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  778. callback(addedNode); // 执行回调函数
  779. }
  780. });
  781. });
  782. });
  783.  
  784. childObserver.observe(node, { childList: true, subtree: true, attributes: false });
  785. }
  786. }
  787.  
  788. /**
  789. * 初始化全局变量
  790. */
  791. async function initVar() {
  792. const { hostname, href } = window.location;
  793. OJBetter.state.formatName = (() => OJBetter.state.name
  794. .toLowerCase()
  795. .replace(/\s+/g, '-')
  796. .replace(/[^a-z0-9-]/g, ''))();
  797. OJBetter.state.lastAnnounceVer = OJB_getGMValue("lastAnnounceVer", "0");
  798. OJBetter.state.lastReadAnnounceVer = OJB_getGMValue("lastReadAnnounceVer", "0");
  799. OJBetter.typeOfPage.is_mSite = /^m[0-9]/.test(hostname);
  800. OJBetter.typeOfPage.is_oldLatex = $('.tex-span').length;
  801. OJBetter.typeOfPage.is_acmsguru = href.includes("acmsguru") && href.includes('/problem/');
  802. OJBetter.typeOfPage.is_contest = /\/contest\/[\d\/\s]+$/.test(href) && !href.includes('/problem/');
  803. OJBetter.typeOfPage.is_problem = href.includes('/problem/');
  804. OJBetter.typeOfPage.is_completeProblemset = /problems\/?$/.test(href);
  805. OJBetter.typeOfPage.is_problemset_problem = href.includes('/problemset/') && href.includes('/problem/');
  806. OJBetter.typeOfPage.is_problemset = href.includes('/problemset') && !href.includes('/problem/');
  807. OJBetter.typeOfPage.is_submitPage = href.includes('/submit');
  808. OJBetter.typeOfPage.is_statePage = href.includes('/status');
  809. OJBetter.typeOfPage.is_submissions = href.includes('/submissions');
  810. OJBetter.typeOfPage.is_cfStandings = href.includes('/standings') &&
  811. $('.standings tr:first th:nth-child(n+5)')
  812. .map(function () {
  813. return $(this).find('span').text();
  814. })
  815. .get()
  816. .every(score => /^[0-9]+$/.test(score));
  817. OJBetter.localization.websiteLang = OJB_getGMValue("localizationLanguage", "zh");
  818. OJBetter.localization.scriptLang = OJB_getGMValue("scriptL10nLanguage", "zh");
  819. OJBetter.basic.renderPerfOpt = OJB_getGMValue("renderPerfOpt", false);
  820. OJBetter.basic.selectElementPerfOpt = OJB_getGMValue("selectElementPerfOpt", false);
  821. OJBetter.basic.commentPaging = OJB_getGMValue("commentPaging", true);
  822. OJBetter.basic.showJumpToLuogu = OJB_getGMValue("showJumpToLuogu", true);
  823. OJBetter.basic.showCF2vjudge = OJB_getGMValue("showCF2vjudge", true);
  824. OJBetter.basic.standingsRecolor = OJB_getGMValue("standingsRecolor", true);
  825. OJBetter.state.notWaiteLoaded = OJB_getGMValue("notWaiteLoaded", false);
  826. OJBetter.translation.targetLang = OJB_getGMValue("transTargetLang", "zh");
  827. OJBetter.translation.choice = OJB_getGMValue("translation", "deepl");
  828. OJBetter.translation.comment.transMode = OJB_getGMValue("commentTranslationMode", "0");
  829. OJBetter.translation.comment.choice = OJB_getGMValue("commentTranslationChoice", "0");
  830. OJBetter.translation.memory.enabled = OJB_getGMValue("memoryTranslateHistory", true);
  831. OJBetter.translation.auto.enabled = OJB_getGMValue("autoTranslation", false);
  832. OJBetter.translation.auto.shortTextLength = OJB_getGMValue("shortTextLength", "2000");
  833. OJBetter.translation.retransAction = OJB_getGMValue("retransAction", "0");
  834. OJBetter.translation.waitTime = OJB_getGMValue("transWaitTime", "200");
  835. OJBetter.translation.auto.mixTrans.enabled = OJB_getGMValue("allowMixTrans", true);
  836. OJBetter.translation.auto.mixTrans.servers = OJB_getGMValue("mixedTranslation", ['deepl', 'iflyrec', 'youdao', 'caiyun']);
  837. OJBetter.common.taskQueue = new TaskQueue();
  838. OJBetter.translation.replaceSymbol = OJB_getGMValue("replaceSymbol", "2");
  839. OJBetter.translation.filterTextWithoutEmphasis = OJB_getGMValue("filterTextWithoutEmphasis", false);
  840. OJBetter.clist.enabled.contest = OJB_getGMValue("showClistRating_contest", false);
  841. OJBetter.clist.enabled.problem = OJB_getGMValue("showClistRating_problem", false);
  842. OJBetter.clist.enabled.problemset = OJB_getGMValue("showClistRating_problemset", false);
  843. OJBetter.clist.ratingHidden = OJB_getGMValue("RatingHidden", false);
  844. OJBetter.clist.authorization = OJB_getGMValue("clist_Authorization", "");
  845. //deepl
  846. OJBetter.deepl.config.type = OJB_getGMValue("deepl_type", "free");
  847. OJBetter.deepl.configs = OJB_getGMValue("deepl_config", {
  848. "choice": "",
  849. "configurations": []
  850. });
  851. if (OJBetter.deepl.configs.choice !== "" && OJBetter.deepl.configs.configurations.length !== 0) {
  852. const choice = OJBetter.deepl.configs.choice;
  853. const configuration = OJBetter.deepl.configs.configurations.find(obj => obj.name === choice);;
  854. if (configuration == undefined) {
  855. let existingConfig = GM_getValue('deepl_config');
  856. existingConfig.choice = "";
  857. GM_setValue('deepl_config', existingConfig);
  858. location.reload();
  859. }
  860. OJBetter.deepl.config.name = configuration.name;
  861. OJBetter.deepl.config.apiGenre = configuration.apiGenre;
  862. OJBetter.deepl.config.key = configuration.key;
  863. OJBetter.deepl.config.proxy = configuration.proxy;
  864. OJBetter.deepl.config.header = OJB_parseLinePairArray(configuration._header);
  865. OJBetter.deepl.config.data = OJB_parseLinePairArray(configuration._data);
  866. OJBetter.deepl.config.quota.url = configuration.quota_url;
  867. OJBetter.deepl.config.quota.method = configuration.quota_method;
  868. OJBetter.deepl.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
  869. OJBetter.deepl.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
  870. OJBetter.deepl.config.quota.surplus = configuration.quota_surplus;
  871. }
  872. OJBetter.deepl.enableEmphasisProtection = OJB_getGMValue("enableEmphasisProtection", true);
  873. OJBetter.deepl.enableLinkProtection = OJB_getGMValue("enableLinkProtection", true);
  874. //openai
  875. OJBetter.chatgpt.isStream = OJB_getGMValue("openai_isStream", true);
  876. OJBetter.chatgpt.configs = OJB_getGMValue("chatgpt_config", {
  877. "choice": "",
  878. "configurations": []
  879. });
  880. if (OJBetter.chatgpt.configs.choice !== "" && OJBetter.chatgpt.configs.configurations.length !== 0) {
  881. const choice = OJBetter.chatgpt.configs.choice;
  882. const configuration = OJBetter.chatgpt.configs.configurations.find(obj => obj.name === choice);;
  883. if (configuration == undefined) {
  884. let existingConfig = GM_getValue('chatgpt_config');
  885. existingConfig.choice = "";
  886. GM_setValue('chatgpt_config', existingConfig);
  887. location.reload();
  888. }
  889. OJBetter.chatgpt.config.name = configuration.name;
  890. OJBetter.chatgpt.config.model = configuration.model;
  891. OJBetter.chatgpt.config.key = configuration.key;
  892. OJBetter.chatgpt.config.proxy = configuration.proxy;
  893. OJBetter.chatgpt.config.header = OJB_parseLinePairArray(configuration._header);
  894. OJBetter.chatgpt.config.data = OJB_parseLinePairArray(configuration._data);
  895. OJBetter.chatgpt.config.quota.url = configuration.quota_url;
  896. OJBetter.chatgpt.config.quota.method = configuration.quota_method;
  897. OJBetter.chatgpt.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
  898. OJBetter.chatgpt.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
  899. OJBetter.chatgpt.config.quota.surplus = configuration.quota_surplus;
  900. }
  901. // 编辑器
  902. if (!OJBetter.typeOfPage.is_mSite) OJBetter.common.cf_csrf_token = Codeforces.getCsrfToken();
  903. else OJBetter.common.cf_csrf_token = "";
  904. OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "61");
  905. OJBetter.monaco.setting.fontsize = OJB_getGMValue("editorFontSize", "15");
  906. OJBetter.monaco.enableOnProblemPage = OJB_getGMValue("problemPageCodeEditor", true);
  907. OJBetter.monaco.beautifyPreBlocks = OJB_getGMValue("beautifyPreBlocks", true);
  908. OJBetter.monaco.complet.cppCodeTemplate = OJB_getGMValue("cppCodeTemplateComplete", true);
  909. OJBetter.monaco.onlineCompilerChoice = OJB_getGMValue("onlineCompilerChoice", "official");
  910. OJBetter.monaco.setting.isCodeSubmitDoubleConfirm = OJB_getGMValue("isCodeSubmitConfirm", true);
  911. OJBetter.monaco.setting.alwaysConsumeMouseWheel = OJB_getGMValue("alwaysConsumeMouseWheel", true);
  912. OJBetter.monaco.setting.submitButtonPosition = OJB_getGMValue("submitButtonPosition", "bottom");
  913. //自定义补全
  914. OJBetter.monaco.complet.customConfig = OJB_getGMValue("Complet_config", {
  915. "choice": -1,
  916. "configurations": []
  917. });
  918. // monaco
  919. OJBetter.monaco.lsp.enabled = OJB_getGMValue("useLSP", false);
  920. OJBetter.monaco.setting.position = OJB_getGMValue("monacoEditor_position", "initial");
  921. OJBetter.monaco.lsp.workUri = OJB_getGMValue("OJBetter_Bridge_WorkUri", "C:/OJBetter_Bridge");
  922. OJBetter.monaco.lsp.socketUrl = OJB_getGMValue("OJBetter_Bridge_SocketUrl", "ws://127.0.0.1:2323/");
  923. OJBetter.preference.showLoading = OJB_getGMValue("showLoading", true);
  924. OJBetter.preference.hoverTargetAreaDisplay = OJB_getGMValue("hoverTargetAreaDisplay", false);
  925. OJBetter.basic.expandFoldingblocks = OJB_getGMValue("expandFoldingblocks", true);
  926. OJBetter.preference.iconButtonSize = OJB_getGMValue("iconButtonSize", "16");
  927. OJBetter.dev.isRuleMarkingEnabled = OJB_getGMValue("isRuleMarkingEnabled", false);
  928. OJBetter.about.updateChannel = OJB_getGMValue("updateChannel", "release");
  929. OJBetter.about.updateSource = OJB_getGMValue("updateSource", "greasyfork");
  930. }
  931.  
  932. /**
  933. * 显示警告消息
  934. */
  935. function showWarnMessage() {
  936. if (OJBetter.typeOfPage.is_oldLatex) {
  937. const loadingMessage = new LoadingMessage();
  938. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_oldLatex', { ns: 'alert' })}`, 'warning');
  939. }
  940. if (OJBetter.typeOfPage.is_acmsguru) {
  941. const loadingMessage = new LoadingMessage();
  942. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_acmsguru', { ns: 'alert' })}`, 'warning');
  943. }
  944. if (OJBetter.translation.comment.transMode == "1") {
  945. const loadingMessage = new LoadingMessage();
  946. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_segment', { ns: 'alert' })}`, 'warning');
  947. }
  948. if (OJBetter.translation.comment.transMode == "2") {
  949. const loadingMessage = new LoadingMessage();
  950. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_select', { ns: 'alert' })}`, 'warning');
  951. }
  952. if (OJBetter.typeOfPage.is_submitPage && OJBetter.monaco.enableOnProblemPage) {
  953. const loadingMessage = new LoadingMessage();
  954. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_submitPage', { ns: 'alert' })}`, 'warning');
  955. }
  956. }
  957.  
  958. // 常量
  959. const helpCircleHTML = '<div class="help-icon"><svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm23.744 191.488c-52.096 0-92.928 14.784-123.2 44.352-30.976 29.568-45.76 70.4-45.76 122.496h80.256c0-29.568 5.632-52.8 17.6-68.992 13.376-19.712 35.2-28.864 66.176-28.864 23.936 0 42.944 6.336 56.32 19.712 12.672 13.376 19.712 31.68 19.712 54.912 0 17.6-6.336 34.496-19.008 49.984l-8.448 9.856c-45.76 40.832-73.216 70.4-82.368 89.408-9.856 19.008-14.08 42.24-14.08 68.992v9.856h80.96v-9.856c0-16.896 3.52-31.68 10.56-45.76 6.336-12.672 15.488-24.64 28.16-35.2 33.792-29.568 54.208-48.576 60.544-55.616 16.896-22.528 26.048-51.392 26.048-86.592 0-42.944-14.08-76.736-42.24-101.376-28.16-25.344-65.472-37.312-111.232-37.312zm-12.672 406.208a54.272 54.272 0 0 0-38.72 14.784 49.408 49.408 0 0 0-15.488 38.016c0 15.488 4.928 28.16 15.488 38.016A54.848 54.848 0 0 0 523.072 768c15.488 0 28.16-4.928 38.72-14.784a51.52 51.52 0 0 0 16.192-38.72 51.968 51.968 0 0 0-15.488-38.016 55.936 55.936 0 0 0-39.424-14.784z"></path></svg></div>';
  960. const closeIcon = `<svg t="1696693011050" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4322" width="14" height="14"><path d="M0 0h1024v1024H0z" fill-opacity="0" p-id="4323"></path><path d="M240.448 168l2.346667 2.154667 289.92 289.941333 279.253333-279.253333a42.666667 42.666667 0 0 1 62.506667 58.026666l-2.133334 2.346667-279.296 279.210667 279.274667 279.253333a42.666667 42.666667 0 0 1-58.005333 62.528l-2.346667-2.176-279.253333-279.253333-289.92 289.962666a42.666667 42.666667 0 0 1-62.506667-58.005333l2.154667-2.346667 289.941333-289.962666-289.92-289.92a42.666667 42.666667 0 0 1 57.984-62.506667z" p-id="4324"></path></svg>`;
  961. const translateIcon = `<svg t="1696837407077" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6325" width="22" height="22"><path d="M536.380952 121.904762a73.142857 73.142857 0 0 1 73.142858 73.142857v219.428571h219.428571a73.142857 73.142857 0 0 1 73.142857 73.142858v341.333333a73.142857 73.142857 0 0 1-73.142857 73.142857H487.619048a73.142857 73.142857 0 0 1-73.142858-73.142857v-219.428571H195.047619a73.142857 73.142857 0 0 1-73.142857-73.142858V195.047619a73.142857 73.142857 0 0 1 73.142857-73.142857h341.333333zM243.809524 682.666667v97.523809h97.523809v73.142857h-97.523809a73.142857 73.142857 0 0 1-73.142857-73.142857v-97.523809h73.142857z m585.142857-195.047619h-219.428571v48.761904a73.142857 73.142857 0 0 1-73.142858 73.142858h-48.761904v219.428571h341.333333V487.619048z m-115.760762 89.526857L787.21219 780.190476h-62.025142l-14.043429-42.715428h-76.068571L620.739048 780.190476h-60.854858l74.605715-203.044571h78.701714z m-38.034286 50.029714h-3.510857l-21.065143 63.488h45.348572l-20.772572-63.488zM536.380952 195.047619H195.047619v341.333333h341.333333V195.047619z
  962. m-195.072 49.883429l44.78781 1.072762v37.278476h87.698286v145.359238h-87.698286v65.974857h-44.78781v-65.974857h-87.698285v-145.359238h87.698285v-38.351238z m0 83.139047h-44.787809v56.05181h44.787809v-56.05181z m89.307429 0h-44.519619v56.05181h44.519619v-56.05181zM780.190476 170.666667a73.142857 73.142857 0 0 1 73.142857 73.142857v97.523809h-73.142857v-97.523809h-97.523809V170.666667h97.523809z" p-id="6326"></path></svg>`;
  963. const clistIcon = `<svg width="37.7pt" height="10pt" viewBox="0 0 181 48" version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="#0057b8ff"><path fill="#0057b8" opacity="1.00" d=" M 17.36 0.00 L 18.59 0.00 C 23.84 6.49 30.28 11.92 36.01 17.98 C 34.01 19.99 32.01 21.99 30.00 23.99 C 26.02 19.97 22.02 15.98 18.02 11.99 C 14.01 15.98 10.01 19.99 6.00 23.99 C 4.16 22.04 2.30 20.05 0.00 18.61 L 0.00 17.37 C 3.44 15.11 6.00 11.84 8.96 9.03 C 11.79 6.05 15.09 3.47 17.36 0.00 Z" /></g><g id="#a0a0a0ff"><path fill="#a0a0a0" opacity="1.00" d=" M 56.76 13.74 C 61.48 4.80 76.07 3.90 81.77 12.27 C 83.09 13.94 83.44 16.10 83.91 18.12 C 81.53 18.23 79.16 18.24 76.78 18.23 C 75.81 15.72 73.99 13.31 71.14 12.95 C 67.14 12.02 63.45 15.29 62.48 18.99 C 61.30 23.27 61.71 28.68 65.34 31.70 C 67.82 34.05 72.19 33.93 74.61 31.55 C 75.97 30.18 76.35 28.23 76.96 26.48 C 79.36 26.43 81.77 26.44 84.17 26.56 C 83.79 30.09 82.43 33.49 79.89 36.02 C 74.14 41.35 64.17 40.80 58.77 35.25 C 53.52 29.56 53.18 20.38 56.76 13.74 Z" />
  964. <path fill="#a0a0a0" opacity="1.00" d=" M 89.01 7.20 C 91.37 7.21 93.74 7.21 96.11 7.22 C 96.22 15.71 96.10 24.20 96.18 32.69 C 101.25 32.76 106.32 32.63 111.39 32.79 C 111.40 34.86 111.41 36.93 111.41 39.00 C 103.94 39.00 96.47 39.00 89.00 39.00 C 89.00 28.40 88.99 17.80 89.01 7.20 Z" /><path fill="#a0a0a0" opacity="1.00" d=" M 115.00 7.21 C 117.33 7.21 119.66 7.21 121.99 7.21 C 122.01 17.81 122.00 28.40 122.00 39.00 C 119.67 39.00 117.33 39.00 115.00 39.00 C 115.00 28.40 114.99 17.80 115.00 7.21 Z" /><path fill="#a0a0a0" opacity="1.00" d=" M 133.35 7.47 C 139.11 5.56 146.93 6.28 150.42 11.87 C 151.42 13.39 151.35 15.31 151.72 17.04 C 149.33 17.05 146.95 17.05 144.56 17.03 C 144.13 12.66 138.66 11.12 135.34 13.30 C 133.90 14.24 133.54 16.87 135.35 17.61 C 139.99 20.02 145.90 19.54 149.92 23.19 C 154.43 26.97 153.16 35.36 147.78 37.72 C 143.39 40.03 137.99 40.11 133.30 38.69 C 128.80 37.34 125.34 32.90 125.91 28.10 C 128.22 28.10 130.53 28.11 132.84 28.16 C 132.98 34.19 142.68 36.07 145.18 30.97 C 146.11 27.99 142.17 27.05 140.05 26.35 C 135.54 25.04 129.83 24.33 127.50 19.63 C 125.30 14.78 128.42 9.00 133.35 7.47 Z" />
  965. <path fill="#a0a0a0" opacity="1.00" d=" M 153.31 7.21 C 161.99 7.21 170.67 7.21 179.34 7.21 C 179.41 9.30 179.45 11.40 179.48 13.50 C 176.35 13.50 173.22 13.50 170.09 13.50 C 170.05 21.99 170.12 30.48 170.05 38.98 C 167.61 39.00 165.18 39.00 162.74 39.00 C 162.64 30.52 162.73 22.04 162.69 13.55 C 159.57 13.49 156.44 13.49 153.32 13.50 C 153.32 11.40 153.31 9.31 153.31 7.21 Z" /></g><g id="#ffd700ff"><path fill="#ffd700" opacity="1.00" d=" M 12.02 29.98 C 14.02 27.98 16.02 25.98 18.02 23.98 C 22.01 27.99 26.03 31.97 30.00 35.99 C 34.01 31.99 38.01 27.98 42.02 23.99 C 44.02 25.98 46.02 27.98 48.01 29.98 C 42.29 36.06 35.80 41.46 30.59 48.00 L 29.39 48.00 C 24.26 41.42 17.71 36.08 12.02 29.98 Z" /></g></svg>`;
  966.  
  967. /**
  968. * 连接数据库
  969. */
  970. async function initDB() {
  971. OJBetter.common.database = new Dexie('CFBetterDB');
  972. OJBetter.common.database.version(3).stores({
  973. samplesData: '&url',
  974. editorCode: '&url',
  975. translateData: '&url',
  976. localizeSubsData: '&lang'
  977. });
  978.  
  979. // 等待数据库打开
  980. await OJBetter.common.database.open();
  981. }
  982.  
  983. /**
  984. * 清空数据库
  985. */
  986. async function clearDatabase() {
  987. const isConfirmed = await OJB_createDialog(
  988. i18next.t('isClearDatabase.title', { ns: 'dialog' }),
  989. i18next.t('isClearDatabase.content', { ns: 'dialog' }),
  990. [
  991. i18next.t('isClearDatabase.buttons.0', { ns: 'dialog' }),
  992. i18next.t('isClearDatabase.buttons.1', { ns: 'dialog' })
  993. ]
  994. );
  995. if (!isConfirmed) {
  996. try {
  997. // 开启一个读写事务,包含数据库中的所有表
  998. await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
  999. // 遍历所有表
  1000. for (const table of OJBetter.common.database.tables) {
  1001. // 清空当前表
  1002. await table.clear();
  1003. }
  1004. });
  1005. console.log("All tables in the database have been cleared.");
  1006. alert("All tables in the database have been cleared.");
  1007. } catch (error) {
  1008. console.error("Error clearing the database:", error);
  1009. }
  1010. }
  1011. }
  1012.  
  1013. /**
  1014. * 导出数据库
  1015. * @returns {Promise<string>} 数据库的JSON字符串
  1016. */
  1017. async function exportDatabase() {
  1018. try {
  1019. // 创建一个存储数据的对象
  1020. const exportData = {};
  1021. // 获取数据库中所有表的名称
  1022. const tableNames = OJBetter.common.database.tables.map(table => table.name);
  1023.  
  1024. // 遍历每一个表,获取数据
  1025. for (const tableName of tableNames) {
  1026. const tableData = await OJBetter.common.database.table(tableName).toArray();
  1027. exportData[tableName] = tableData;
  1028. }
  1029.  
  1030. // 将数据对象转换为JSON字符串
  1031. const jsonData = JSON.stringify(exportData, null, 4);
  1032. return jsonData;
  1033. } catch (error) {
  1034. console.error("Error exporting database:", error);
  1035. }
  1036. }
  1037.  
  1038. /**
  1039. * 导入数据库
  1040. * @param {string} jsonData 数据库的JSON字符串
  1041. */
  1042. async function importDatabase(jsonData) {
  1043. const isConfirmed = await OJB_createDialog(
  1044. i18next.t('isImportDatabase.title', { ns: 'dialog' }),
  1045. i18next.t('isImportDatabase.content', { ns: 'dialog' }),
  1046. [
  1047. i18next.t('isImportDatabase.buttons.0', { ns: 'dialog' }),
  1048. i18next.t('isImportDatabase.buttons.1', { ns: 'dialog' })
  1049. ]
  1050. );
  1051. if (!isConfirmed) {
  1052. try {
  1053. // 将JSON字符串解析为对象
  1054. const importData = JSON.parse(jsonData);
  1055.  
  1056. // 开启一个事务,并清空现有数据
  1057. await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
  1058. // 清空所有表的数据
  1059. for (const tableName of OJBetter.common.database.tables.map(table => table.name)) {
  1060. await OJBetter.common.database.table(tableName).clear();
  1061. }
  1062.  
  1063. // 插入新数据
  1064. for (const [tableName, rows] of Object.entries(importData)) {
  1065. await OJBetter.common.database.table(tableName).bulkAdd(rows);
  1066. }
  1067. });
  1068. alert("Data imported successfully");
  1069. } catch (error) {
  1070. console.error("Error importing database:", error);
  1071. }
  1072. }
  1073. }
  1074.  
  1075. /**
  1076. * 将数据下载为文件
  1077. * @param {string} data 数据
  1078. * @param {string} filename 文件名,默认为'export.json'
  1079. * @param {string} fileType 文件MIME类型,默认为'application/json'
  1080. * @returns {void}
  1081. */
  1082. function downloadDataAsFile(data, filename = 'export.json', fileType = 'application/json') {
  1083. // 创建一个blob对象,指定文件类型
  1084. const blob = new Blob([data], { type: fileType });
  1085. const url = URL.createObjectURL(blob);
  1086.  
  1087. // 创建一个隐藏的a标签,模拟点击进行下载
  1088. const a = document.createElement('a');
  1089. a.href = url;
  1090. a.download = filename;
  1091. document.body.appendChild(a);
  1092. a.click();
  1093.  
  1094. // 清理
  1095. document.body.removeChild(a);
  1096. URL.revokeObjectURL(url);
  1097. }
  1098.  
  1099.  
  1100. /**
  1101. * 从文件中读取数据
  1102. * @param {Function} callback 回调函数
  1103. * @returns {void}
  1104. */
  1105. function readFileInput(callback) {
  1106. const fileInput = document.createElement('input');
  1107. fileInput.type = 'file';
  1108. fileInput.accept = '.json';
  1109. fileInput.style.display = 'none'; // 隐藏input元素
  1110.  
  1111. fileInput.onchange = (e) => {
  1112. const file = e.target.files[0];
  1113. if (file) {
  1114. const reader = new FileReader();
  1115. reader.onload = (e) => {
  1116. const fileContent = e.target.result;
  1117. if (callback && typeof callback === 'function') {
  1118. callback(fileContent); // 调用回调函数,传入文件内容
  1119. }
  1120. };
  1121. reader.readAsText(file);
  1122. }
  1123. };
  1124.  
  1125. document.body.appendChild(fileInput);
  1126. fileInput.click();
  1127. document.body.removeChild(fileInput);
  1128. }
  1129.  
  1130. /**
  1131. * 清除所有设置
  1132. */
  1133. async function deleteAllConfigSettings() {
  1134. const isConfirmed = await OJB_createDialog(
  1135. i18next.t('isDeleteAllConfigSettings.title', { ns: 'dialog' }),
  1136. i18next.t('isDeleteAllConfigSettings.content', { ns: 'dialog' }),
  1137. [
  1138. i18next.t('isDeleteAllConfigSettings.buttons.0', { ns: 'dialog' }),
  1139. i18next.t('isDeleteAllConfigSettings.buttons.1', { ns: 'dialog' })
  1140. ]
  1141. );
  1142. if (!isConfirmed) {
  1143. const keys = GM_listValues();
  1144.  
  1145. keys.forEach(key => {
  1146. GM_deleteValue(key);
  1147. });
  1148.  
  1149. alert('All settings have been deleted.');
  1150. window.location.reload();
  1151. }
  1152. }
  1153.  
  1154. /**
  1155. * 导出设置到JSON
  1156. * @returns {string} JSON字符串
  1157. */
  1158. function exportSettingsToJSON() {
  1159. const keys = GM_listValues();
  1160. let settings = {};
  1161.  
  1162. keys.forEach(key => {
  1163. settings[key] = GM_getValue(key);
  1164. });
  1165.  
  1166. return JSON.stringify(settings, null, 4);
  1167. }
  1168.  
  1169. /**
  1170. * 从JSON导入设置
  1171. * @param {string} jsonData JSON字符串
  1172. * @returns {void}
  1173. */
  1174. async function importSettingsFromJSON(jsonData) {
  1175. const isConfirmed = await OJB_createDialog(
  1176. i18next.t('isImportSettings.title', { ns: 'dialog' }),
  1177. i18next.t('isImportSettings.content', { ns: 'dialog' }),
  1178. [
  1179. i18next.t('isImportSettings.buttons.0', { ns: 'dialog' }),
  1180. i18next.t('isImportSettings.buttons.1', { ns: 'dialog' })
  1181. ]
  1182. );
  1183. if (!isConfirmed) {
  1184. let settings;
  1185. try {
  1186. settings = JSON.parse(jsonData);
  1187. } catch (e) {
  1188. console.error('JSON parsing error:', e);
  1189. return;
  1190. }
  1191.  
  1192. Object.keys(settings).forEach(key => {
  1193. GM_setValue(key, settings[key]);
  1194. });
  1195.  
  1196. alert('Settings imported successfully!');
  1197. window.location.reload();
  1198. }
  1199. }
  1200.  
  1201. /**
  1202. * 加载元素本地化语言数据
  1203. * @param {JQuery} element jQuery元素
  1204. * @param {number} [retries=10] 重试次数
  1205. * @param {number} [interval=50] 重试间隔
  1206. */
  1207. function elementLocalize(element, retries = 10, interval = 50) {
  1208. if ($.isFunction(element.localize)) {
  1209. element.localize();
  1210. } else if (retries > 0) {
  1211. setTimeout(elementLocalize, interval, element, retries - 1, interval);
  1212. } else {
  1213. console.error('Unable to localize', element);
  1214. }
  1215. }
  1216.  
  1217. // 切换系统黑暗监听
  1218. const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  1219. const changeEventListeners = [];
  1220. function handleColorSchemeChange(event) {
  1221. event.matches ? $('html').attr('data-theme', 'dark') : $('html').attr('data-theme', 'light');
  1222. if (!event.matches) {
  1223. var originalColor = $(this).data("original-color");
  1224. $(this).css("background-color", originalColor);
  1225. if (OJBetter.monaco.editor) {
  1226. monaco.editor.setTheme('vs');
  1227. }
  1228. } else {
  1229. if (OJBetter.monaco.editor) {
  1230. monaco.editor.setTheme('vs-dark');
  1231. }
  1232. }
  1233. }
  1234.  
  1235. /**
  1236. * 黑暗模式
  1237. */
  1238. (function setDark() {
  1239. /**
  1240. * 初始化
  1241. */
  1242. function setDarkTheme() {
  1243. const htmlElement = document.querySelector('html');
  1244. if (htmlElement) {
  1245. htmlElement.setAttribute('data-theme', 'dark');
  1246. } else {
  1247. setTimeout(setDarkTheme, 100);
  1248. }
  1249. }
  1250. OJBetter.basic.darkMode = OJB_getGMValue("darkMode", "follow")
  1251. if (OJBetter.basic.darkMode == "dark") {
  1252. setDarkTheme();
  1253. } else if (OJBetter.basic.darkMode == "follow") {
  1254. // 添加事件监听器
  1255. changeEventListeners.push(handleColorSchemeChange);
  1256. mediaQueryList.addEventListener('change', handleColorSchemeChange);
  1257.  
  1258. if (window.matchMedia('(prefers-color-scheme: dark)').matches) setDarkTheme();
  1259. }
  1260.  
  1261. // 定义全局变量
  1262. GM_addStyle(`
  1263. /* 黑暗支持 */
  1264. html[data-theme=dark]:root {
  1265. color-scheme: light dark;
  1266. }
  1267. /* 颜色 */
  1268. :root {
  1269. /* 文字颜色 */
  1270. --ojb-color-text-primary: #a0adb9; /* 主要文字颜色 */
  1271. --ojb-color-text-secondary: #9AA4B1; /* 次要文字颜色 */
  1272. --ojb-color-text-tertiary: #9BA5B2; /* 第三级文字颜色 */
  1273. --ojb-color-text-success: #43A047; /* 成功状态文字颜色 */
  1274. --ojb-color-text-highlight: #cbd6e2; /* 高亮文字颜色 */
  1275. --ojb-color-text-disabled: #506778; /* 禁用状态文字颜色 */
  1276. --ojb-color-text-icon-success: #2e7d32; /* 成功状态图标颜色 */
  1277. --ojb-color-text-link: #4b8eda; /* 链接颜色 */
  1278.  
  1279. /* 背景颜色 */
  1280. --ojb-color-bg-primary: #22272e; /* 主背景颜色 */
  1281. --ojb-color-bg-secondary: #2d333b; /* 次级背景颜色 */
  1282. --ojb-color-bg-disabled: #24292e; /* 禁用元素背景颜色 */
  1283.  
  1284. /* 边框颜色 */
  1285. --ojb-color-border-primary: #48535F; /* 主要边框颜色 */
  1286. --ojb-color-border-disabled: #404950; /* 禁用状态边框颜色 */
  1287. --ojb-color-border-dashed-hover: #03A9F4; /* 虚线边框悬浮颜色 */
  1288. --ojb-color-border-radio-checked: #326154; /* 选中的单选框边框颜色 */
  1289.  
  1290. /* 阴影颜色 */
  1291. --ojb-shadow-standard: 0px 0px 0.5px 0.5px #3A4048; /* 标准阴影 */
  1292. --ojb-shadow-menu-modal: 0px 0px 0px 4px #2d333b; /* 菜单和模态框阴影 */
  1293.  
  1294. /* 区域遮罩颜色 */
  1295. --ojb-overlay-background: repeating-linear-gradient(135deg, #49525f6e, #49525f6e 30px, #49525f29 0px, #49525f29 55px); /* 区域遮罩背景 */
  1296.  
  1297. /* 文字阴影 */
  1298. --ojb-text-shadow-icon: 1px 1px 0px #2d333b, 1px -1px 0px #2d333b, -1px -1px 0px #2d333b, -1px 1px 0px #2d333b; /* 图标文字阴影 */
  1299. }
  1300. /* 边框样式 */
  1301. :root {
  1302. /* 边框样式 */
  1303. --ojb-border-width: 1px; /* 边框宽度 */
  1304. --ojb-border-style-solid: solid; /* 实线样式 */
  1305. --ojb-border-style-dashed: dashed; /* 虚线样式 */
  1306. --ojb-border-radius-small: 4px; /* 小圆角 */
  1307. --ojb-border-radius-medium: 8px; /* 中圆角 */
  1308. --ojb-border-radius-large: 12px; /* 大圆角 */
  1309. /* 组合边框样式 */
  1310. --ojb-border-solid-primary: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-primary); /* 主要实线边框 */
  1311. --ojb-border-dashed: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-primary); /* 主要虚线边框 */
  1312. --ojb-border-dashed-hover: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-dashed-hover); /* 悬浮虚线边框 */
  1313. --ojb-border-solid-disabled: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-disabled); /* 禁用状态实线边框 */
  1314. }
  1315. `);
  1316.  
  1317. // OJBetter界面样式
  1318. GM_addStyle(`
  1319. /* 主要文字颜色 */
  1320. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1321. html[data-theme=dark] .alert-warning, html[data-theme=dark] .markItUpEditor,
  1322. html[data-theme=dark] .translate-problem-statement, html[data-theme=dark] .OJBetter_setting_menu,
  1323. html[data-theme=dark] .help_tip .tip_text,
  1324. html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
  1325. html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
  1326. html[data-theme=dark] #items-per-page, html[data-theme=dark] #pagBar,
  1327. html[data-theme=dark] .OJBetter_setting_sidebar li a:link,
  1328. html[data-theme=dark] .popup .content{
  1329. color: var(--ojb-color-text-primary);
  1330. }
  1331. /* 次要文字颜色 */
  1332. html[data-theme=dark] .ojb_btn:hover, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_statusBar,
  1333. html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #addCustomTest,
  1334. html[data-theme=dark] #customTestBlock, html[data-theme=dark] .OJBetter_setting_list.alert_info{
  1335. color: var(--ojb-color-text-secondary);
  1336. }
  1337. /* 文字颜色3 */
  1338. html[data-theme=dark] .ojb_btn{
  1339. color: var(--ojb-color-text-tertiary);
  1340. }
  1341. /* 文字颜色 浅绿 */
  1342. html[data-theme=dark] #SubmitButton{
  1343. color: var(--ojb-color-text-success);
  1344. }
  1345. /* 禁止文字颜色 */
  1346. html[data-theme=dark] .ojb_btn[disabled]{
  1347. color: var(--ojb-color-text-disabled);
  1348. }
  1349. /* 主要背景层次 */
  1350. html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] li#add_button:hover,
  1351. html[data-theme=dark] .ojb_btn:hover,
  1352. html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
  1353. html[data-theme=dark] #OJBetter_SubmitForm input,
  1354. html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
  1355. html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
  1356. html[data-theme=dark] .OJBetter_setting_sidebar li a.active, html[data-theme=dark] .OJBetter_setting_sidebar li,
  1357. html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-track, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-track,
  1358. html[data-theme=dark] .OJBetter_modal, html[data-theme=dark] .OJBetter_modal button:hover,
  1359. html[data-theme=dark] .popup .content,
  1360. html[data-theme=dark] .config_bar_list, html[data-theme=dark] #LSPLog,
  1361. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
  1362. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before,
  1363. html[data-theme=dark] .OJBetter_setting_menu a, html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button:hover,
  1364. html[data-theme=dark] .OJBetter_setting_menu select{
  1365. background-color: var(--ojb-color-bg-primary);
  1366. background-image: none;
  1367. }
  1368. /* 次要背景层次 */
  1369. html[data-theme=dark] .ojb_btn,
  1370. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1371. html[data-theme=dark] .alert-warning, html[data-theme=dark] .SumoSelect>.optWrapper>.options li.opt:hover,
  1372. html[data-theme=dark] .translate-problem-statement-panel,
  1373. html[data-theme=dark] .translate-problem-statement,
  1374. html[data-theme=dark] .OJBetter_setting_list,
  1375. html[data-theme=dark] .OJBetter_setting_menu hr,
  1376. html[data-theme=dark] .OJBetter_setting_sidebar li a,
  1377. html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-thumb, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-thumb,
  1378. html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] .test-for-popup pre,
  1379. html[data-theme=dark] .popup .content pre, html[data-theme=dark] .popup .content pre code,
  1380. html[data-theme=dark] ul.config_bar_ul::-webkit-scrollbar-thumb, html[data-theme=dark] #OJBetter_statusBar,
  1381. html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] .sampleDiv,
  1382. html[data-theme=dark] #addCustomTest, html[data-theme=dark] #LSPLog li:nth-child(odd),
  1383. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::before,
  1384. html[data-theme=dark] .config::before, html[data-theme=dark] .config li.tempConfig_add_button:hover,
  1385. html[data-theme=dark] .OJBetter_setting_menu details, html[data-theme=dark] #config_bar_menu,
  1386. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button,
  1387. html[data-theme=dark] .OJBetter_setting_menu .badge, html[data-theme=dark] #OJBetter_SubmitForm #SubmitButton{
  1388. background-color: var(--ojb-color-bg-secondary);
  1389. }
  1390. /* 禁止背景层次 */
  1391. html[data-theme=dark] .ojb_btn[disabled]{
  1392. background-color: var(--ojb-color-bg-disabled);
  1393. }
  1394. /* 实线边框颜色-圆角 */
  1395. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1396. html[data-theme=dark] .alert-warning, html[data-theme=dark] .translate-problem-statement{
  1397. border: var(--ojb-border-solid-primary);
  1398. border-radius: 2px;
  1399. }
  1400. /* 实线边框颜色-无圆角 */
  1401. html[data-theme=dark] .ojb_btn,
  1402. html[data-theme=dark] .OJBetter_setting_list, html[data-theme=dark] .config_bar_list,
  1403. html[data-theme=dark] label.config_bar_ul_li_text,
  1404. html[data-theme=dark] .OJBetter_setting_sidebar li, html[data-theme=dark] .OJBetter_setting_menu select,
  1405. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_SubmitForm select,
  1406. html[data-theme=dark] #OJBetter_editor, html[data-theme=dark] #OJBetter_statusBar,
  1407. html[data-theme=dark] #OJBetter_SubmitForm #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #customTestBlock,
  1408. html[data-theme=dark] #OJBetter_SubmitForm #addCustomTest, html[data-theme=dark] #OJBetter_SubmitForm #SubmitButton,
  1409. html[data-theme=dark] .OJBetter_setting_menu input,
  1410. html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
  1411. html[data-theme=dark] .OJBetter_setting_menu textarea,
  1412. html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea,
  1413. html[data-theme=dark] #CompilerSetting select, html[data-theme=dark] #CompilerSetting textarea, html[data-theme=dark] #CompilerBox,
  1414. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
  1415. html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] .config::before,
  1416. html[data-theme=dark] #statePanel, html[data-theme=dark] .test-case, html[data-theme=dark] .OJBetter_setting_menu .badge{
  1417. border: var(--ojb-border-solid-primary);
  1418. }
  1419. html[data-theme=dark] #customTestBlock #customTests{
  1420. border-top: var(--ojb-border-solid-primary);
  1421. }
  1422. html[data-theme=dark] .OJBetter_setting_sidebar {
  1423. border-right: var(--ojb-border-solid-primary);
  1424. }
  1425. /* 实线边框-禁止 */
  1426. html[data-theme=dark] .ojb_btn[disabled]{
  1427. border: var(--ojb-border-solid-disabled);
  1428. }
  1429. /* 虚线边框 */
  1430. html[data-theme=dark] li#add_button,
  1431. html[data-theme=dark] .OJBetter_setting_menu_label_text{
  1432. border: var(--ojb-border-dashed);
  1433. }
  1434. /* 虚线边框-悬浮 */
  1435. html[data-theme=dark] li#add_button:hover{
  1436. border: var(--ojb-border-dashed-hover);
  1437. background-color: var(--ojb-color-bg-secondary);
  1438. color: var(--ojb-color-border-dashed-hover);
  1439. }
  1440. /* 无边框 */
  1441. html[data-theme=dark] .translate-problem-statement-panel .ojb_btn{
  1442. border: none;
  1443. }
  1444. /* 区域遮罩 */
  1445. html[data-theme=dark] .overlay::before {
  1446. background: var(--ojb-overlay-background);
  1447. color: var(--ojb-color-text-secondary);
  1448. text-shadow: 0px 0px 2px #000000;
  1449. }
  1450. /* 阴影 */
  1451. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .translate-problem-statement{
  1452. box-shadow: var(--ojb-shadow-standard);
  1453. }
  1454. /* 图标按钮状态样式 */
  1455. html[data-theme=dark] .ojb_btn_popover.success i:before, html[data-theme=dark] .ojb_btn_popover.success i {
  1456. color: var(--ojb-color-text-icon-success);
  1457. }
  1458. html[data-theme=dark] .ojb_btn_popover i:before {
  1459. text-shadow: var(--ojb-text-shadow-icon);
  1460. }
  1461. /* 其他样式 */
  1462. html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .OJBetter_modal{
  1463. box-shadow: var(--ojb-shadow-menu-modal);
  1464. border: 1px solid var(--ojb-color-bg-secondary);
  1465. }
  1466. html[data-theme=dark] input[type="radio"]:checked+.OJBetter_setting_menu_label_text {
  1467. color: var(--ojb-color-text-primary);
  1468. border: 1px solid var(--ojb-color-border-radio-checked);
  1469. }
  1470. html[data-theme=dark] .alert{
  1471. text-shadow: none;
  1472. }
  1473. `);
  1474.  
  1475. // 网站界面样式
  1476. GM_addStyle(`
  1477. /* 文字颜色1 */
  1478. html[data-theme=dark] body, html[data-theme=dark] .title,
  1479. html[data-theme=dark] .problem-statement, html[data-theme=dark] #pageContent,
  1480. html[data-theme=dark] .ttypography, html[data-theme=dark] .roundbox, html[data-theme=dark] .info,
  1481. html[data-theme=dark] .ttypography .bordertable, html[data-theme=dark] .ttypography .bordertable thead th,
  1482. html[data-theme=dark] .ttypography h1, html[data-theme=dark] .ttypography h2, html[data-theme=dark] .ttypography h3,
  1483. html[data-theme=dark] .ttypography h4, html[data-theme=dark] .ttypography h5, html[data-theme=dark] .ttypography h6,
  1484. html[data-theme=dark] .datatable table, html[data-theme=dark] .problem-statement .sample-tests pre,
  1485. html[data-theme=dark] .ace-chrome .ace_gutter,
  1486. html[data-theme=dark] .setting-name,
  1487. html[data-theme=dark] .user-black, html[data-theme=dark] .comments label.show-archived,
  1488. html[data-theme=dark] .comments label.show-archived *, html[data-theme=dark] table{
  1489. color: var(--ojb-color-text-primary) !important;
  1490. }
  1491. html[data-theme=dark] h1 a, html[data-theme=dark] h2 a, html[data-theme=dark] h3 a, html[data-theme=dark] h4 a{
  1492. color: var(--ojb-color-text-secondary);
  1493. }
  1494. /* 文字颜色2 */
  1495. html[data-theme=dark] .contest-state-phase, html[data-theme=dark] .legendary-user-first-letter,
  1496. html[data-theme=dark] .lang-chooser,
  1497. html[data-theme=dark] .second-level-menu-list li a, html[data-theme=dark] #footer,
  1498. html[data-theme=dark] .ttypography .tt, html[data-theme=dark] select,
  1499. html[data-theme=dark] .roundbox .caption, html[data-theme=dark] .topic .title *,
  1500. html[data-theme=dark] .user-admin{
  1501. color: var(--ojb-color-text-secondary) !important;
  1502. }
  1503. /* 文字颜色3 */
  1504. html[data-theme=dark] #program-source-text-copy{
  1505. color: var(--ojb-color-text-secondary);
  1506. }
  1507. html[data-theme=dark] input{
  1508. color: var(--ojb-color-text-secondary) !important;
  1509. }
  1510. /* 文字颜色4 */
  1511. html[data-theme=dark] .ttypography .MathJax, html[data-theme=dark] .MathJax span{
  1512. color: var(--ojb-color-text-highlight) !important;
  1513. }
  1514. /* 链接颜色 */
  1515. html[data-theme=dark] a:link {
  1516. color: var(--ojb-color-text-link);
  1517. }
  1518. html[data-theme=dark] a:visited {
  1519. color: var(--ojb-color-text-secondary);
  1520. }
  1521. html[data-theme=dark] .menu-box a, html[data-theme=dark] .sidebox th a{
  1522. color: var(--ojb-color-text-secondary) !important;
  1523. }
  1524. /* 按钮 */
  1525. html[data-theme=dark] .second-level-menu-list li.backLava {
  1526. border-radius: 6px;
  1527. overflow: hidden;
  1528. filter: invert(1) hue-rotate(.5turn);
  1529. }
  1530. html[data-theme=dark] input:hover{
  1531. background-color: var(--ojb-color-bg-primary) !important;
  1532. }
  1533. /* 背景层次1 */
  1534. html[data-theme=dark] body, html[data-theme=dark] .ttypography .bordertable thead th,
  1535. html[data-theme=dark] .datatable table, html[data-theme=dark] .datatable .dark, html[data-theme=dark] li#add_button,
  1536. html[data-theme=dark] .problem-statement .sample-tests pre, html[data-theme=dark] .markItUpEditor,
  1537. html[data-theme=dark] .SumoSelect>.CaptionCont, html[data-theme=dark] .SumoSelect>.optWrapper,
  1538. html[data-theme=dark] .SumoSelect>.optWrapper.multiple>.options li.opt span i, html[data-theme=dark] .ace_scroller,
  1539. html[data-theme=dark] textarea, html[data-theme=dark] .state, html[data-theme=dark] .ace-chrome .ace_gutter-active-line,
  1540. html[data-theme=dark] .sidebar-menu ul li:hover, html[data-theme=dark] .sidebar-menu ul li.active,
  1541. html[data-theme=dark] .popup .content, html[data-theme=dark] .file.input-view .text, html[data-theme=dark] .file.output-view .text,
  1542. html[data-theme=dark] .file.answer-view .text, html[data-theme=dark] .file.checker-comment-view .text{
  1543. background-color: var(--ojb-color-bg-primary) !important;
  1544. background-image: none;
  1545. }
  1546. /* 背景层次2 */
  1547. html[data-theme=dark] .roundbox, html[data-theme=dark] .roundbox .dark, html[data-theme=dark] .bottom-links,
  1548. html[data-theme=dark] .spoiler-content, html[data-theme=dark] input,
  1549. html[data-theme=dark] .problem-statement .test-example-line-even, html[data-theme=dark] .highlight-blue,
  1550. html[data-theme=dark] .ttypography .tt, html[data-theme=dark] select,
  1551. html[data-theme=dark] .SumoSelect>.optWrapper>.options li.opt:hover,
  1552. html[data-theme=dark] .aceEditorTd, html[data-theme=dark] .ace-chrome .ace_gutter,
  1553. html[data-theme=dark] .datatable,
  1554. html[data-theme=dark] .highlighted-row td, html[data-theme=dark] .highlighted-row th,
  1555. html[data-theme=dark] .pagination span.active, html[data-theme=dark] .test-for-popup pre{
  1556. background-color: var(--ojb-color-bg-secondary) !important;
  1557. }
  1558. /* 实线边框颜色-圆角 */
  1559. html[data-theme=dark] .roundbox, html[data-theme=dark] .roundbox .rtable td,
  1560. html[data-theme=dark] .sidebar-menu ul li,
  1561. html[data-theme=dark] input, html[data-theme=dark] .ttypography .tt, html[data-theme=dark] #items-per-page,
  1562. html[data-theme=dark] .datatable td, html[data-theme=dark] .datatable th,
  1563. html[data-theme=dark] textarea, html[data-theme=dark] .input-output-copier{
  1564. border: var(--ojb-border-solid-primary) !important;
  1565. border-radius: 2px;
  1566. }
  1567. /* 实线边框颜色-无圆角 */
  1568. html[data-theme=dark] .problem-statement .sample-tests .input,
  1569. html[data-theme=dark] .problem-statement .sample-tests .output, html[data-theme=dark] .pagination span.active,
  1570. html[data-theme=dark] .test-for-popup pre{
  1571. border: var(--ojb-border-solid-primary) !important;
  1572. }
  1573. html[data-theme=dark] .roundbox .titled, html[data-theme=dark] .roundbox .rtable th {
  1574. border-bottom: var(--ojb-border-solid-primary) !important;
  1575. }
  1576. html[data-theme=dark] .roundbox .bottom-links, html[data-theme=dark] #footer{
  1577. border-top: var(--ojb-border-solid-primary) !important;
  1578. }
  1579. html[data-theme=dark] .topic .content {
  1580. border-left: 4px solid var(--ojb-color-border-primary) !important;
  1581. }
  1582. html[data-theme=dark] hr {
  1583. border-color: var(--ojb-color-border-primary) !important;
  1584. }
  1585. /* 虚线边框 */
  1586. html[data-theme=dark] .comment-table{
  1587. border: var(--ojb-border-dashed);
  1588. }
  1589. /* input-output-copier特殊处理 */
  1590. html[data-theme=dark] .html2md-panel.input-output-copier,
  1591. html[data-theme=dark] .translateDiv.input-output-copier,
  1592. html[data-theme=dark] #OJBetter_SubmitForm.input-output-copier{
  1593. border: none !important;
  1594. }
  1595. html[data-theme=dark] .html2md-panel.input-output-copier:hover,
  1596. html[data-theme=dark] #OJBetter_SubmitForm.input-output-copier:hover{
  1597. background-color: #ffffff00 !important;
  1598. }
  1599. /* focus-visible */
  1600. html[data-theme=dark] input:focus-visible, html[data-theme=dark] textarea, html[data-theme=dark] select{
  1601. border-width: 1.5px !important;
  1602. outline: none;
  1603. }
  1604. /* 图片-亮度 */
  1605. html[data-theme=dark] img, html[data-theme=dark] #facebox .popup a{
  1606. opacity: .75;
  1607. }
  1608. /* 反转 */
  1609. html[data-theme=dark] .SumoSelect>.CaptionCont>label>i, html[data-theme=dark] .delete-resource-link,
  1610. html[data-theme=dark] #program-source-text, html[data-theme=dark] .spoiler-content pre,
  1611. html[data-theme=dark] .popup .content pre code{
  1612. filter: invert(1) hue-rotate(.5turn);
  1613. }
  1614. /* 阴影 */
  1615. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .translate-problem-statement{
  1616. box-shadow: var(--ojb-shadow-standard);
  1617. }
  1618. /* 图标按钮状态样式 */
  1619. html[data-theme=dark] .ojb_btn_popover.success i:before, html[data-theme=dark] .ojb_btn_popover.success i {
  1620. color: var(--ojb-color-text-icon-success);
  1621. }
  1622. html[data-theme=dark] .ojb_btn_popover i:before {
  1623. text-shadow: var(--ojb-text-shadow-icon);
  1624. }
  1625. /* 评测状态文字颜色 */
  1626. html[data-theme=dark] .verdict-accepted, html[data-theme=dark] .verdict-accepted-challenged,
  1627. html[data-theme=dark] .verdict-successful-challenge{
  1628. color: #0a0 !important;
  1629. }
  1630. html[data-theme=dark] .verdict-failed, html[data-theme=dark] .verdict-challenged{
  1631. color: red !important;
  1632. }
  1633. html[data-theme=dark] .verdict-rejected, html[data-theme=dark] .verdict-unsuccessful-challenge{
  1634. color: #673ab7 !important;
  1635. }
  1636. html[data-theme=dark] .verdict-waiting {
  1637. color: gray !important;
  1638. }
  1639. /* 样例hover样式 */
  1640. html[data-theme=dark] .problem-statement .darkhighlight {
  1641. background-color: #455a64 !important;
  1642. }
  1643. /* 其他样式 */
  1644. html[data-theme=dark] .rated-user{
  1645. display: initial;
  1646. }
  1647. html[data-theme=dark] .datatable .ilt, html[data-theme=dark] .datatable .irt,
  1648. html[data-theme=dark] .datatable .ilb, html[data-theme=dark] .datatable .irb,
  1649. html[data-theme=dark] .datatable .lt, html[data-theme=dark] .datatable .rt,
  1650. html[data-theme=dark] .datatable .lb, html[data-theme=dark] .datatable .rb{
  1651. background: none;
  1652. }
  1653. html[data-theme=dark] .problems .accepted-problem td.id{
  1654. border-left: 6px solid #47837d !important;
  1655. }
  1656. html[data-theme=dark] .problems .rejected-problem td.id{
  1657. border-left: 6px solid #ef9a9a !important;
  1658. }
  1659. html[data-theme=dark] .problems .accepted-problem td.act {
  1660. background-color: #47837d !important;
  1661. border-radius: 0px;
  1662. }
  1663. html[data-theme=dark] .problems .rejected-problem td.act{
  1664. background-color: #ef9a9a !important;
  1665. border-radius: 0px;
  1666. }
  1667. html[data-theme=dark] .collapsible-topic.collapsed .content .collapsible-topic-options:before{
  1668. background-image: linear-gradient(#22272e00, #22272e);
  1669. }
  1670. `);
  1671. })()
  1672.  
  1673. /**
  1674. * 黑暗模式额外的处理事件
  1675. */
  1676. function darkModeStyleAdjustment() {
  1677. $(".test-example-line").off("mouseenter mouseleave"); // 移除上面原本的事件
  1678. // 为奇数行添加悬停效果
  1679. $('.test-example-line-odd').hover(
  1680. function () {
  1681. $(this).addClass('darkhighlight');
  1682. $(this).prevUntil(':not(.test-example-line-odd)').addClass('darkhighlight');
  1683. $(this).nextUntil(':not(.test-example-line-odd)').addClass('darkhighlight');
  1684. },
  1685. function () {
  1686. $(this).removeClass('darkhighlight');
  1687. $(this).prevUntil(':not(.test-example-line-odd)').removeClass('darkhighlight');
  1688. $(this).nextUntil(':not(.test-example-line-odd)').removeClass('darkhighlight');
  1689. }
  1690. );
  1691.  
  1692. // 为偶数行添加悬停效果
  1693. $('.test-example-line-even').hover(
  1694. function () {
  1695. $(this).addClass('darkhighlight');
  1696. $(this).prevUntil(':not(.test-example-line-even)').addClass('darkhighlight');
  1697. $(this).nextUntil(':not(.test-example-line-even)').addClass('darkhighlight');
  1698. },
  1699. function () {
  1700. $(this).removeClass('darkhighlight');
  1701. $(this).prevUntil(':not(.test-example-line-even)').removeClass('darkhighlight');
  1702. $(this).nextUntil(':not(.test-example-line-even)').removeClass('darkhighlight');
  1703. }
  1704. );
  1705. }
  1706.  
  1707. /**
  1708. * 初始化monaco编辑器资源
  1709. */
  1710. async function initMonacoEditor() {
  1711. if (OJBetter.monaco.enableOnProblemPage || OJBetter.monaco.beautifyPreBlocks) {
  1712. try {
  1713. // 等待Monaco Editor加载器脚本加载完成
  1714. await OJB_LoadJS("https://cdn.staticfile.net/monaco-editor/0.44.0/min/vs/loader.min.js");
  1715.  
  1716. // 配置Monaco Editor
  1717. require.config({
  1718. paths: { vs: "https://cdn.staticfile.net/monaco-editor/0.44.0/min/vs" },
  1719. "vs/nls": { availableLanguages: { "*": "zh-cn" } },
  1720. });
  1721.  
  1722. // 加载Monaco Editor主脚本
  1723. require(["vs/editor/editor.main"], () => {
  1724. OJBetter.monaco.loaderOnload = true;
  1725. });
  1726. } catch (error) {
  1727. console.error("Failed to load Monaco Editor: ", error);
  1728. }
  1729. }
  1730. }
  1731.  
  1732. /**
  1733. * 美化代码块
  1734. */
  1735. async function beautifyPreBlocksWithMonaco() {
  1736. // 判断monacoLoader是否加载完毕
  1737. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  1738.  
  1739. // 用于替换 <pre> 标签为 Monaco 编辑器的函数
  1740. function replacePreWithMonaco(preElement) {
  1741. const pre = $(preElement);
  1742. if (pre.hasClass('source-code-for-copy')) return; // 跳过复制块
  1743. const code = OJB_getCodeFromPre(pre.get(0));
  1744. if (!code) return;
  1745. const language = OJB_codeLangDetect(code);
  1746.  
  1747. // 创建一个用于 Monaco 编辑器的容器
  1748. const container = $('<div></div>');
  1749. const lineCount = code.split('\n').length; // 代码的行数
  1750.  
  1751. // 计算容器的高度
  1752. const calculateContainerHeight = (lineCount) => {
  1753. const lineHeight = 20; // 每行代码的高度
  1754. const minHeight = 100; // 最小高度
  1755. const maxHeight = 1000; // 最大高度
  1756. const dynamicHeight = lineCount * lineHeight;
  1757. return Math.min(Math.max(dynamicHeight, minHeight), maxHeight) + 'px';
  1758. };
  1759.  
  1760. // 应用样式
  1761. container.css({
  1762. height: calculateContainerHeight(lineCount),
  1763. width: '100%'
  1764. });
  1765. pre.replaceWith(container);
  1766.  
  1767. // 初始化 Monaco 编辑器
  1768. monaco.editor.create(container[0], {
  1769. value: code,
  1770. language: language,
  1771. readOnly: true,
  1772. tabSize: 4,
  1773. theme: OJBetter.basic.darkMode == "dark" ? "vs-dark" : "vs",
  1774. scrollbar: {
  1775. verticalScrollbarSize: 10,
  1776. horizontalScrollbarSize: 10,
  1777. alwaysConsumeMouseWheel: false
  1778. },
  1779. automaticLayout: true,
  1780. scrollBeyondLastLine: false
  1781. });
  1782. }
  1783. // 全局替换页面上所有的 <pre> 元素
  1784. $('pre').each(function () {
  1785. replacePreWithMonaco(this);
  1786. });
  1787. // 监听页面上的提交状态页面窗口的 <pre> 元素
  1788. if (OJBetter.typeOfPage.is_statePage || OJBetter.typeOfPage.is_submissions) {
  1789. OJB_observeElement({
  1790. selector: '#facebox',
  1791. callback: (node) => {
  1792. // 如果 facebox 中存在 pre 元素,则替换它们
  1793. const preElements = $(node).find('pre');
  1794. preElements.each(function () {
  1795. replacePreWithMonaco(this);
  1796. });
  1797. }
  1798. });
  1799. }
  1800. }
  1801.  
  1802. // 样式
  1803. GM_addStyle(`
  1804. /*动画*/
  1805. @keyframes shake {
  1806. 0% { transform: translateX(-5px); }
  1807. 100% { transform: translateX(5px); }
  1808. }
  1809. @keyframes rotate {
  1810. from {
  1811. transform: rotate(0deg);
  1812. }
  1813.  
  1814. to {
  1815. transform: rotate(360deg);
  1816. }
  1817. }
  1818. @keyframes rippleout {
  1819. 0% {
  1820. box-shadow: 0 0 0 0 rgba(96, 98, 102, 0.2);
  1821. }
  1822.  
  1823. 100% {
  1824. box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
  1825. }
  1826. }
  1827. @keyframes bounce-in {
  1828. 20%,40%,60%,80%,from,to {
  1829. animation-timing-function: cubic-bezier(.215,.61,.355,1);
  1830. }
  1831.  
  1832. 0% {
  1833. opacity: 0;
  1834. transform: scale3d(.995,.995,.995);
  1835. }
  1836.  
  1837. 20% {
  1838. opacity: 1;
  1839. transform: scale3d(1.005,1.005,1.005);
  1840. }
  1841.  
  1842. 40% {
  1843. transform: scale3d(.998,.998,.998);
  1844. }
  1845.  
  1846. 60% {
  1847. transform: scale3d(1.002,1.002,1.002);
  1848. }
  1849.  
  1850. 80% {
  1851. transform: scale3d(.995,.995,.995);
  1852. }
  1853.  
  1854. to {
  1855. opacity: 1;
  1856. transform: scale3d(1,1,1);
  1857. }
  1858. }
  1859. /*iconfont图标*/
  1860. .iconfont {
  1861. font-family: "iconfont" !important;
  1862. font-size: 16px;
  1863. font-style: normal !important;
  1864. -webkit-font-smoothing: antialiased;
  1865. -moz-osx-font-smoothing: grayscale;
  1866. }
  1867. @font-face {
  1868. font-family: 'iconfont'; /* Project id 4284341 */
  1869. src: url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2') format('woff2'),
  1870. url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2.ttf') format('truetype');
  1871. }
  1872. html {
  1873. scroll-behavior: smooth;
  1874. }
  1875. :root {
  1876. --vp-font-family-base: "Chinese Quotes", "Inter var", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  1877. }
  1878. span.mdViewContent {
  1879. white-space: pre-wrap;
  1880. }
  1881.  
  1882. /* 特殊处理,加上input-output-copier类, 让convertStatementToText方法忽略该元素 */
  1883. .translateDiv.input-output-copier, .html2md-panel.input-output-copier, #OJBetter_SubmitForm.input-output-copier {
  1884. font-size: initial;
  1885. float: initial;
  1886. color: initial;
  1887. cursor: initial;
  1888. border: none;
  1889. padding: 0px;
  1890. margin: 0px;
  1891. line-height: initial;
  1892. text-transform: none;
  1893. }
  1894. .translateDiv.input-output-copier:hover, .html2md-panel.input-output-copier:hover, #OJBetter_SubmitForm.input-output-copier:hover {
  1895. background-color: #ffffff00;
  1896. }
  1897.  
  1898. /* dialog */
  1899. dialog {
  1900. margin: 0px;
  1901. }
  1902. dialog::backdrop {
  1903. background-color: rgba(0, 0, 0, 0.4);
  1904. }
  1905.  
  1906. /*题目页链接栏样式*/
  1907. #problemToolbar {
  1908. display: flex;
  1909. flex-wrap: wrap;
  1910. justify-content: flex-end;
  1911. overflow: auto;
  1912. height: 100%;
  1913. margin: 0.5em;
  1914. }
  1915.  
  1916. /*html2md面板*/
  1917. .html2md-panel {
  1918. display: flex;
  1919. justify-content: flex-end;
  1920. align-items: center;
  1921. }
  1922. .html2md-panel a {
  1923. text-decoration: none;
  1924. }
  1925. .html2md-panel > button {
  1926. margin: 5px;
  1927. }
  1928. .html2md-panel.is_simple {
  1929. position: absolute;
  1930. right: 2%;
  1931. }
  1932.  
  1933. /*通用按钮*/
  1934. .ojb_btn {
  1935. display: flex;
  1936. align-items: center;
  1937. justify-content: center;
  1938. cursor: pointer;
  1939. background-color: #ffffff;
  1940. color: #606266;
  1941. width: auto;
  1942. font-size: 13px;
  1943. border-radius: 0.3rem;
  1944. padding: 2px 5px;
  1945. margin: 0px 5px;
  1946. border: 1px solid #dcdfe6;
  1947. }
  1948. .ojb_btn[disabled] {
  1949. cursor: not-allowed !important;
  1950. color: rgb(168, 171, 178) !important;
  1951. border: 1px solid #e4e7ed;
  1952. background-color: #ffffff;
  1953. }
  1954. .ojb_btn:hover {
  1955. color: #409eff;
  1956. border-color: #409eff;
  1957. background-color: #f1f8ff;
  1958. z-index: 150;
  1959. }
  1960. .ojb_btn.primary {
  1961. color: #ffffff;
  1962. border: 1px solid #409eff;
  1963. background-color: #409eff;
  1964. }
  1965. .ojb_btn.primary:hover {
  1966. color: #ffffff;
  1967. border: 1px solid #79bbff;
  1968. background-color: #79bbff;
  1969. }
  1970. .ojb_btn.success {
  1971. color: #4caf50;
  1972. border: 1px solid #C8E6C9;
  1973. background-color: #f0f9eb;
  1974. }
  1975. .ojb_btn.warning {
  1976. color: #e6a23c;
  1977. border: 1px solid #f3d19e;
  1978. background-color: #fdf6ec;
  1979. }
  1980. .ojb_btn.error {
  1981. color: #f56c6c;
  1982. border: 1px solid #fab6b6;
  1983. background-color: #fef0f0;
  1984. }
  1985. .ojb_btn.enabled {
  1986. color: #42A5F5;
  1987. border: 1px solid #90CAF9;
  1988. background-color: #fafbff;
  1989. }
  1990. .ojb_btn.active {
  1991. animation: rippleout 0.5s ease-in-out;
  1992. }
  1993. a.ojb_btn {
  1994. text-decoration: none;
  1995. }
  1996. a.ojb_btn:link {
  1997. color: #606266;
  1998. }
  1999. a.ojb_btn span {
  2000. margin-left: 2px;
  2001. }
  2002. /*按钮图标和popover*/
  2003. .ojb_btn_popover {
  2004. display: flex;
  2005. justify-content: center;
  2006. position: relative;
  2007. outline: none;
  2008. appearance: none;
  2009. }
  2010. .ojb_btn_popover:hover span {
  2011. opacity: 1;
  2012. visibility: visible;
  2013. }
  2014. .ojb_btn_popover i:before {
  2015. position: absolute;
  2016. text-shadow: 1px 1px 0px #ffffff, 1px -1px 0px #ffffff, -1px -1px 0px #ffffff, -1px 1px 0px #ffffff;
  2017. }
  2018. .ojb_btn_popover span {
  2019. cursor: initial;
  2020. position: absolute;
  2021. left: 50%;
  2022. opacity: 0;
  2023. visibility: hidden;
  2024. padding: 4px 8px;
  2025. background-color: rgba(33, 33, 33, 0.8);
  2026. color: rgba(255, 255, 255, 0.9019607843);
  2027. font-size: 12px;
  2028. border-radius: 6px;
  2029. line-height: 1.6;
  2030. text-align: left;
  2031. white-space: nowrap;
  2032. transition: all 0.15s ease-in-out;
  2033. z-index: 999;
  2034. }
  2035. .ojb_btn_popover span:hover {
  2036. opacity: 0;
  2037. visibility: hidden;
  2038. }
  2039. .ojb_btn_popover.top:hover span {
  2040. transform: translate(-50%, 0);
  2041. }
  2042. .ojb_btn_popover.top span {
  2043. bottom: 100%;
  2044. transform: translate(-50%, -20%);
  2045. margin-bottom: 4px;
  2046. }
  2047. .ojb_btn_popover.top span:hover {
  2048. transform: translate(-50%, -20%);
  2049. }
  2050. .ojb_btn_popover.bottom:hover span {
  2051. transform: translate(-50%, 105%);
  2052. }
  2053. .ojb_btn_popover.bottom span {
  2054. bottom: -2%;
  2055. transform: translate(-50%, 100%);
  2056. margin-top: 4px;
  2057. }
  2058. .ojb_btn_popover.bottom span:hover {
  2059. transform: translate(-50%, 50%);
  2060. }
  2061. .ojb_btn_popover.loading i {
  2062. color: rgba(33, 33, 33, 0.1);
  2063. }
  2064. .ojb_btn_popover.loading i:before {
  2065. content: "\\e640";
  2066. color: rgb(168, 171, 178);
  2067. animation: rotate 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
  2068. }
  2069. .ojb_btn_popover.running i {
  2070. color: rgba(33, 33, 33, 0.1);
  2071. }
  2072. .ojb_btn_popover.running i:before {
  2073. content: "\\e600";
  2074. color: rgb(168, 171, 178);
  2075. animation: rotate 1s linear infinite;
  2076. }
  2077. .ojb_btn_popover.warning i {
  2078. color: rgba(230, 162, 61, 0.8);
  2079. }
  2080. .ojb_btn_popover.warning i:before {
  2081. content: "\\e68b";
  2082. font-size: 15px;
  2083. left: 10px;
  2084. bottom: 0%;
  2085. color: #ff9800;
  2086. }
  2087. .ojb_btn_popover.error i {
  2088. color: rgba(245, 108, 108, 0.8);
  2089. }
  2090. .ojb_btn_popover.error i:before {
  2091. content: "\\e651";
  2092. font-size: 15px;
  2093. left: 10px;
  2094. bottom: 0%;
  2095. color: #F44336;
  2096. }
  2097. .ojb_btn_popover.success i {
  2098. color: rgba(76, 175, 80, 0.9);
  2099. }
  2100. .ojb_btn_popover.success i:before {
  2101. content: "\\e61e";
  2102. font-size: 15px;
  2103. left: 10px;
  2104. bottom: 0%;
  2105. color: #4caf50;
  2106. }
  2107. .ojb_btn_popover.enabled i {
  2108. color: rgba(33, 150, 243, 0.6);
  2109. }
  2110. .ojb_btn_popover.enabled i:before {
  2111. content: "\\e6f4";
  2112. font-size: 15px;
  2113. left: 10px;
  2114. bottom: 0%;
  2115. color: #2196F3;
  2116. }
  2117. .ojb_btn_popover.redo i {
  2118. color: rgba(33, 33, 33, 0.1);
  2119. }
  2120. .ojb_btn_popover.redo i:before {
  2121. content: "\\e831";
  2122. color: #616161;
  2123. }
  2124. .ojb_btn_popover.reverse i {
  2125. transform: rotate(180deg);
  2126. }
  2127.  
  2128. /*translateDiv样式*/
  2129. .translateDiv .topText {
  2130. display: flex;
  2131. margin-left: 5px;
  2132. color: #9e9e9e;
  2133. font-size: 13px;
  2134. align-items: center;
  2135. }
  2136. .translateDiv .borderlessButton{
  2137. display: flex;
  2138. align-items: center;
  2139. margin: 2.5px 7px;
  2140. fill: #9E9E9E;
  2141. }
  2142. .translateDiv .borderlessButton:hover{
  2143. cursor: pointer;
  2144. fill: #059669;
  2145. }
  2146. .translateDiv.bounce-in {
  2147. animation: bounce-in 1s forwards;
  2148. }
  2149. html:not([data-theme='dark']) .translateDiv {
  2150. box-shadow: 0px 0px 0.5px 0.5px #defdf378;
  2151. }
  2152. .translate-problem-statement {
  2153. justify-items: start;
  2154. letter-spacing: 1.8px;
  2155. color: #059669;
  2156. background-color: #f9f9fa;
  2157. border: 1px solid #c5ebdf;
  2158. border-radius: 0rem 0rem 0.3rem 0.3rem;
  2159. padding: 5px;
  2160. margin: -5px 0px 6px 0px;
  2161. width: 100%;
  2162. box-sizing: border-box;
  2163. font-size: 13px;
  2164. }
  2165. .translate-problem-statement h2 {
  2166. font-size: 1.6em;
  2167. font-weight: 700;
  2168. }
  2169. .translate-problem-statement h3 {
  2170. font-size: 1.3em;
  2171. font-weight: 700;
  2172. }
  2173. .translate-problem-statement-panel{
  2174. display: flex;
  2175. justify-content: space-between;
  2176. background-color: #f9f9fa;
  2177. border: 1px solid #c5ebdf;
  2178. border-radius: 0.3rem;
  2179. margin: 4px 0px;
  2180. }
  2181. .translate-problem-statement-panel .ojb_btn {
  2182. background: none;
  2183. border: none;
  2184. color: #9e9e9e;
  2185. }
  2186. .translate-problem-statement-panel.error, .translate-problem-statement.error {
  2187. color: red;
  2188. border-color: red;
  2189. }
  2190. .translate-problem-statement a, .translate-problem-statement a:link {
  2191. color: #10b981;
  2192. font-weight: 600;
  2193. background: 0 0;
  2194. text-decoration: none;
  2195. }
  2196. .translate-problem-statement ol, .translate-problem-statement ul {
  2197. display: grid;
  2198. margin-inline-start: 0.8em;
  2199. margin-block-start: 0em;
  2200. margin: 0.5em 0 0 3em;
  2201. padding-inline-start: 0px;
  2202. }
  2203. .translate-problem-statement li {
  2204. display: list-item;
  2205. height: auto;
  2206. word-wrap: break-word;
  2207. }
  2208. .translate-problem-statement ol li {
  2209. list-style-type: auto;
  2210. }
  2211. .translate-problem-statement ul li {
  2212. list-style-type: disc;
  2213. }
  2214. .translate-problem-statement img {
  2215. max-width: 100.0%;
  2216. max-height: 100.0%;
  2217. }
  2218. .ttypography .translate-problem-statement .MathJax {
  2219. color: #059669!important;
  2220. }
  2221. .translate-problem-statement span.math {
  2222. margin: 0px 2.5px !important;
  2223. }
  2224. .translate-problem-statement a:hover {
  2225. background-color: #800;
  2226. color: #fff;
  2227. text-decoration: none;
  2228. }
  2229. .translate-problem-statement table {
  2230. border: 1px #ccc solid !important;
  2231. margin: 1.5em 0 !important;
  2232. color: #059669 !important;
  2233. }
  2234. .translate-problem-statement table thead th {
  2235. border: 1px #ccc solid !important;
  2236. color: #059669 !important;
  2237. }
  2238. .translate-problem-statement table td {
  2239. border-right: 1px solid #ccc;
  2240. border-top: 1px solid #ccc;
  2241. padding: 0.7143em 0.5em;
  2242. }
  2243. .translate-problem-statement table th {
  2244. padding: 0.7143em 0.5em;
  2245. }
  2246. .translate-problem-statement p:not(:first-child) {
  2247. margin: 1.5em 0 0;
  2248. }
  2249. .translate-problem-statement p {
  2250. line-height: 20px !important;
  2251. word-wrap: break-word;
  2252. font-size: 13px !important
  2253. }
  2254. .problem-statement p:last-child {
  2255. margin-bottom: 0px !important;
  2256. }
  2257.  
  2258. /*设置按钮*/
  2259. header .enter-or-register-box, header .languages {
  2260. position: absolute;
  2261. right: 170px;
  2262. }
  2263. .ojb_btn.OJBetter_setting {
  2264. float: right;
  2265. height: 30px;
  2266. background: #60a5fa;
  2267. color: white;
  2268. margin: 10px;
  2269. border: 1px solid #60a5fa;
  2270. }
  2271. .ojb_btn.OJBetter_setting.open {
  2272. background-color: #e6e6e6;
  2273. color: #727378;
  2274. cursor: not-allowed;
  2275. }
  2276.  
  2277. /*设置面板*/
  2278. .OJBetter_setting_menu {
  2279. box-shadow: 0px 0px 0px 4px #ffffff;
  2280. position: fixed;
  2281. top: 50%;
  2282. left: 50%;
  2283. width: 600px;
  2284. min-height: 600px;
  2285. transform: translate(-50%, -50%);
  2286. border-radius: 6px;
  2287. background-color: #f0f4f9;
  2288. border-collapse: collapse;
  2289. border: 1px solid #ffffff;
  2290. color: #697e91;
  2291. font-family: var(--vp-font-family-base);
  2292. padding: 10px 20px 20px 10px;
  2293. box-sizing: content-box;
  2294. }
  2295. .OJBetter_setting_menu h3 {
  2296. margin-top: 10px;
  2297. font-size: 1.4em;
  2298. font-weight: 700;
  2299. }
  2300. .OJBetter_setting_menu h4 {
  2301. margin: 15px 0px 10px 0px;
  2302. }
  2303. .OJBetter_setting_menu h4,.OJBetter_setting_menu h5 {
  2304. font-weight: 600;
  2305. }
  2306. .OJBetter_setting_menu hr {
  2307. border: none;
  2308. height: 1px;
  2309. background-color: #ccc;
  2310. margin: 10px 0;
  2311. }
  2312. .OJBetter_setting_menu details {
  2313. padding: 10px;
  2314. margin-bottom: 5px;
  2315. background-color: #ffffff;
  2316. border-bottom: 1px solid #c9c6c696;
  2317. border-radius: 8px;
  2318. }
  2319. .OJBetter_setting_menu .badge {
  2320. border-radius: 4px;
  2321. border: 1px solid #009688;
  2322. color: #009688;
  2323. background-color: #fff;
  2324. padding: 0.5px 4px;
  2325. margin-left: 5px;
  2326. margin-right: auto;
  2327. line-height: initial;
  2328. font-weight: initial;
  2329. }
  2330. .OJBetter_setting_menu .missing {
  2331. box-shadow: inset 0px 0px 1px 1px red;
  2332. }
  2333. /* 页面切换 */
  2334. .OJBetter_setting_menu .settings-page {
  2335. display: none;
  2336. }
  2337. .OJBetter_setting_menu .settings-page.active {
  2338. display: block;
  2339. }
  2340. .OJBetter_setting_container {
  2341. display: flex;
  2342. }
  2343. .OJBetter_setting_sidebar {
  2344. flex: 0 0 auto;
  2345. min-width: 110px;
  2346. padding: 6px 10px 6px 6px;
  2347. margin: 20px 0px;
  2348. border-right: 1px solid #d4d8e9;
  2349. }
  2350. .OJBetter_setting_content {
  2351. flex-grow: 1;
  2352. margin: 20px 0px 0px 12px;
  2353. padding-right: 10px;
  2354. max-height: 580px;
  2355. overflow-y: auto;
  2356. box-sizing: border-box;
  2357. }
  2358. .OJBetter_setting_sidebar h3 {
  2359. margin-top: 0;
  2360. }
  2361. .OJBetter_setting_sidebar hr {
  2362. margin-top: 10px;
  2363. margin-bottom: 10px;
  2364. border: none;
  2365. border-top: 1px solid #DADCE0;
  2366. }
  2367. .OJBetter_setting_sidebar ul {
  2368. list-style-type: none;
  2369. margin: 0;
  2370. padding: 0;
  2371. }
  2372. .OJBetter_setting_sidebar li {
  2373. margin: 5px 0px;
  2374. background-color: #ffffff;
  2375. border: 1px solid #d4d8e9;
  2376. border-radius: 4px;
  2377. font-size: 16px;
  2378. }
  2379. .OJBetter_setting_sidebar li a {
  2380. text-decoration: none;
  2381. display: flex;
  2382. width: 100%;
  2383. font-size: 16px;
  2384. color: gray;
  2385. background-color: #ffffff;
  2386. border: none;
  2387. letter-spacing: 2px;
  2388. padding: 7px;
  2389. margin: 0px;
  2390. border-radius: 4px;
  2391. align-items: center;
  2392. -webkit-box-sizing: border-box;
  2393. -moz-box-sizing: border-box;
  2394. box-sizing: border-box;
  2395. }
  2396. .OJBetter_setting_sidebar li a.active {
  2397. background-color: #eceff1c7;
  2398. }
  2399. /* 链接样式 */
  2400. .OJBetter_setting_menu a {
  2401. font-size: 13px;
  2402. color: #009688;
  2403. background-color: #E0F2F1;
  2404. border: 1px solid #009688;
  2405. border-radius: 4px;
  2406. padding: 0px 5px;
  2407. margin: 0px 5px;
  2408. text-decoration: none;
  2409. }
  2410. /* 下拉选择框 */
  2411. .OJBetter_setting_menu select {
  2412. appearance: none;
  2413. padding: 5px 10px;
  2414. margin: -5px 0px;
  2415. border-radius: 6px;
  2416. border-style: solid;
  2417. border: 1px solid #ced4da;
  2418. color: #009688;
  2419. background: #ffffff;
  2420. font-size: 15px;
  2421. }
  2422. .OJBetter_setting_menu select:focus-visible {
  2423. outline: none;
  2424. }
  2425. .OJBetter_setting_menu select option:disabled {
  2426. color: #EEEEEE;
  2427. }
  2428. /* 数值输入框 */
  2429. .OJBetter_setting_menu input[type="number"] {
  2430. width: 40px;
  2431. color: #009688;
  2432. font-size: 15px;
  2433. appearance: none;
  2434. padding: 5px 10px;
  2435. margin: -5px 3px;
  2436. border-radius: 6px;
  2437. border-style: solid;
  2438. border: 1px solid #ced4da;
  2439. }
  2440. .OJBetter_setting_menu input[type="number"]:focus-visible {
  2441. outline: none;
  2442. }
  2443. .OJBetter_setting_menu input[type="number"]::-webkit-inner-spin-button,
  2444. .OJBetter_setting_menu input[type="number"]::-webkit-outer-spin-button {
  2445. -webkit-appearance: none;
  2446. margin: 0;
  2447. }
  2448. /*设置面板-滚动条*/
  2449. .OJBetter_setting_menu::-webkit-scrollbar, .OJBetter_setting_content::-webkit-scrollbar,
  2450. .OJBetter_modal .content::-webkit-scrollbar {
  2451. width: 5px;
  2452. height: 7px;
  2453. background-color: #aaa;
  2454. }
  2455. .OJBetter_setting_menu::-webkit-scrollbar-thumb, .OJBetter_setting_content::-webkit-scrollbar-thumb,
  2456. .OJBetter_modal .content::-webkit-scrollbar-thumb {
  2457. background-clip: padding-box;
  2458. background-color: #d7d9e4;
  2459. }
  2460. .OJBetter_setting_menu::-webkit-scrollbar-track, .OJBetter_setting_content::-webkit-scrollbar-track,
  2461. .OJBetter_modal .content::-webkit-scrollbar-track {
  2462. background-color: #f1f1f1;
  2463. }
  2464. /*设置面板-关闭按钮*/
  2465. .OJBetter_setting_menu .tool-box {
  2466. position: absolute;
  2467. width: 20px;
  2468. height: 20px;
  2469. top: 3px;
  2470. right: 3px;
  2471. }
  2472. .OJBetter_setting_menu .btn-close {
  2473. width: 20px;
  2474. height: 20px;
  2475. border-radius: 50%;
  2476. border: none;
  2477. margin: 0px;
  2478. padding: 0px;
  2479. background-color: #ff000080;
  2480. transition: .15s ease all;
  2481. box-sizing: border-box;
  2482. text-align: center;
  2483. color: transparent;
  2484. }
  2485. .OJBetter_setting_menu .iconfont {
  2486. font-size: 10px;
  2487. font-weight: bolder;
  2488. }
  2489. .OJBetter_setting_menu .btn-close:hover {
  2490. color: #ffffff;
  2491. background-color: #ff0000cc;
  2492. box-shadow: 0 5px 5px 0 #00000026;
  2493. }
  2494. .OJBetter_setting_menu .btn-close:active {
  2495. color: #ffffffde;
  2496. background-color: #ff000080;
  2497. }
  2498. /*设置面板-checkbox*/
  2499. .OJBetter_setting_menu input[type=checkbox]:focus {
  2500. outline: 0px;
  2501. }
  2502. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"] {
  2503. margin: 0px;
  2504. appearance: none;
  2505. -webkit-appearance: none;
  2506. width: 40px;
  2507. height: 20px;
  2508. border: 1.5px solid #D7CCC8;
  2509. padding: 0px !important;
  2510. border-radius: 20px;
  2511. background: #efebe978;
  2512. position: relative;
  2513. box-sizing: border-box;
  2514. }
  2515. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::before {
  2516. content: "";
  2517. width: 17px;
  2518. height: 17px;
  2519. background: #D7CCC8;
  2520. border: 1.5px solid #BCAAA4;
  2521. border-radius: 50%;
  2522. position: absolute;
  2523. top: 0;
  2524. left: 0;
  2525. transform: translate(2%, 2%);
  2526. transition: all 0.3s ease-in-out;
  2527. box-sizing: border-box;
  2528. }
  2529. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::after {
  2530. content: url("data:image/svg+xml,%3Csvg xmlns='://www.w3.org/2000/svg' width='23' height='23' viewBox='0 0 23 23' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.55021 5.84315L17.1568 16.4498L16.4497 17.1569L5.84311 6.55026L6.55021 5.84315Z' fill='%23EA0707' fill-opacity='0.89'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M17.1567 6.55021L6.55012 17.1568L5.84302 16.4497L16.4496 5.84311L17.1567 6.55021Z' fill='%23EA0707' fill-opacity='0.89'/%3E%3C/svg%3E");
  2531. position: absolute;
  2532. top: 0;
  2533. left: 24px;
  2534. }
  2535. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked {
  2536. border: 1.5px solid #C5CAE9;
  2537. background: #E8EAF6;
  2538. }
  2539. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::before {
  2540. background: #C5CAE9;
  2541. border: 1.5px solid #7986CB;
  2542. transform: translate(122%, 2%);
  2543. transition: all 0.3s ease-in-out;
  2544. }
  2545. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::after {
  2546. content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 15 13' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M14.8185 0.114533C15.0314 0.290403 15.0614 0.605559 14.8855 0.818454L5.00187 12.5L0.113036 6.81663C-0.0618274 6.60291 -0.0303263 6.2879 0.183396 6.11304C0.397119 5.93817 0.71213 5.96967 0.886994 6.18339L5.00187 11L14.1145 0.181573C14.2904 -0.0313222 14.6056 -0.0613371 14.8185 0.114533Z' fill='%2303A9F4' fill-opacity='0.9'/%3E%3C/svg%3E");
  2547. position: absolute;
  2548. top: 1.5px;
  2549. left: 4.5px;
  2550. }
  2551. .OJBetter_setting_menu .OJBetter_setting_list button {
  2552. cursor: pointer;
  2553. color: #7986cb;
  2554. background-color: #e8eaf6;
  2555. border: 1px solid #7986cb;
  2556. border-radius: 6px;
  2557. width: 100px;
  2558. margin: -5px 2px;
  2559. padding: 5px 10px;
  2560. }
  2561. .OJBetter_setting_menu .OJBetter_setting_list button:hover {
  2562. color: #e8eaf6;
  2563. background-color: #7986cb;
  2564. border: 1px solid #7986cb;
  2565. }
  2566. .OJBetter_setting_menu label, #darkMode_span, #loaded_span {
  2567. font-size: 16px;
  2568. }
  2569. .OJBetter_setting_list {
  2570. display: flex;
  2571. flex-wrap: wrap;
  2572. align-items: center;
  2573. padding: 10px;
  2574. margin: 5px 0px;
  2575. background-color: #ffffff;
  2576. border: 1px solid #c9c6c642;
  2577. border-bottom-color: #c9c6c696;
  2578. border-radius: 8px;
  2579. justify-content: space-between;
  2580. }
  2581. .OJBetter_setting_list.alert_danger {
  2582. color: #F44336;
  2583. background-color: #FFEBEE;
  2584. border: 1px solid #F44336;
  2585. margin: 10px 0px;
  2586. }
  2587. .OJBetter_setting_list.alert_warn {
  2588. color: #E65100;
  2589. background-color: #FFF3E0;
  2590. border: 1px solid #FF9800;
  2591. margin: 10px 0px;
  2592. }
  2593. .OJBetter_setting_list.alert_tip {
  2594. color: #009688;
  2595. background-color: #E0F2F1;
  2596. border: 1px solid #009688;
  2597. margin: 10px 0px;
  2598. }
  2599. .OJBetter_setting_list.alert_info {
  2600. color: #ffffff;
  2601. background-color: #009688;
  2602. margin: 10px 0px;
  2603. box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset;
  2604. }
  2605. .OJBetter_setting_list p:not(:last-child) {
  2606. margin-bottom: 10px;
  2607. }
  2608. .OJBetter_setting_list p:not(:first-child) {
  2609. margin-top: 10px;
  2610. }
  2611. /*设置面板-checkboxs*/
  2612. .OJBetter_setting_menu .OJBetter_checkboxs {
  2613. flex-basis: 100%;
  2614. display: flex;
  2615. padding: 8px;
  2616. margin: 10px 0px 0px 0px;
  2617. border-bottom: 1px solid #c9c6c696;
  2618. border-radius: 8px;
  2619. border: 1px solid #c5cae9;
  2620. background-color: #f0f8ff;
  2621. }
  2622. .OJBetter_setting_menu .OJBetter_checkboxs label {
  2623. font-size: 13px;
  2624. margin: 0px 6px 0px 3px;
  2625. }
  2626. .OJBetter_setting_menu .OJBetter_checkboxs input[type=checkbox]:checked+label{
  2627. color: #7986cb;
  2628. }
  2629. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"] {
  2630. border: none;
  2631. width: 16px;
  2632. height: 16px;
  2633. }
  2634. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before{
  2635. background: #ffffff;
  2636. transform: none;
  2637. width: 16px;
  2638. height: 16px;
  2639. }
  2640. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked {
  2641. background: none;
  2642. border: none;
  2643. }
  2644. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::before {
  2645. border: 1.5px solid #95a2de;
  2646. background: #e8eaf6;
  2647. transform: none;
  2648. }
  2649. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::after {
  2650. content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9' viewBox='0 0 15 13' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M14.8185 0.114533C15.0314 0.290403 15.0614 0.605559 14.8855 0.818454L5.00187 12.5L0.113036 6.81663C-0.0618274 6.60291 -0.0303263 6.2879 0.183396 6.11304C0.397119 5.93817 0.71213 5.96967 0.886994 6.18339L5.00187 11L14.1145 0.181573C14.2904 -0.0313222 14.6056 -0.0613371 14.8185 0.114533Z' fill='%2303A9F4' fill-opacity='0.9'/%3E%3C/svg%3E");
  2651. top: 0px;
  2652. left: 3.5px;
  2653. }
  2654. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:disabled+label {
  2655. color: #BDBDBD;
  2656. }
  2657. /*设置面板-radio*/
  2658. .OJBetter_setting_menu label {
  2659. display: block;
  2660. font-weight: initial;
  2661. list-style-type: none;
  2662. padding-inline-start: 0px;
  2663. overflow-x: auto;
  2664. max-width: 100%;
  2665. margin: 3px 0px;
  2666. overflow-x: visible;
  2667. }
  2668. .OJBetter_setting_menu_label_text {
  2669. display: flex;
  2670. border: 1px dashed #00aeeccc;
  2671. height: 35px;
  2672. width: 100%;
  2673. color: #6e6e6e;
  2674. font-weight: 300;
  2675. font-size: 14px;
  2676. letter-spacing: 2px;
  2677. padding: 7px;
  2678. margin-bottom: 4px;
  2679. align-items: center;
  2680. -webkit-box-sizing: border-box;
  2681. -moz-box-sizing: border-box;
  2682. box-sizing: border-box;
  2683. }
  2684. input[type="radio"]:checked+.OJBetter_setting_menu_label_text {
  2685. background: #41e49930;
  2686. border: 1px solid green;
  2687. color: green;
  2688. text-shadow: 0px 0px 0.5px green;
  2689. }
  2690. input[type="radio"]:disabled+.OJBetter_setting_menu_label_text {
  2691. background: #fafafa00;
  2692. border: 1px solid #e0e0e07a;
  2693. color: #e0e0e0;
  2694. }
  2695. .OJBetter_setting_menu label input[type="radio"], .OJBetter_contextmenu label input[type="radio"]{
  2696. appearance: none;
  2697. list-style: none;
  2698. padding: 0px !important;
  2699. margin: 0px;
  2700. clip: rect(0 0 0 0);
  2701. -webkit-clip-path: inset(100%);
  2702. clip-path: inset(100%);
  2703. height: 1px;
  2704. overflow: hidden;
  2705. position: absolute;
  2706. white-space: nowrap;
  2707. width: 1px;
  2708. }
  2709. /*设置面板-文本输入框*/
  2710. .OJBetter_setting_menu input[type="text"] {
  2711. display: block;
  2712. height: 25px !important;
  2713. width: 100%;
  2714. background-color: #ffffff;
  2715. color: #727378;
  2716. font-size: 12px;
  2717. border-radius: 0.3rem;
  2718. padding: 1px 5px !important;
  2719. box-sizing: border-box;
  2720. margin: 5px 0px 5px 0px;
  2721. border: 1px solid #00aeeccc;
  2722. box-shadow: 0 0 1px #0000004d;
  2723. }
  2724. .OJBetter_setting_menu .OJBetter_setting_list input[type="text"] {
  2725. margin-left: 5px;
  2726. }
  2727. .OJBetter_setting_menu input[type="text"]:focus-visible{
  2728. border-style: solid;
  2729. border-color: #3f51b5;
  2730. outline: none;
  2731. }
  2732. .OJBetter_setting_menu_config_box {
  2733. width: 100%;
  2734. display: grid;
  2735. margin-top: 5px;
  2736. -webkit-box-sizing: border-box;
  2737. -moz-box-sizing: border-box;
  2738. box-sizing: border-box;
  2739. }
  2740. .OJBetter_setting_menu input::placeholder {
  2741. color: #727378;
  2742. }
  2743. .OJBetter_setting_menu input.no_default::placeholder{
  2744. color: #BDBDBD;
  2745. }
  2746. .OJBetter_setting_menu input.is_null::placeholder{
  2747. color: red;
  2748. border-width: 1.5px;
  2749. }
  2750. .OJBetter_setting_menu input.is_null{
  2751. border-color: red;
  2752. }
  2753. .OJBetter_setting_menu textarea {
  2754. resize: vertical;
  2755. display: block;
  2756. width: 100%;
  2757. height: 60px;
  2758. background-color: #ffffff;
  2759. color: #727378;
  2760. font-size: 12px;
  2761. padding: 1px 5px !important;
  2762. box-sizing: border-box;
  2763. margin: 5px 0px 5px 0px;
  2764. border: 1px solid #00aeeccc;
  2765. box-shadow: 0 0 1px #0000004d;
  2766. }
  2767. .OJBetter_setting_menu textarea:focus-visible{
  2768. border-style: solid;
  2769. border-color: #3f51b5;
  2770. outline: none;
  2771. }
  2772. .OJBetter_setting_menu textarea::placeholder{
  2773. color: #BDBDBD;
  2774. font-size: 14px;
  2775. }
  2776. .OJBetter_setting_menu #tempConfig_save {
  2777. cursor: pointer;
  2778. display: inline-flex;
  2779. padding: 5px;
  2780. background-color: #1aa06d;
  2781. color: #ffffff;
  2782. font-size: 14px;
  2783. line-height: 1.5rem;
  2784. font-weight: 500;
  2785. justify-content: center;
  2786. width: 100%;
  2787. border-radius: 0.375rem;
  2788. border: none;
  2789. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  2790. margin-top: 20px
  2791. }
  2792. .OJBetter_setting_menu button#debug_button.debug_button {
  2793. width: 18%;
  2794. }
  2795. .OJBetter_setting_menu span.tip {
  2796. color: #999;
  2797. font-size: 12px;
  2798. font-weight: 500;
  2799. padding: 5px 0px;
  2800. }
  2801. /*设置面板-tip*/
  2802. .help_tip {
  2803. margin-right: auto;
  2804. }
  2805. span.input_label {
  2806. font-size: 14px;
  2807. }
  2808. .help_tip .tip_text {
  2809. display: none;
  2810. position: absolute;
  2811. color: #697e91;
  2812. font-weight: 400;
  2813. font-size: 14px;
  2814. letter-spacing: 0px;
  2815. background-color: #ffffff;
  2816. padding: 10px;
  2817. margin: 5px 0px;
  2818. border-radius: 4px;
  2819. border: 1px solid #e4e7ed;
  2820. box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);
  2821. z-index: 100;
  2822. }
  2823. .help_tip .tip_text p {
  2824. margin-bottom: 5px;
  2825. }
  2826. .help_tip .tip_text:before {
  2827. content: "";
  2828. position: absolute;
  2829. top: -20px;
  2830. right: -10px;
  2831. bottom: -10px;
  2832. left: -10px;
  2833. z-index: -1;
  2834. }
  2835. .help-icon {
  2836. cursor: help;
  2837. width: 15px;
  2838. color: #b4b9d4;
  2839. margin-left: 5px;
  2840. margin-top: 3px;
  2841. }
  2842. .OJBetter_setting_menu .OJBetter_setting_menu_label_text .help_tip .help-icon {
  2843. color: #7fbeb2;
  2844. }
  2845. .help_tip .help-icon:hover + .tip_text, .help_tip .tip_text:hover {
  2846. display: block;
  2847. cursor: help;
  2848. width: 250px;
  2849. }
  2850. /* 版本信息 */
  2851. .OJBetter_setting_menu .versionInfo{
  2852. display: grid;
  2853. justify-items: center;
  2854. font-size: 16px;
  2855. padding: 10px;
  2856. }
  2857. .OJBetter_setting_menu .versionInfo>* {
  2858. margin: 10px 0px;
  2859. }
  2860.  
  2861. /* 配置管理面板 */
  2862. .config{
  2863. width: 100%;
  2864. margin: 10px 0px;
  2865. }
  2866. .config li.tempConfig_add_button {
  2867. cursor: pointer;
  2868. height: 40px;
  2869. border: 1px dashed #BDBDBD;
  2870. border-radius: 8px;
  2871. background-color: #fcfbfb36;
  2872. color: #bdbdbd;
  2873. font-size: 14px;
  2874. align-items: center;
  2875. justify-content: center;
  2876. }
  2877. .config li.tempConfig_add_button:hover {
  2878. border: 1px dashed #03A9F4;
  2879. background-color: #d7f0fb8c;
  2880. color: #03A9F4;
  2881. }
  2882. .config .config_bar_list {
  2883. display: flex;
  2884. width: 100%;
  2885. padding-bottom: 2px;
  2886. border: 1px solid #c5cae9;
  2887. background-color: #f0f8ff;
  2888. box-sizing: border-box;
  2889. border-radius: 0px 0px 8px 8px;
  2890. }
  2891. .config .config_bar_list input[type="radio"] {
  2892. appearance: none;
  2893. width: 0;
  2894. height: 0;
  2895. overflow: hidden;
  2896. }
  2897. .config .config_bar_list input[type="radio"] {
  2898. margin: 0px;
  2899. }
  2900. .config .config_bar_list input[type=radio]:focus {
  2901. outline: 0px;
  2902. }
  2903. .config .config_bar_ul_li_text {
  2904. display: flex;
  2905. align-items: center;
  2906. justify-content: center;
  2907. max-width: 100%;
  2908. height: 40px;
  2909. overflow-x: auto;
  2910. font-size: 14px;
  2911. font-weight: 400;
  2912. margin: 0px 4px;
  2913. padding: 3px;
  2914. border: 1px solid #dedede;
  2915. border-radius: 10px;
  2916. box-shadow: 0px 2px 4px 0px rgba(0,0,0,.05);
  2917. box-sizing: border-box;
  2918. }
  2919. .config .config_bar_ul li button {
  2920. background-color: #e6e6e6;
  2921. color: #727378;
  2922. height: 23px;
  2923. font-size: 14px;
  2924. border-radius: 0.3rem;
  2925. padding: 1px 5px;
  2926. margin: 5px;
  2927. border: none;
  2928. box-shadow: 0 0 1px #0000004d;
  2929. }
  2930. .config .config_bar_ul {
  2931. display: flex;
  2932. align-items: center;
  2933. list-style-type: none;
  2934. padding-inline-start: 0px;
  2935. overflow-x: auto;
  2936. max-width: 100%;
  2937. margin: 0px;
  2938. padding: 5px;
  2939. }
  2940. .config .config_bar_ul li {
  2941. width: 80px;
  2942. display: grid;
  2943. margin: 4px 4px;
  2944. min-width: 100px;
  2945. box-sizing: border-box;
  2946. }
  2947. .config .config_bar_ul_li_text:hover {
  2948. background-color: #eae4dc24;
  2949. }
  2950. input[type="radio"]:checked + .config_bar_ul_li_text {
  2951. background: #41b3e430;
  2952. border: 1px solid #5e7ce0;
  2953. color: #5e7ce0;
  2954. }
  2955. .config .config_bar_ul::-webkit-scrollbar {
  2956. width: 5px;
  2957. height: 4px;
  2958. }
  2959. .config .config_bar_ul::-webkit-scrollbar-thumb {
  2960. background-clip: padding-box;
  2961. background-color: #d7d9e4;
  2962. border-radius: 8px;
  2963. }
  2964. .config .config_bar_ul::-webkit-scrollbar-button:start:decrement {
  2965. width: 4px;
  2966. background-color: transparent;
  2967. }
  2968. .config .config_bar_ul::-webkit-scrollbar-button:end:increment {
  2969. width: 4px;
  2970. background-color: transparent;
  2971. }
  2972. .config .config_bar_ul::-webkit-scrollbar-track {
  2973. border-radius: 5px;
  2974. }
  2975. .config .config_bar_ul_li_text::-webkit-scrollbar {
  2976. width: 5px;
  2977. height: 7px;
  2978. background-color: #aaa;
  2979. }
  2980. .config .config_bar_ul_li_text::-webkit-scrollbar-thumb {
  2981. background-clip: padding-box;
  2982. background-color: #d7d9e4;
  2983. }
  2984. .config .config_bar_ul_li_text::-webkit-scrollbar-track {
  2985. background-color: #f1f1f1;
  2986. }
  2987. .config .config_bar_list_add_div {
  2988. display: flex;
  2989. height: 40px;
  2990. margin: 4px 2px;
  2991. }
  2992.  
  2993. /* 修改菜单 */
  2994. #config_bar_menu {
  2995. z-index: 400;
  2996. position: fixed;
  2997. width: 60px;
  2998. background: #ffffff;
  2999. box-shadow: 1px 1px 4px 0px #0000004d;
  3000. border: 0px solid rgba(0,0,0,0.04);
  3001. border-radius: 4px;
  3002. padding: 8px 0;
  3003. }
  3004. .config_bar_menu_item {
  3005. cursor: pointer;
  3006. padding: 2px 6px;
  3007. display: flex;
  3008. justify-content: center;
  3009. align-items: center;
  3010. height: 32px;
  3011. font-size: 14px;
  3012. font-weight: 500;
  3013. box-shadow: inset 0px 0px 0px 0px #8bb2d9;
  3014. }
  3015. #config_bar_menu_edit:hover {
  3016. background-color: #00aeec;
  3017. color: white;
  3018. }
  3019. #config_bar_menu_delete:hover {
  3020. background-color: #FF5722;
  3021. color: white;
  3022. }
  3023.  
  3024. /* 配置编辑页面 */
  3025. #config_edit_menu {
  3026. z-index: 300;
  3027. width: 450px;
  3028. }
  3029.  
  3030. /* 黑暗模式选项按钮 */
  3031. .dark-mode-selection {
  3032. display: flex;
  3033. justify-content: center;
  3034. align-items: center;
  3035. max-width: 350px;
  3036. -webkit-user-select: none;
  3037. -moz-user-select: none;
  3038. -ms-user-select: none;
  3039. user-select: none;
  3040. }
  3041. .dark-mode-selection label {
  3042. margin: 8px 0px 8px 8px;
  3043. }
  3044. .dark-mode-selection > * {
  3045. margin: 6px;
  3046. }
  3047. .dark-mode-selection .OJBetter_setting_menu_label_text {
  3048. border-radius: 8px;
  3049. margin-bottom: 0px;
  3050. }
  3051.  
  3052. /*确认弹窗*/
  3053. .OJBetter_modal {
  3054. z-index: 600;
  3055. display: grid;
  3056. position: fixed;
  3057. top: 50%;
  3058. left: 50%;
  3059. transform: translate(-50%, -50%);
  3060. font-size: 12px;
  3061. font-family: var(--vp-font-family-base);
  3062. width: max-content;
  3063. padding: 10px 20px;
  3064. box-shadow: 0px 0px 0px 4px #ffffff;
  3065. border-radius: 6px;
  3066. background-color: #f0f4f9;
  3067. border-collapse: collapse;
  3068. border: 1px solid #ffffff;
  3069. color: #697e91;
  3070. }
  3071. .OJBetter_modal h2 {
  3072. font-size: 1.6em;
  3073. font-weight: 700;
  3074. }
  3075. .OJBetter_modal .content{
  3076. white-space: nowrap;
  3077. max-height: 500px;
  3078. overflow-y: auto;
  3079. }
  3080. .OJBetter_modal .buttons{
  3081. display: flex;
  3082. padding-top: 15px;
  3083. }
  3084. .OJBetter_modal button {
  3085. display: inline-flex;
  3086. justify-content: center;
  3087. align-items: center;
  3088. line-height: 1;
  3089. white-space: nowrap;
  3090. cursor: pointer;
  3091. text-align: center;
  3092. box-sizing: border-box;
  3093. outline: none;
  3094. transition: .1s;
  3095. user-select: none;
  3096. vertical-align: middle;
  3097. -webkit-appearance: none;
  3098. height: 24px;
  3099. padding: 5px 11px;
  3100. margin-right: 15px;
  3101. font-size: 12px;
  3102. border-radius: 4px;
  3103. color: #ffffff;
  3104. background: #009688;
  3105. border-color: #009688;
  3106. border: none;
  3107. }
  3108. .OJBetter_modal button.secondary{
  3109. background-color:#4DB6AC;
  3110. }
  3111. .OJBetter_modal button:hover{
  3112. background-color:#4DB6AC;
  3113. }
  3114. .OJBetter_modal button.secondary:hover {
  3115. background-color: #80CBC4;
  3116. }
  3117. .OJBetter_modal .help-icon {
  3118. margin: 0px 8px 0px 0px;
  3119. height: 1em;
  3120. width: 1em;
  3121. line-height: 1em;
  3122. display: inline-flex;
  3123. justify-content: center;
  3124. align-items: center;
  3125. position: relative;
  3126. fill: currentColor;
  3127. font-size: inherit;
  3128. }
  3129. .OJBetter_modal p {
  3130. margin: 5px 0px;
  3131. }
  3132.  
  3133. /* 右键菜单 */
  3134. .OJBetter_contextmenu {
  3135. z-index: 500;
  3136. display: grid;
  3137. position: absolute;
  3138. background-color: #f0f4f9;
  3139. border-collapse: collapse;
  3140. color: #697e91;
  3141. font-family: var(--vp-font-family-base);
  3142. overflow: hidden;
  3143. box-sizing: content-box;
  3144. box-shadow: 0px 0px 0px 2px #eddbdb4d;
  3145. }
  3146. .OJBetter_contextmenu label {
  3147. margin: 0px;
  3148. }
  3149. input[type="radio"]:checked+.OJBetter_contextmenu_label_text {
  3150. background: #41e49930;
  3151. border: 1px solid green;
  3152. color: green;
  3153. font-weight: 500;
  3154. }
  3155. .OJBetter_contextmenu_label_text {
  3156. display: flex;
  3157. border: 1px dashed #80cbc4;
  3158. height: 26px;
  3159. width: 100%;
  3160. color: gray;
  3161. font-size: 13px;
  3162. font-weight: initial;
  3163. padding: 4px;
  3164. align-items: center;
  3165. -webkit-box-sizing: border-box;
  3166. -moz-box-sizing: border-box;
  3167. box-sizing: border-box;
  3168. }
  3169. .OJBetter_contextmenu_label_text:hover {
  3170. color: #F44336;
  3171. border: 1px dashed #009688;
  3172. background-color: #ffebcd;
  3173. }
  3174.  
  3175. /* RatingByClist */
  3176. .ratingBadge, html[data-theme=dark] button.ratingBadge{
  3177. display: block;
  3178. font-weight: 700;
  3179. font-size: 11px;
  3180. margin-top: 5px;
  3181. padding: 2px;
  3182. border-radius: 4px;
  3183. color: #B0BEC5;
  3184. border: 1px solid #cccccc66;
  3185. }
  3186.  
  3187. /* 多选翻译 */
  3188. .block_selected{
  3189. box-shadow: 0px 0px 0px 1px #FF9800;
  3190. outline: none;
  3191. }
  3192.  
  3193. /* 悬浮菜单 */
  3194. .OJBetter_MiniTranslateButton {
  3195. z-index: 100;
  3196. display: grid;
  3197. position: absolute;
  3198. border-collapse: collapse;
  3199. fill: #F57C00;
  3200. background-color: #FFF3E0;
  3201. overflow: hidden;
  3202. box-sizing: content-box;
  3203. box-shadow: 0px 0px 0px 2px #FFE0B2;
  3204. border-radius: 100%;
  3205. }
  3206. .OJBetter_MiniTranslateButton:hover {
  3207. cursor: pointer;
  3208. box-shadow: 0px 0px 0px 2px #FFB74D;
  3209. }
  3210.  
  3211. /* acmsguru划分块 */
  3212. .OJBetter_acmsguru {
  3213. margin: 0 0 1em!important;
  3214. }
  3215.  
  3216. /* 代码提交表单 */
  3217. #OJBetter_SubmitForm.input-output-copier:hover {
  3218. background-color: #ffffff00;
  3219. }
  3220. #OJBetter_SubmitForm input[type="number"] {
  3221. width: 40px;
  3222. color: #009688;
  3223. appearance: none;
  3224. border-radius: 6px;
  3225. border-style: solid;
  3226. border: none;
  3227. background-color: #ffffff00;
  3228. }
  3229. #OJBetter_SubmitForm :focus-visible {
  3230. outline: none;
  3231. }
  3232. #OJBetter_SubmitForm .topDiv {
  3233. height: 50px;
  3234. display: flex;
  3235. align-items: center;
  3236. justify-content: space-between;
  3237. padding: 10px 0px;
  3238. box-sizing: border-box;
  3239. }
  3240. #OJBetter_SubmitForm .topDiv .topRightDiv {
  3241. height: 100%;
  3242. display: flex;
  3243. flex-wrap: wrap;
  3244. gap: 0px;
  3245. }
  3246. #OJBetter_SubmitForm input[type="checkbox"], #OJBetter_SubmitForm label {
  3247. margin: 0px;
  3248. font-weight: initial;
  3249. }
  3250. #OJBetter_SubmitForm #fontSizeInput {
  3251. border: none;
  3252. background-color: #ffffff00;
  3253. }
  3254.  
  3255. /* 顶部区域 */
  3256. #OJBetter_SubmitForm .topRightDiv>* {
  3257. height: 100%;
  3258. box-sizing: border-box;
  3259. }
  3260. #OJBetter_SubmitForm .topRightDiv>button{
  3261. padding: 0px 8px;
  3262. }
  3263. #OJBetter_SubmitForm .topRightDiv {
  3264. display: flex;
  3265. flex-wrap: wrap;
  3266. gap: 0px;
  3267. align-items: center;
  3268. }
  3269. #OJBetter_SubmitForm input[type="checkbox"], #OJBetter_SubmitForm label {
  3270. margin: 0px;
  3271. font-weight: initial;
  3272. }
  3273.  
  3274. /* LSP连接Log */
  3275. #LSPLog{
  3276. width: 500px;
  3277. height: 500px;
  3278. position: fixed;
  3279. top: 50%;
  3280. left: 50%;
  3281. padding: 10px;
  3282. transform: translate(-50%, -50%);
  3283. border: 1px solid;
  3284. z-index: 200;
  3285. background-color: #ffffff;
  3286. }
  3287. #LSPLog button{
  3288. position: fixed;
  3289. top: 10px;
  3290. right: 10px;
  3291. z-index: 200;
  3292. }
  3293. #LSPLog #LSPLogList{
  3294. width: 500px;
  3295. height: 500px;
  3296. overflow: auto;
  3297. color: #424242;
  3298. }
  3299. #LSPLog li:nth-child(odd){
  3300. background-color: #f5f5f5;
  3301. }
  3302. #LSPLog details{
  3303. padding: 2px;
  3304. }
  3305.  
  3306. /* 代码编辑器 */
  3307. #OJBetter_editor{
  3308. box-sizing: border-box;
  3309. height: 600px;
  3310. border: 1px solid #d3d3d3;
  3311. width: 100%;
  3312. resize: vertical;
  3313. display: flex;
  3314. flex-direction: column;
  3315. }
  3316. #OJBetter_editor.fullscreen{
  3317. position: fixed;
  3318. top: 0;
  3319. left: 0;
  3320. width: 100%;
  3321. height: 100vh;
  3322. z-index: 2000;
  3323. }
  3324. #OJBetter_editor.bottom{
  3325. position: fixed;
  3326. bottom: 0;
  3327. left: 0;
  3328. width: 100%;
  3329. height: 50vh;
  3330. z-index: 2000;
  3331. }
  3332. .ojb_btn.exit_button_bottom {
  3333. position: fixed;
  3334. bottom: 30px;
  3335. right: 15px;
  3336. z-index: 2000;
  3337. height: 28px;
  3338. }
  3339.  
  3340. /* monaco */
  3341. #OJBetter_monaco {
  3342. flex: 1;
  3343. min-height: 0;
  3344. width: 100%;
  3345. }
  3346. #OJBetter_monaco .highlight {
  3347. border: 1px solid #ffffff00;
  3348. background-color: #ffffff00!important
  3349. }
  3350. .monaco-hover hr {
  3351. margin: 4px -8px 4px !important;
  3352. }
  3353.  
  3354. /* 状态底栏 */
  3355. #OJBetter_statusBar{
  3356. height: 22px;
  3357. font-size: 12px;
  3358. color: #757575;
  3359. border: 1px solid #d3d3d3;
  3360. background-color: #f8f8f8;
  3361. padding: 3px;
  3362. box-sizing: border-box;
  3363. }
  3364.  
  3365. /* 提交 */
  3366. #OJBetter_submitDiv{
  3367. display: flex;
  3368. padding-top: 15px;
  3369. height: 50px;
  3370. box-sizing: border-box;
  3371. }
  3372. #OJBetter_submitDiv >* {
  3373. border-radius: 6px;
  3374. }
  3375. #OJBetter_submitDiv > button {
  3376. height: 100%;
  3377. aspect-ratio: 1 / 1;
  3378. }
  3379. #SubmitButton {
  3380. color: #fff;
  3381. background-color: #209978;
  3382. border-color: #17795E;
  3383. }
  3384. #SubmitButton:hover {
  3385. background-color: #17795e;
  3386. }
  3387. #SubmitButton.disabled {
  3388. background-color: red;
  3389. animation: shake 0.07s infinite alternate;
  3390. }
  3391. #programTypeId{
  3392. height: 100%;
  3393. padding: 5px 10px;
  3394. border-radius: 6px;
  3395. border-style: solid;
  3396. border: 1px solid #ced4da;
  3397. color: #212529;
  3398. }
  3399.  
  3400. /* 调试 */
  3401. .OJBetter_loding{
  3402. padding: 6px 0px 0px 5px;
  3403. height: 22px;
  3404. }
  3405. #CompilerArgsInput{
  3406. flex-grow: 1;
  3407. width: 100%;
  3408. height: 100%;
  3409. margin-bottom: 10px;
  3410. padding: 5px 10px;
  3411. border-radius: 6px;
  3412. box-sizing: border-box;
  3413. border: 1px solid #ccc;
  3414. box-shadow: inset 0px 1px 1px rgba(0,0,0,.075);
  3415. }
  3416. #CompilerArgsInput[disabled] {
  3417. cursor: not-allowed;
  3418. }
  3419. #CompilerSetting{
  3420. font-size: 14px;
  3421. margin-top: 10px;
  3422. display: none;
  3423. }
  3424. #CompilerSetting select, #CompilerSetting textarea{
  3425. padding: 4px 10px;
  3426. border-radius: 6px;
  3427. border-style: solid;
  3428. border: 1px solid #ced4da;
  3429. color: #212529;
  3430. }
  3431. #CompilerBox{
  3432. display: grid;
  3433. margin-top: 10px;
  3434. border: #d0d7de solid 1px;
  3435. border-radius: 6px;
  3436. }
  3437. #CompilerBox > * {
  3438. margin: 5px;
  3439. }
  3440.  
  3441. /* 自定义样例 */
  3442. #customTestBlock {
  3443. margin-top: 10px;
  3444. font-size: 14px;
  3445. color: #616161;
  3446. border: 1px solid #d3d3d3;
  3447. box-sizing: border-box;
  3448. position: relative;
  3449. }
  3450. #customTestBlock #customTests{
  3451. border-top: 1px solid #d3d3d3;
  3452. margin: 0px 0px 40px 0px;
  3453. }
  3454. #customTestBlock summary {
  3455. cursor: pointer;
  3456. padding: 10px;
  3457. }
  3458. #customTestBlock textarea {
  3459. resize: vertical;
  3460. }
  3461. .sampleDiv {
  3462. color: #727378;
  3463. background-color: #FAFAFA;
  3464. padding: 5px;
  3465. margin-bottom: 10px;
  3466. box-shadow: inset 0 0 1px #0000004d;
  3467. position: relative;
  3468. }
  3469. .dynamicTextarea {
  3470. width: 98%;
  3471. height: 120px;
  3472. margin: 10px 5px;
  3473. border: 1px solid #E0E0E0;
  3474. }
  3475. .deleteCustomTest {
  3476. cursor: pointer;
  3477. position: absolute;
  3478. top: 5px;
  3479. right: 5px;
  3480. display: flex;
  3481. fill: #9E9E9E;
  3482. padding: 2px 2px;
  3483. border-radius: 4px;
  3484. border: 1px solid #ffffff00;
  3485. background-color: #ffffff00;
  3486. align-items: center;
  3487. }
  3488. .deleteCustomTest:hover {
  3489. fill: #EF5350;
  3490. border: 1px solid #ef9a9a;
  3491. background-color: #FFEBEE;
  3492. }
  3493. #addCustomTest {
  3494. cursor: pointer;
  3495. position: absolute;
  3496. bottom: 5px;
  3497. right: 5px;
  3498. padding: 3px 10px;
  3499. color: #795548;
  3500. border: 1px solid #ccc;
  3501. border-radius: 4px;
  3502. background-color: #FAFAFA;
  3503. }
  3504. #addCustomTest:hover {
  3505. background-color: #f5f5f5;
  3506. }
  3507.  
  3508. /* 调试结果 */
  3509. #statePanel{
  3510. display: none;
  3511. padding: 5px;
  3512. margin-top: 10px;
  3513. border: 1px solid #ddd;
  3514. border-radius: 4px;
  3515. }
  3516. .test-case {
  3517. padding: 10px;
  3518. border: 1px solid #ddd;
  3519. border-radius: 4px;
  3520. }
  3521. .test-case:not(:first-child){
  3522. margin-top: 5px;
  3523. }
  3524. .test-case > * {
  3525. margin: 5px 0px;
  3526. }
  3527. .test-case > :first-child {
  3528. margin-top: 0px;
  3529. }
  3530. .test-case > :last-child {
  3531. margin-bottom: 0px;
  3532. }
  3533. .test-case-title, .test-case-status {
  3534. font-size: 16px;
  3535. display: inline;
  3536. }
  3537. .test-case-status{
  3538. margin-left: 5px;
  3539. }
  3540. .test-case-status.error{
  3541. color: red;
  3542. }
  3543. .test-case-status.success{
  3544. color: #449d44;
  3545. }
  3546. .test-case-judge {
  3547. font-size: 13px;
  3548. }
  3549.  
  3550. /* 差异对比 */
  3551. .output_diff {
  3552. color: #5d4037;
  3553. margin: 5px 0px;
  3554. display: grid;
  3555. border: 1px solid #bcaaa4;
  3556. font-size: 13px;
  3557. font-family: Consolas, "Lucida Console", "Andale Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
  3558. overflow: auto;
  3559. }
  3560. .output_diff .added {
  3561. background-color: #c8f7c5;
  3562. user-select: none;
  3563. }
  3564. .output_diff .removed {
  3565. background-color: #f7c5c5;
  3566. }
  3567. .output_diff .diffLine {
  3568. display: flex;
  3569. }
  3570. .output_diff .diffLine:nth-child(odd) {
  3571. background-color: #f5f5f5;
  3572. }
  3573. .lineNo {
  3574. display: flex;
  3575. align-items: center;
  3576. justify-content: center;
  3577. width: 17px;
  3578. color: #BDBDBD;
  3579. font-size: 10px;
  3580. border-right: 1px solid;
  3581. user-select: none;
  3582. }
  3583. .lineContent {
  3584. display: grid;
  3585. width: 100%;
  3586. }
  3587. .lineContent>span {
  3588. height: 16px;
  3589. padding-left: 3px;
  3590. }
  3591. .output_no_diff {
  3592. padding: 5px;
  3593. border: 1px solid #ddd;
  3594. }
  3595. .diff_note {
  3596. font-size: 10px;
  3597. }
  3598.  
  3599. /* 网站本地化替换规则标记 */
  3600. .markingTextReplaceRule{
  3601. color: #FFF3E0;
  3602. background-color: #FF9800;
  3603. }
  3604.  
  3605. /* SelectPage样式 */
  3606. .sp_input {
  3607. padding: 4px 6px px !important;
  3608. height: 20px !important;
  3609. min-height: 20px !important;
  3610. line-height: 20px !important;
  3611. }
  3612. div.sp_clear_btn {
  3613. padding: 0px !important;
  3614. }
  3615.  
  3616. /* 移动设备 */
  3617. @media (max-device-width: 450px) {
  3618. .ojb_btn{
  3619. height: 2em;
  3620. font-size: 1.2em;
  3621. }
  3622. .ojb_btn.OJBetter_setting{
  3623. height: 2.5em;
  3624. font-size: 1em;
  3625. }
  3626. .OJBetter_setting_menu{
  3627. width: 90%;
  3628. }
  3629. .OJBetter_setting_menu label, #darkMode_span, #loaded_span, .OJBetter_setting_menu_label_text,
  3630. .OJBetter_setting_sidebar li{
  3631. font-size: 1em;
  3632. }
  3633. .translate-problem-statement{
  3634. font-size: 1.2em;
  3635. }
  3636. .OJBetter_modal{
  3637. font-size: 1.5em;
  3638. }
  3639. .OJBetter_setting_list, .translate-problem-statement{
  3640. padding: 0.5em;
  3641. }
  3642. .OJBetter_setting_menu_label_text{
  3643. height: 2.5em;
  3644. padding: 0.5em;
  3645. }
  3646. #pagBar #jump-input, #pagBar #items-per-page, .OJBetter_modal button{
  3647. height: 2.5em;
  3648. font-size: 1em;
  3649. }
  3650. .translate-problem-statement p, .translate-problem-statement ul li{
  3651. line-height: 1.5em !important;
  3652. }
  3653. .OJBetter_contextmenu_label_text{
  3654. height: 3em;
  3655. font-size: 1em;
  3656. }
  3657. }
  3658.  
  3659. /* 覆盖网站原本的样式 */
  3660. #footer > div:nth-child(7) {
  3661. left: 0px !important;
  3662. }
  3663. `);
  3664.  
  3665. /**
  3666. * 添加一些依赖库和条件加载的css样式
  3667. */
  3668. function addDependencyStyles() {
  3669. GM_addStyle(GM_getResourceText("xtermcss"));
  3670. GM_addStyle(GM_getResourceText("selectpagecss"));
  3671. // 自定义图标大小
  3672. GM_addStyle(`
  3673. .iconfont {
  3674. font-size: ${OJBetter.preference.iconButtonSize}px;
  3675. }
  3676. `);
  3677. }
  3678.  
  3679. /**
  3680. * 添加包含i18n内容的css样式
  3681. */
  3682. function addI18nStyles() {
  3683. GM_addStyle(`
  3684. /* 加载鼠标悬浮覆盖层css */
  3685. .overlay::before {
  3686. content: '';
  3687. position: absolute;
  3688. top: 0;
  3689. left: 0;
  3690. width: 100%;
  3691. height: 100%;
  3692. background: repeating-linear-gradient(135deg, rgb(77 208 225 / 30%), rgb(77 208 225 / 30%) 30px, rgb(77 208 225 / 10%) 0px, rgb(77 208 225 / 10%) 55px);
  3693. z-index: 100;
  3694. }
  3695. .overlay::after {
  3696. content: '${i18next.t('targetArea', { ns: 'common' })}';
  3697. position: absolute;
  3698. top: 50%;
  3699. left: 50%;
  3700. transform: translate(-50%, -50%);
  3701. color: #00695C;
  3702. font-size: 16px;
  3703. font-weight: bold;
  3704. z-index: 100;
  3705. }
  3706.  
  3707. .config::before {
  3708. content: "${i18next.t('common.configManageTitle', { ns: 'settings' })}";
  3709. display: block;
  3710. height: 20px;
  3711. background-color: #f0f8ff;
  3712. border: 1px solid #c5cae9;
  3713. border-bottom: 0px;
  3714. line-height: 20px;
  3715. padding: 2px 10px;
  3716. border-radius: 8px 8px 0px 0px;
  3717. box-sizing: content-box;
  3718. }
  3719. .config.missing::before {
  3720. content: "${i18next.t('common.missing.radio', { ns: 'settings' })}";
  3721. background-color: #fef0f0;
  3722. color: #f56c6c;
  3723. border: 1px solid #fab6b6;
  3724. }
  3725. `);
  3726. }
  3727.  
  3728. // ------------------------------
  3729. // 一些工具类
  3730. // ------------------------------
  3731.  
  3732. /**
  3733. * 自定义错误类,以区分不同的错误类型
  3734. */
  3735. class OJB_GMError extends Error {
  3736. constructor(type, message, originalError) {
  3737. super(message);
  3738. this.name = 'GMError';
  3739. this.type = type;
  3740. this.stack = originalError.stack;
  3741. Object.assign(this, originalError);
  3742. }
  3743. }
  3744.  
  3745. /**
  3746. * 文本块替换/恢复类
  3747. */
  3748. class TextBlockReplacer {
  3749. constructor() {
  3750. /** @type {string[]} 匹配项 */
  3751. this.matches = [];
  3752. /** @type {Map<string, string>} 待还原项 */
  3753. this.replacements = new Map();
  3754. /** @type {Map<string, string>} 暂时未找到的待还原项 */
  3755. this.tempReplacements = new Map();
  3756. /** @type {string} 替换符号 */
  3757. this.replaceSymbol = OJBetter.translation.replaceSymbol;
  3758. }
  3759.  
  3760. /**
  3761. * 替换文本
  3762. * @param {string} text 原文本
  3763. * @param {RegExp} regex 匹配规则
  3764. * @returns {string} 替换后的文本
  3765. */
  3766. replace(text, regex) {
  3767. this.matches = text.match(regex) || [];
  3768. try {
  3769. for (let i = 0; i < this.matches.length; i++) {
  3770. const match = this.matches[i];
  3771. const id = OJB_getRandomNumber(8);
  3772. let replacement = '';
  3773. switch (this.replaceSymbol) {
  3774. case "1":
  3775. replacement = `【${id}】`;
  3776. break;
  3777. case "2":
  3778. replacement = `{${id}}`;
  3779. break;
  3780. case "3":
  3781. replacement = `[${id}]`;
  3782. break;
  3783. default:
  3784. replacement = `【${id}】`;
  3785. break;
  3786. }
  3787. text = text.replace(match, replacement);
  3788. this.replacements.set(id, match);
  3789. }
  3790. } catch (e) { }
  3791. return text;
  3792. }
  3793.  
  3794.  
  3795. /**
  3796. * 恢复替换的文本
  3797. * @param {string} text 还原前的文本
  3798. * @returns {string} 还原后的文本
  3799. */
  3800. recover(text) {
  3801. let textCopy = text;
  3802.  
  3803. /**
  3804. * 替换文本
  3805. * @param {string} replacement 替换的文本
  3806. * @param {string} regexPattern 匹配规则
  3807. * @returns {void}
  3808. */
  3809. const replaceText = (replacement, regexPattern) => {
  3810. const latexMatch = '(?<latex_block>\\$\\$(\\\\.|[^\\$])*?\\$\\$)|(?<latex_inline>\\$(\\\\.|[^\\$])*?\\$)|';
  3811. const regex = new RegExp(latexMatch + regexPattern, 'g');
  3812. textCopy = textCopy.replace(regex, (match, ...args) => {
  3813. // LaTeX中的不替换
  3814. const groups = args[args.length - 1]; // groups是replace方法的最后一个参数
  3815. if (groups.latex_block || groups.latex_inline) return match;
  3816. // 没有空格则加一个
  3817. const offset = args[args.length - 3]; // offset是replace方法的倒数第三个参数
  3818. let leftSpace = "", rightSpace = "";
  3819. if (!/\s/.test(textCopy[offset - 1])) leftSpace = " ";
  3820. if (!/\s/.test(textCopy[offset + match.length])) rightSpace = " ";
  3821. return leftSpace + replacement + rightSpace;
  3822. });
  3823. };
  3824.  
  3825. /**
  3826. * 尝试还原
  3827. * @param {string} replacement 替换的文本
  3828. * @param {string} id 替换的 id
  3829. * @returns {boolean} 是否替换成功
  3830. */
  3831. const tryRecover = (replacement, id) => {
  3832. // 尝试还原,如果还原成功,则从 replacements 中删除
  3833. const originalText = textCopy;
  3834. replaceText(replacement, `【\\s*${id}\\s*】|\\[\\s*${id}\\s*\\]|{\\s*${id}\\s*}`); // 替换符完整匹配(考虑了多出空格的情况)
  3835. replaceText(replacement, `【\\s*${id}(?![】\\d])|(?<![【\\d])${id}\\s*】|\\[\\s*${id}(?![\\]\\d])|(?<![\\[\\d])${id}\\s*\\]|{\\s*${id}(?![}\\d])|(?<![{\\d])${id}\\s*}`); // 替换符部分匹配
  3836.  
  3837. if (textCopy === originalText) {
  3838. // 如果文本没有变化,说明没有找到,加入到 tempReplacements
  3839. this.tempReplacements.set(id, replacement);
  3840. return false;
  3841. } else {
  3842. // 如果文本变化了,说明找到并成功替换,则删除
  3843. this.replacements.delete(id);
  3844. this.tempReplacements.delete(id);
  3845. return true;
  3846. }
  3847. }
  3848.  
  3849. // 处理 replacements 中的项
  3850. this.replacements.forEach((replacement, id) => {
  3851. tryRecover(replacement, id);
  3852. });
  3853.  
  3854. // 处理 tempReplacements 中的项
  3855. while (this.tempReplacements.size > 0) {
  3856. let found = false;
  3857. this.tempReplacements.forEach((replacement, id) => {
  3858. found = tryRecover(replacement, id) || found;
  3859. });
  3860. if (!found) break; // 如果这一轮没有找到任何项,终止循环
  3861. }
  3862.  
  3863. // 如果 tempReplacements 还有未找到的项
  3864. if (this.tempReplacements.size > 0) {
  3865. console.warn("There are still some replacements not found:", this.tempReplacements);
  3866. }
  3867.  
  3868. return textCopy;
  3869. }
  3870. }
  3871.  
  3872. // ------------------------------
  3873. // 一些工具函数
  3874. // ------------------------------
  3875.  
  3876. /**
  3877. * 格式化链接格式
  3878. * @param {string} url 链接字符串
  3879. * @returns {string} 清理后的链接字符串
  3880. */
  3881. function OJB_cleanLink(url) {
  3882. if (url === null || url === undefined) return "";
  3883.  
  3884. // 替换'http://'为'https://'
  3885. let cleanUrl = url.replace(/^http:\/\//i, 'https://');
  3886.  
  3887. // 移除末尾的斜杠
  3888. cleanUrl = cleanUrl.replace(/\/$/, '');
  3889.  
  3890. return cleanUrl;
  3891. }
  3892.  
  3893. /**
  3894. * 深度比较两个对象或数组是否完全相等。
  3895. * @param {any} a - 第一个比较对象。
  3896. * @param {any} b - 第二个比较对象。
  3897. * @returns {boolean} - 如果两个对象或数组深度相等,则返回true,否则返回false。
  3898. */
  3899. function OJB_deepEquals(a, b) {
  3900. if (a === b) return true;
  3901. if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false;
  3902. const keysA = Object.keys(a);
  3903. const keysB = Object.keys(b);
  3904. if (keysA.length !== keysB.length) return false;
  3905. for (let key of keysA) {
  3906. if (!b.hasOwnProperty(key)) return false;
  3907. if (!OJB_deepEquals(a[key], b[key])) return false;
  3908. }
  3909. return true;
  3910. }
  3911.  
  3912. /**
  3913. * 用于封装需要重试的异步函数
  3914. * @param {Function} task 需要封装的异步函数
  3915. * @param {Object} options 配置项
  3916. * @param {Number} options.maxRetries 重试次数,默认为 5
  3917. * @param {Number} options.retryInterval 重试时间间隔,默认为 0 毫秒
  3918. * @param {Function} options.errorHandler 错误处理函数,默认为抛出错误
  3919. * @param {...any} args task 函数的参数
  3920. * @returns {Promise} 返回 Promise
  3921. */
  3922. async function OJB_promiseRetryWrapper(task, {
  3923. maxRetries = 5,
  3924. retryInterval = 0,
  3925. errorHandler = (err) => { throw err }
  3926. } = {}, ...args) {
  3927. let attemptsLeft = maxRetries;
  3928. while (attemptsLeft--) {
  3929. try {
  3930. return await task(...args);
  3931. } catch (err) {
  3932. if (attemptsLeft <= 0) {
  3933. return errorHandler(err, maxRetries, attemptsLeft);
  3934. }
  3935. if (retryInterval > 0) {
  3936. await OJB_delay(retryInterval);
  3937. }
  3938. }
  3939. }
  3940. }
  3941.  
  3942. /**
  3943. * GM_xmlhttpRequest 的 Promise 封装
  3944. * @param {Object} options GM_xmlhttpRequest 的参数
  3945. * @param {Boolean} isStream 是否为流式请求
  3946. * @returns {Promise<OJB_GMError>} 返回 Promise
  3947. */
  3948. function OJB_GMRequest(options, isStream = false) {
  3949. return new Promise((resolve, reject) => {
  3950. GM_xmlhttpRequest({
  3951. ...options,
  3952. ...(isStream ? {
  3953. onloadstart: resolve
  3954. } : {
  3955. onload: resolve
  3956. }),
  3957. onerror: (error) => reject(new OJB_GMError('error', 'An error occurred during the request.', error)),
  3958. ontimeout: (error) => reject(new OJB_GMError('timeout', 'The request timed out.', error)),
  3959. onabort: (error) => reject(new OJB_GMError('abort', 'The request was aborted.', error)),
  3960. });
  3961. });
  3962. }
  3963.  
  3964. /**
  3965. * 获取cookie
  3966. * @param {string} name cookie名称
  3967. * @returns {string} cookie值
  3968. */
  3969. function OJB_getCookie(name) {
  3970. const cookies = document.cookie.split(";");
  3971. for (let i = 0; i < cookies.length; i++) {
  3972. const cookie = cookies[i].trim();
  3973. const [cookieName, cookieValue] = cookie.split("=");
  3974.  
  3975. if (cookieName === name) {
  3976. return decodeURIComponent(cookieValue);
  3977. }
  3978. }
  3979. return "";
  3980. }
  3981.  
  3982. /**
  3983. * 检查是否仍在同一浏览器会话中
  3984. * @param {string} sessionKey - 会话键名,用于标识会话
  3985. * @returns {boolean} - 如果在当前会话中之前已经设置过这个键,则返回true,否则返回false
  3986. */
  3987. function OJB_isSameBrowserSession(sessionKey) {
  3988. const fullCookieName = `OJB_Session_${sessionKey}`;
  3989. const sessionValue = OJB_getCookie(fullCookieName);
  3990. if (sessionValue === "") {
  3991. document.cookie = `${fullCookieName}=true; path=/`;
  3992. return false;
  3993. }
  3994. return true;
  3995. }
  3996.  
  3997. /**
  3998. * 随机数生成
  3999. * @param {number} numDigits 位数
  4000. * @returns {number}
  4001. */
  4002. function OJB_getRandomNumber(numDigits) {
  4003. let min = Math.pow(10, numDigits - 1);
  4004. let max = Math.pow(10, numDigits) - 1;
  4005. return Math.floor(Math.random() * (max - min + 1)) + min;
  4006. }
  4007.  
  4008. /**
  4009. * 防抖函数
  4010. * @param {Function} callback 回调函数
  4011. * @returns {Function}
  4012. */
  4013. function OJB_debounce(callback) {
  4014. let timer;
  4015. let immediateExecuted = false;
  4016. const delay = 500;
  4017. return function () {
  4018. clearTimeout(timer);
  4019. if (!immediateExecuted) { callback.call(this); immediateExecuted = true; }
  4020. timer = setTimeout(() => { immediateExecuted = false; }, delay);
  4021. };
  4022. }
  4023.  
  4024. /**
  4025. * 为元素添加鼠标拖拽支持
  4026. * @param {JQuery<HTMLElement>} element 要添加拖拽支持的元素
  4027. * @returns {void}
  4028. */
  4029. function OJB_addDraggable(element) {
  4030. let isDragging = false;
  4031. let x, y, l, t, nl, nt;
  4032. let isSpecialMouseDown = false; // 选取某些元素时不拖动
  4033.  
  4034. element.on('mousedown', function (e) {
  4035. isSpecialMouseDown = $(e.target).is('label, p, input, textarea, span, select, details, summary');
  4036. if (isSpecialMouseDown) return;
  4037.  
  4038. isDragging = true;
  4039. x = e.clientX;
  4040. y = e.clientY;
  4041. l = element.offset().left - $(window).scrollLeft();
  4042. t = element.offset().top - $(window).scrollTop();
  4043.  
  4044. element.css({ left: l + 'px', top: t + 'px', transform: 'none' });
  4045.  
  4046. $(document).on("mousemove", drag);
  4047. $(document).on("mouseup", stopDrag);
  4048. element.css('cursor', 'all-scroll');
  4049. });
  4050.  
  4051. const drag = (e) => {
  4052. if (!isDragging) return;
  4053. // 不执行拖动操作
  4054. if ($(e.target).is('label, p, input, textarea, span') || isSpecialMouseDown && !$(e.target).is('input, textarea')) return;
  4055. e.preventDefault();
  4056.  
  4057. const nx = e.clientX;
  4058. const ny = e.clientY;
  4059. nl = nx - (x - l);
  4060. nt = ny - (y - t);
  4061. element.css({ transform: `translate(${nx - x}px, ${ny - y}px)` });
  4062. };
  4063.  
  4064. const stopDrag = () => {
  4065. isDragging = false;
  4066. isSpecialMouseDown = false;
  4067. element.css('cursor', 'default');
  4068.  
  4069. // 在停止拖拽后,设置元素的left和top,并还原transform
  4070. element.css({ left: nl + 'px', top: nt + 'px', transform: 'none' });
  4071. $(document).off("mousemove", drag);
  4072. $(document).off("mouseup", stopDrag);
  4073. };
  4074. }
  4075.  
  4076. /**
  4077. * 切换元素的折叠/展开过渡动画
  4078. * @param {HTMLElement} element
  4079. */
  4080. function OJB_toggleCollapseExpand(element) {
  4081. // 设置transitionend事件监听器的函数
  4082. const setTransitionListener = (listener) => {
  4083. const listenerName = `transitionEndListener${Date.now()}`;
  4084. window[listenerName] = listener;
  4085. element.addEventListener('transitionend', listener);
  4086. element.setAttribute('data-transition-end-listener', listenerName);
  4087. };
  4088.  
  4089. // 移除事件监听器的函数
  4090. const removeTransitionListener = () => {
  4091. const transitionEndListenerName = element.getAttribute('data-transition-end-listener');
  4092. if (transitionEndListenerName) {
  4093. element.removeEventListener('transitionend', window[transitionEndListenerName]);
  4094. element.removeAttribute('data-transition-end-listener');
  4095. }
  4096. };
  4097.  
  4098. const collapsed = element.getAttribute('data-collapsed') === 'true';
  4099. const sectionHeight = element.scrollHeight;
  4100.  
  4101. // 移除事件监听器
  4102. removeTransitionListener();
  4103.  
  4104. // 设置初始样式
  4105. element.style.overflow = 'hidden';
  4106. element.style.transition = 'height 0.3s ease-out 0s';
  4107. element.style.height = collapsed ? `0px` : `${sectionHeight}px`;
  4108. element.style.opacity = collapsed ? '' : '1';
  4109.  
  4110. // 需要立即开始动画
  4111. requestAnimationFrame(() => {
  4112. // 设置结束样式
  4113. element.style.height = collapsed ? `${sectionHeight}px` : `0px`;
  4114. });
  4115.  
  4116. const transitionEndListener = (event) => {
  4117. if (event.propertyName === 'height') {
  4118. if (collapsed) {
  4119. // 展开后的设置
  4120. element.style.height = '';
  4121. element.style.overflow = '';
  4122. } else {
  4123. // 折叠后的设置
  4124. element.style.opacity = '0';
  4125. }
  4126. removeTransitionListener();
  4127. }
  4128. };
  4129.  
  4130. setTransitionListener(transitionEndListener);
  4131.  
  4132. // 更新data-collapsed属性
  4133. element.setAttribute('data-collapsed', collapsed ? 'false' : 'true');
  4134. }
  4135.  
  4136. /**
  4137. * 获取外部JSON并转换为Object
  4138. * @param {string} url JSON Url
  4139. * @param {boolean} [nacache=true] 是否不使用缓存
  4140. * @returns {Promise<Object>} JSON Object
  4141. */
  4142. async function OJB_getExternalJSON(url, nacache = true) {
  4143. const response = await OJB_GMRequest({
  4144. method: "GET",
  4145. url: url,
  4146. nocache: nacache
  4147. });
  4148. try {
  4149. return JSON.parse(response.responseText);
  4150. } catch (e) {
  4151. throw new Error(`JSON parse error\n${e}`);
  4152. }
  4153. }
  4154.  
  4155. /**
  4156. * 创建确认对话框dialog
  4157. * @param {string} title 标题
  4158. * @param {string} content 内容
  4159. * @param {string[]} buttons 按钮 (取消 确定) 可以为null
  4160. * @param {boolean} renderMarkdown 是否使用markdown渲染文本
  4161. * @returns {Promise<boolean>} 用户点击了确定按钮返回true, 否则返回false
  4162. */
  4163. function OJB_createDialog(title, content, buttons, renderMarkdown = false) {
  4164. return new Promise(resolve => {
  4165. let contentHtml = content;
  4166.  
  4167. if (renderMarkdown) {
  4168. const md = window.markdownit();
  4169. contentHtml = md.render(content);
  4170. }
  4171.  
  4172. const dialog = OJB_safeCreateJQElement(`
  4173. <dialog class="OJBetter_modal">
  4174. <h2>${title}</h2>
  4175. <div class="content">${contentHtml}</div>
  4176. </dialog>
  4177. `);
  4178. const buttonbox = OJB_safeCreateJQElement(`<div class="buttons"></div>`);
  4179. const cancelButton = OJB_safeCreateJQElement(`<button class="cancelButton">${buttons[0]}</button>`)
  4180. .addClass("secondary");
  4181. const continueButton = OJB_safeCreateJQElement(`<button class="continueButton">${buttons[1]}</button>`);
  4182. if (buttons[0] !== null) buttonbox.append(cancelButton);
  4183. if (buttons[1] !== null) buttonbox.append(continueButton);
  4184. dialog.append(buttonbox);
  4185. $('body').before(dialog);
  4186.  
  4187. OJB_showModal(dialog);
  4188. OJB_addDraggable(dialog);
  4189.  
  4190. continueButton.click(function () {
  4191. OJB_closeAndRemoveModal(dialog);
  4192. resolve(true);
  4193. });
  4194.  
  4195. cancelButton.click(function () {
  4196. OJB_closeAndRemoveModal(dialog);
  4197. resolve(false);
  4198. });
  4199. });
  4200. }
  4201.  
  4202. /**
  4203. * 显示模态对话框并阻止页面滚动,同时考虑滚动条宽度变化和原始marginRight
  4204. * @param {JQuery<HTMLElement>} element
  4205. */
  4206. function OJB_showModal(element) {
  4207. const dialog = element.get(0);
  4208. dialog.showModal();
  4209. OJBetter.state.openDialogCount++;
  4210.  
  4211. if (OJBetter.state.openDialogCount === 1) {
  4212. const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
  4213. // 获取原始的html marginRight,考虑到可能的非数字值,比如auto
  4214. const originalMarginRight = window.getComputedStyle(document.documentElement).marginRight;
  4215. const marginRightValue = parseFloat(originalMarginRight) || 0; // 将非数字值转换为0
  4216.  
  4217. if (scrollbarWidth > 0) {
  4218. // 保存原始的marginRight,并设置新的值以补偿滚动条宽度
  4219. document.documentElement.style.setProperty('--original-margin-right', originalMarginRight);
  4220. document.documentElement.style.marginRight = `${marginRightValue + scrollbarWidth}px`;
  4221. }
  4222.  
  4223. // 保存原始的overflow样式
  4224. document.documentElement.setAttribute('data-original-overflow', document.documentElement.style.overflow);
  4225. document.documentElement.style.overflow = 'hidden';
  4226. }
  4227.  
  4228. const allowScrollIfNeeded = () => {
  4229. OJBetter.state.openDialogCount--;
  4230. if (OJBetter.state.openDialogCount === 0) {
  4231. // 恢复原始的html marginRight和overflow样式
  4232. const originalMarginRight = document.documentElement.style.getPropertyValue('--original-margin-right');
  4233. document.documentElement.style.marginRight = originalMarginRight;
  4234. document.documentElement.style.removeProperty('--original-margin-right');
  4235.  
  4236. const originalOverflow = document.documentElement.getAttribute('data-original-overflow');
  4237. document.documentElement.style.overflow = originalOverflow;
  4238. document.documentElement.removeAttribute('data-original-overflow');
  4239. }
  4240. };
  4241.  
  4242. dialog.addEventListener('close', allowScrollIfNeeded);
  4243. }
  4244.  
  4245. /**
  4246. * 关闭并移除模态对话框
  4247. * @param {JQuery<HTMLElement>} element
  4248. */
  4249. function OJB_closeAndRemoveModal(element) {
  4250. const dialog = element.get(0);
  4251. dialog.close();
  4252. dialog.remove();
  4253. }
  4254.  
  4255. /**
  4256. * 关闭并移除模态对话框
  4257. * @param {JQuery<HTMLElement>} element
  4258. */
  4259. function OJB_closeModal(element) {
  4260. const dialog = element.get(0);
  4261. dialog.close();
  4262. }
  4263.  
  4264. /**
  4265. * 清除i18next的缓存数据并刷新
  4266. */
  4267. function clearI18nextCache() {
  4268. Object.keys(localStorage)
  4269. .filter(key => key.startsWith('i18next_res_'))
  4270. .forEach(key => localStorage.removeItem(key));
  4271. window.location.reload();
  4272. }
  4273.  
  4274. /**
  4275. * 清除网站本地化数据
  4276. */
  4277. async function clearWebsiteL10nData() {
  4278. OJBetter.common.database.localizeSubsData.clear().then(() => {
  4279. console.log('localizeSubsData table has been cleared');
  4280. window.location.reload();
  4281. }).catch((error) => {
  4282. console.error('Failed to clear localizeSubsData table:', error);
  4283. });
  4284. }
  4285.  
  4286. /**
  4287. * 从Pre代码块中获取原始代码
  4288. * @param {HTMLElement} element pre代码块元素
  4289. * @returns {string|null} 代码文本
  4290. */
  4291. function OJB_getCodeFromPre(element) {
  4292. /**
  4293. * 从Ace格式化的代码块中获取原始代码
  4294. * @param {HTMLElement} element pre代码块元素
  4295. * @returns {string} 代码文本
  4296. */
  4297. const getCodeFromAcePre = function (element) {
  4298. const editor = ace.edit(element);
  4299. return editor.getValue();
  4300. }
  4301.  
  4302. /**
  4303. * 从Pretty格式化的代码块中获取原始代码-1
  4304. * 代码直接存放在 pre 元素中
  4305. * @param {HTMLElement} element pre代码块元素
  4306. * @returns {string} 代码文本
  4307. */
  4308. const getCodeFromPrettyPre = function (element) {
  4309. return Array.from(element.querySelectorAll('li')).map(function (li) {
  4310. return li.textContent;
  4311. }).join('\n');
  4312. }
  4313.  
  4314. /**
  4315. * 从Pretty格式化的代码块中获取原始代码-2
  4316. * 代码存放在子元素 code 中
  4317. * @param {HTMLElement} element pre代码块元素
  4318. * @returns {string} 代码文本
  4319. */
  4320. const getCodeFromPreChild = function (element) {
  4321. const code = element.querySelector("code.prettyprint");
  4322. if (code.classList.contains("linenums")) {
  4323. return getCodeFromPrettyPre(element);
  4324. } else {
  4325. return element.querySelector("code.prettyprint").textContent;
  4326. }
  4327. }
  4328.  
  4329. if (element.id === "submission-code") {
  4330. return getCodeFromAcePre(element);
  4331. } else if (element.classList.contains("prettyprint")) {
  4332. return getCodeFromPrettyPre(element);
  4333. } else if (element.querySelector("code.prettyprint")) {
  4334. return getCodeFromPreChild(element);
  4335. } else {
  4336. return null;
  4337. }
  4338. }
  4339.  
  4340. /**
  4341. * 判断代码的语言
  4342. * @param {string} code 代码文本
  4343. * @returns {string} 可能的语言
  4344. */
  4345. function OJB_codeLangDetect(code) {
  4346. result = hljs.highlightAuto(code);
  4347. return result.language;
  4348. }
  4349.  
  4350. /**
  4351. * 获取指定命名空间下的所有i18n翻译键值对。
  4352. *
  4353. * @param {string} namespace - 要获取键值对的i18next命名空间。
  4354. * @returns {Map<string, string>} 一个包含命名空间下所有键值对的Map对象。
  4355. */
  4356. function OJB_getAllI18nKeysForNamespace(namespace) {
  4357. const language = i18next.language; // 获取当前语言
  4358. const resources = i18next.store.data[language]; // 获取当前语言的所有资源
  4359. const nsResources = resources[namespace]; // 获取特定命名空间的资源
  4360. const resultMap = new Map();
  4361.  
  4362. if (nsResources) {
  4363. // 遍历命名空间下的所有键值对,并添加到Map中
  4364. Object.keys(nsResources).forEach(key => {
  4365. resultMap.set(key, nsResources[key]);
  4366. });
  4367. } else {
  4368. console.log(`No resources found for namespace "${namespace}"`);
  4369. }
  4370.  
  4371. return resultMap;
  4372. }
  4373.  
  4374. /**
  4375. * 更新检查
  4376. */
  4377. async function checkScriptVersion() {
  4378. try {
  4379. const versionResponse = await OJB_GMRequest({
  4380. method: "GET",
  4381. url: "https://aowuucdn.oss-accelerate.aliyuncs.com/script/versions.json",
  4382. timeout: 10 * 1e3,
  4383. nocache: true
  4384. });
  4385. const versionData = JSON.parse(versionResponse.responseText);
  4386. const { [OJBetter.state.formatName]: { dev: version_dev, release: version_release } } = versionData;
  4387. const baseUrls = {
  4388. greasyfork: 'https://update.greasyfork.org/scripts/465777/Codeforces%20Better%21.user.js',
  4389. github: `https://github.com/beijixiaohu/OJBetter/raw/main/script/${OJBetter.about.updateChannel}/${OJBetter.state.formatName}.user.js`,
  4390. aliyunoss: `https://aowuucdn.oss-accelerate.aliyuncs.com/script/${OJBetter.about.updateChannel}/${OJBetter.state.formatName}.user.js`
  4391. };
  4392. /** @type {string} 更新跳转url */
  4393. const updateUrl = baseUrls[OJBetter.about.updateSource];
  4394. /** @type {string} 是否暂时跳过cookie */
  4395. const skipUpdate = OJB_getCookie("skipUpdate");
  4396. /** @type {string} 当前更新频道的最新版本 */
  4397. const version = OJBetter.about.updateChannel == "release" ? version_release : version_dev;
  4398. if (OJB_compareVersions(version, OJBetter.state.version) === 1 && skipUpdate !== "true") {
  4399. const updateConfirmed = await OJB_createDialog(
  4400. i18next.t('update.title', { ns: 'dialog', scriptName: OJBetter.state.name }),
  4401. i18next.t('update.content', { ns: 'dialog', oldVersion: OJBetter.state.version, newVersion: version }),
  4402. [
  4403. i18next.t('update.buttons.0', { ns: 'dialog' }),
  4404. i18next.t('update.buttons.1', { ns: 'dialog' })
  4405. ],
  4406. true
  4407. );
  4408.  
  4409. if (updateConfirmed) {
  4410. window.location.href = updateUrl;
  4411. } else {
  4412. document.cookie = "skipUpdate=true; path=/";
  4413. }
  4414. }
  4415. } catch (error) {
  4416. console.error("Update check failed: ", error);
  4417. }
  4418. }
  4419.  
  4420. /**
  4421. * 公告
  4422. */
  4423. async function showAnnounce() {
  4424. /** @type {string} 最新公告版本*/
  4425. const lastAnnounceVer = i18next.t('lastVersion', { ns: 'announce' });
  4426. if (OJB_compareVersions(OJBetter.state.version, OJBetter.state.lastAnnounceVer) === 1) {
  4427. const title = `🎉${i18next.t('announce.title', { ns: 'dialog' })} ${OJBetter.state.version}`;
  4428. /** @type {Boolean} 是否是新的公告 */
  4429. const isNewAnnounceVer = OJB_compareVersions(lastAnnounceVer, OJBetter.state.lastReadAnnounceVer) === 1;
  4430. /** @type {Boolean} 是否展示新的公告(高于当前版本的测试公告不展示) */
  4431. const showNewAnnounceVer = OJB_compareVersions(lastAnnounceVer, OJBetter.state.version) !== 1;
  4432. /**
  4433. * 获取公告的内容
  4434. * @returns {string} 公告内容
  4435. */
  4436. const getAnnounceContent = function () {
  4437. // 获取公告
  4438. const announceMap = OJB_getAllI18nKeysForNamespace('announce');
  4439. // 移除 'lastVersion' 键
  4440. announceMap.delete('lastVersion');
  4441. // 将 Map 转换为数组并根据版本号排序
  4442. const sortedVersions = [...announceMap.keys()].sort(OJB_compareVersions).reverse();
  4443. let content = "";
  4444. sortedVersions.forEach(version => {
  4445. content += `### ${version}\n\n`; // 使用版本号作为标题
  4446. content += announceMap.get(version); // 添加对应版本的公告内容
  4447. content += "\n\n";
  4448. });
  4449.  
  4450. return content;
  4451. };
  4452.  
  4453. const content = (() => {
  4454. if (isNewAnnounceVer && showNewAnnounceVer) {
  4455. return `${i18next.t('announce.prefix', { ns: 'dialog' })}\n\n${getAnnounceContent()}`;
  4456. } else {
  4457. return i18next.t('announce.divContent', { ns: 'dialog' });
  4458. }
  4459. })();
  4460. const ok = await OJB_createDialog(
  4461. title,
  4462. content,
  4463. [
  4464. null,
  4465. i18next.t('announce.buttons.0', { ns: 'dialog' })
  4466. ],
  4467. true
  4468. ); //跳过折叠块确认
  4469. if (ok) {
  4470. if (isNewAnnounceVer && showNewAnnounceVer) {
  4471. GM_setValue('lastReadAnnounceVer', lastAnnounceVer);
  4472. }
  4473. GM_setValue('lastAnnounceVer', OJBetter.state.version);
  4474. }
  4475. }
  4476. };
  4477.  
  4478. /**
  4479. * 页面顶部提示信息alert类
  4480. */
  4481. class LoadingMessage {
  4482. constructor() {
  4483. this._statusElement = null;
  4484. this._isDisplayed = false;
  4485. this.init();
  4486. }
  4487.  
  4488. /**
  4489. * 初始化加载提示信息
  4490. */
  4491. init() {
  4492. this._statusElement = this.createStatusElement();
  4493. this.insertStatusElement();
  4494. }
  4495.  
  4496. /**
  4497. * 创建提示信息元素
  4498. */
  4499. createStatusElement() {
  4500. const statusElement = $("<div></div>").addClass("alert OJBetter_alert")
  4501. .css({
  4502. "margin": "1em",
  4503. "text-align": "center",
  4504. "position": "relative"
  4505. }).hide();
  4506. return statusElement;
  4507. }
  4508.  
  4509. /**
  4510. * 插入提示信息
  4511. * @returns {void}
  4512. */
  4513. insertStatusElement() {
  4514. (OJBetter.typeOfPage.is_mSite ? $("header") : $(".menu-box:first").next()).after(this._statusElement);
  4515. }
  4516.  
  4517. /**
  4518. * 显示提示信息
  4519. */
  4520. showStatus() {
  4521. this._statusElement.show();
  4522. this._isDisplayed = true;
  4523. }
  4524.  
  4525. /**
  4526. * 隐藏提示信息
  4527. */
  4528. hideStatus() {
  4529. this._statusElement.fadeOut(500);
  4530. this._isDisplayed = false;
  4531. }
  4532.  
  4533. /**
  4534. * 移除提示信息
  4535. */
  4536. removeStatus() {
  4537. this._statusElement.remove();
  4538. this._isDisplayed = false;
  4539. }
  4540.  
  4541. /**
  4542. * 更新提示信息
  4543. * @param {string} text 提示信息文本
  4544. * @param {string} type 提示信息类型,可选值:info, success, warning, error
  4545. * @param {number} timeout 提示信息显示的持续时间(毫秒), 默认为无限长
  4546. */
  4547. updateStatus(text, type = 'info', timeout = Infinity, isMarkdown = false) {
  4548. if (isMarkdown) {
  4549. let md = window.markdownit({
  4550. html: !is_escapeHTML,
  4551. });
  4552. text = md.render(text);
  4553. }
  4554. this._statusElement.html(text).removeClass("alert-info alert-success alert-warning alert-error").addClass(`alert-${type}`);
  4555. if (!this._isDisplayed) {
  4556. this.showStatus();
  4557. }
  4558. if (timeout !== Infinity) {
  4559. setTimeout(() => {
  4560. this.hideStatus();
  4561. }, timeout);
  4562. }
  4563. }
  4564. }
  4565.  
  4566. /**
  4567. * 获取网站本地化的数据
  4568. * @param {*} localizationLanguage 本地化语言
  4569. * @returns {Promise<Object>} 本地化数据
  4570. */
  4571. async function getLocalizeWebsiteJson(localizationLanguage) {
  4572. let data = await OJBetter.common.database.localizeSubsData.get(localizationLanguage);
  4573. let url = localizationLanguage === "zh" ?
  4574. `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/subs/${OJBetter.state.formatName}.json` :
  4575. `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${localizationLanguage}/resources/subs/${OJBetter.state.formatName}.json`;
  4576. if (data) data = data.data;
  4577. if (!data) {
  4578. // 如果本地没有数据,从远端获取并保存
  4579. data = await OJB_getExternalJSON(url);
  4580. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: data });
  4581. } else {
  4582. // 如果本地有数据,检查是否已经在当前会话中尝试过更新
  4583. const sessionKey = `ojb_updateL10nWebsiteJson_${localizationLanguage}`;
  4584. if (!OJB_isSameBrowserSession(sessionKey)) {
  4585. // 如果尚未更新,则在后台更新
  4586. (async () => {
  4587. try {
  4588. const newData = await OJB_getExternalJSON(url);
  4589. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: newData });
  4590. console.log("Website local data has been refreshed!");
  4591. } catch (error) {
  4592. console.error('Failed to update localization data:', error);
  4593. }
  4594. })();
  4595. }
  4596. }
  4597. return data;
  4598. }
  4599.  
  4600. /**
  4601. * 网站本地化替换
  4602. * @returns
  4603. */
  4604. async function localizeWebsite() {
  4605. if (OJBetter.localization.websiteLang === "initial") return;
  4606.  
  4607. // 设置网页语言
  4608. var htmlTag = document.getElementsByTagName("html")[0];
  4609. htmlTag.setAttribute("lang", OJBetter.localization.websiteLang);
  4610.  
  4611. // 获取网站本地化的数据
  4612. var subs = await getLocalizeWebsiteJson(OJBetter.localization.websiteLang);
  4613.  
  4614. /**
  4615. * 文本节点遍历替换
  4616. * @param {JQuery} $nodes jQuery对象
  4617. * @param {Object} textReplaceRules 文本替换规则对象
  4618. * @param {string} key 应用的规则集的名字
  4619. */
  4620. const traverseTextNodes = ($nodes, textReplaceRules, key) => {
  4621. if (!$nodes) return;
  4622.  
  4623. $nodes.each((_, node) => {
  4624. if (node.nodeType === Node.TEXT_NODE) {
  4625. Object.entries(textReplaceRules).forEach(([match, replace]) => {
  4626. try {
  4627. const regex = new RegExp(match, 'g');
  4628. const beforeText = node.textContent;
  4629. node.textContent = node.textContent.replace(regex, replace);
  4630. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4631. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4632. }
  4633. } catch (error) {
  4634. console.error(`Error processing text replacement for match: ${match}`, error);
  4635. }
  4636. });
  4637. } else {
  4638. $(node).contents().each((_, childNode) => {
  4639. traverseTextNodes($(childNode), textReplaceRules, key);
  4640. });
  4641. }
  4642. });
  4643. };
  4644.  
  4645. /**
  4646. * value替换
  4647. * @param {JQuery} $nodes jQuery对象
  4648. * @param {Object} valueReplaceRules 值替换规则对象
  4649. * @param {string} key 应用的规则集的名字
  4650. */
  4651. function traverseValueNodes($nodes, valueReplaceRules, key) {
  4652. if (!$nodes) return;
  4653.  
  4654. $nodes.each(function () {
  4655. let $node = $(this);
  4656. if ($node.is('[value]')) {
  4657. Object.keys(valueReplaceRules).forEach(match => {
  4658. const replace = valueReplaceRules[match];
  4659. const regex = new RegExp(match, 'g');
  4660. let currentValue = $node.val();
  4661. let newValue = currentValue.replace(regex, replace);
  4662. $node.val(newValue);
  4663. if (OJBetter.dev.isRuleMarkingEnabled) {
  4664. if (newValue !== currentValue) $($node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4665. }
  4666. });
  4667. } else {
  4668. $node.children().each(function () {
  4669. traverseValueNodes($(this), valueReplaceRules, key);
  4670. });
  4671. }
  4672. });
  4673. }
  4674.  
  4675. /**
  4676. * 严格的文本节点遍历替换
  4677. * 要求被替换文本严格与规则文本一致
  4678. * @param {JQuery} $nodes jQuery对象
  4679. * @param {Object} textReplaceRules 文本替换规则对象
  4680. * @param {string} key 应用的规则集的名字
  4681. */
  4682. const strictTraverseTextNodes = ($nodes, textReplaceRules, key) => {
  4683. if (!$nodes) return;
  4684.  
  4685. $nodes.each((_, node) => {
  4686. if (node.nodeType === Node.TEXT_NODE) {
  4687. const trimmedNodeText = node.textContent.trim();
  4688. for (const [match, replacement] of Object.entries(textReplaceRules)) {
  4689. if (trimmedNodeText === match) {
  4690. const beforeText = node.textContent;
  4691. node.textContent = replacement;
  4692. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4693. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4694. }
  4695. }
  4696. }
  4697. } else {
  4698. $(node).contents().each((_, childNode) => {
  4699. strictTraverseTextNodes($(childNode), textReplaceRules, key);
  4700. });
  4701. }
  4702. });
  4703. };
  4704.  
  4705. /**
  4706. * 应用文本替换
  4707. */
  4708. let commonReplacements = subs.commonReplacements;
  4709. Object.entries(commonReplacements).forEach(([key, value]) => {
  4710. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4711. classSelectors.forEach(classSelector => {
  4712. if (value.isStrict) {
  4713. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4714. } else {
  4715. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4716. }
  4717. });
  4718. });
  4719.  
  4720. /**
  4721. * 应用value替换
  4722. */
  4723. let InputValueReplacements = subs.InputValueReplacements;
  4724. Object.entries(InputValueReplacements).forEach(([key, value]) => {
  4725. const classSelectors = Array.isArray(value.class) ? value.class : [value.class];
  4726. classSelectors.forEach(classSelector => {
  4727. traverseValueNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4728. });
  4729. });
  4730.  
  4731. /**
  4732. * 动态添加的文本的替换
  4733. */
  4734. let dynamicReplacements = subs.dynamicReplacements;
  4735. Object.entries(dynamicReplacements).forEach(([key, value]) => {
  4736. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4737. classSelectors.forEach(classSelector => {
  4738. OJB_observeElement({
  4739. selector: classSelector,
  4740. callback: (node) => {
  4741. // let popupContent = node.textContent.replace(/^×/, ''); // 去除开头多余的 '×' 字符
  4742. if (value.isStrict) {
  4743. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4744. } else {
  4745. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4746. }
  4747. }
  4748. });
  4749. });
  4750. });
  4751.  
  4752. // 杂项
  4753. (function () {
  4754. // 选项汉化input[type="radio"]
  4755. var translations = {
  4756. "as individual participant": "个人",
  4757. "as a team member": "作为一个团队成员",
  4758. };
  4759. $('input[type="radio"]').each(function () {
  4760. var tag = $(this).parent().contents().filter(function () {
  4761. return this.nodeType === Node.TEXT_NODE;
  4762. });
  4763. for (var i = 0; i < tag.length; i++) {
  4764. var text = tag[i].textContent.trim();
  4765. if (translations.hasOwnProperty(text)) {
  4766. $(this).addClass(text);
  4767. tag[i].replaceWith(translations[text]);
  4768. break;
  4769. }
  4770. }
  4771. });
  4772. })();
  4773. (function () {
  4774. var translations = {
  4775. "(standard input\/output)": "标准输入/输出",
  4776. };
  4777. $("div.notice").each(function () {
  4778. var tag = $(this).children().eq(0).text();
  4779. for (var property in translations) {
  4780. if (tag.match(property)) {
  4781. $(this).children().eq(0).text(translations[property]);
  4782. break;
  4783. }
  4784. }
  4785. });
  4786. })();
  4787.  
  4788. // 轻量站特殊
  4789. if (OJBetter.typeOfPage.is_mSite) {
  4790. traverseTextNodes($('nav'), commonReplacements['.second-level-menu']['rules']);
  4791. }
  4792. if (OJBetter.typeOfPage.is_mSite) {
  4793. (function () {
  4794. var translations = {
  4795. "Announcements": "公告",
  4796. "Submissions": "提交记录",
  4797. "Contests": "比赛",
  4798. };
  4799. $(".caption").each(function () {
  4800. var optionValue = $(this).text();
  4801. if (translations[optionValue]) {
  4802. $(this).text(translations[optionValue]);
  4803. }
  4804. });
  4805. })();
  4806. }
  4807. };
  4808.  
  4809. /**
  4810. * i18next初始化
  4811. */
  4812. async function initI18next() {
  4813. return new Promise((resolve, reject) => {
  4814. i18next
  4815. .use(i18nextChainedBackend)
  4816. .init({
  4817. lng: OJBetter.localization.scriptLang,
  4818. ns: ['common', 'settings', 'config', 'dialog', 'alert', 'translator',
  4819. 'button', 'codeEditor', 'comments', 'announce', 'logMessage'], // 命名空间列表
  4820. defaultNS: 'settings',
  4821. fallbackLng: ['zh', OJBetter.translation.targetLang],
  4822. load: 'currentOnly',
  4823. debug: false,
  4824. backend: {
  4825. backends: [
  4826. i18nextLocalStorageBackend,
  4827. i18nextHttpBackend
  4828. ],
  4829. backendOptions: [{
  4830. prefix: 'i18next_res_',
  4831. expirationTime: 7 * 24 * 60 * 60 * 1000,
  4832. defaultVersion: `v${OJBetter.state.version}`,
  4833. store: typeof window !== 'undefined' ? window.localStorage : null
  4834. }, {
  4835. /* options for secondary backend */
  4836. loadPath: (lng, ns) => {
  4837. if (lng[0] === 'zh' || lng[0] === 'zh-Hans') {
  4838. return `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4839. }
  4840. return `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${lng}/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4841. }
  4842. }]
  4843. }
  4844. }, (err, t) => {
  4845. if (err) {
  4846. reject(err);
  4847. } else {
  4848. jqueryI18next.init(i18next, $, {
  4849. useOptionsAttr: true
  4850. });
  4851. resolve(t);
  4852. }
  4853. });
  4854. });
  4855. };
  4856.  
  4857. /**
  4858. * 抽象命令类
  4859. */
  4860. class Command {
  4861. execute() { }
  4862. undo() { }
  4863. }
  4864.  
  4865. /**
  4866. * 命令调用者
  4867. */
  4868. class CommandInvoker {
  4869. constructor() {
  4870. this.history = [];
  4871. }
  4872.  
  4873. /**
  4874. * 执行命令
  4875. * @param {Command} command 命令对象
  4876. */
  4877. execute(command) {
  4878. this.history.push(command);
  4879. command.execute();
  4880. }
  4881.  
  4882. /**
  4883. * 撤销命令
  4884. */
  4885. undo() {
  4886. const command = this.history.pop();
  4887. if (command) {
  4888. command.undo();
  4889. }
  4890. }
  4891. }
  4892.  
  4893. /**
  4894. * 接收者
  4895. */
  4896. class DOMContainer {
  4897. /**
  4898. * @param {JQueryObject} element 容器对象
  4899. */
  4900. constructor(element) {
  4901. this.containerElement = element;
  4902. }
  4903.  
  4904. /**
  4905. * 添加元素
  4906. * @param {JQueryObject} element 元素对象
  4907. * @returns {JQueryObject} 添加的元素对象
  4908. */
  4909. add(element) {
  4910. this.containerElement.append(element);
  4911. return this.containerElement.children().last();
  4912. }
  4913.  
  4914. /**
  4915. * 删除元素
  4916. * @param {JQueryObject} element 元素对象
  4917. */
  4918. remove(element) {
  4919. $(element).remove();
  4920. }
  4921. }
  4922.  
  4923. /**
  4924. * 具体命令类:添加元素
  4925. */
  4926. class AddElementCommand extends Command {
  4927. /**
  4928. * @param {DOMContainer} receiver 接收者
  4929. * @param {JQueryObject} element 元素对象
  4930. */
  4931. constructor(receiver, element) {
  4932. super();
  4933. this.receiver = receiver;
  4934. this.element = element;
  4935. this.addedElement = null;
  4936. }
  4937.  
  4938. execute() {
  4939. this.addedElement = this.receiver.add(this.element);
  4940. }
  4941.  
  4942. undo() {
  4943. if (this.addedElement) {
  4944. this.receiver.remove(this.addedElement);
  4945. }
  4946. }
  4947. }
  4948.  
  4949. /**
  4950. * 具体命令类:删除元素
  4951. */
  4952. class RemoveElementCommand extends Command {
  4953. /**
  4954. * @param {DOMContainer} receiver 接收者
  4955. * @param {JQueryObject} element 元素对象
  4956. */
  4957. constructor(receiver, element) {
  4958. super();
  4959. this.receiver = receiver;
  4960. this.element = element;
  4961. this.parent = $(element).parent();
  4962. this.nextSibling = $(element).next();
  4963. }
  4964.  
  4965. execute() {
  4966. this.receiver.remove(this.element);
  4967. }
  4968.  
  4969. undo() {
  4970. if (this.nextSibling.length > 0) {
  4971. $(this.element).insertBefore(this.nextSibling);
  4972. } else {
  4973. this.parent.append(this.element);
  4974. }
  4975. }
  4976. }
  4977.  
  4978. /**
  4979. * 验证器
  4980. */
  4981. class Validator {
  4982. /**
  4983. * 表单必填项空值校验
  4984. */
  4985. static required(structure) {
  4986. let config = {};
  4987. let allFieldsValid = true;
  4988. for (const key in structure) {
  4989. let value = key.type == 'checkbox' ?
  4990. $(key).prop("checked") : $(key).val();
  4991.  
  4992. config[structure[key].value] = value;
  4993.  
  4994. if (value || structure[key].require === false) {
  4995. $(key).removeClass('is_null');
  4996. } else {
  4997. $(key).addClass('is_null');
  4998. allFieldsValid = false;
  4999. }
  5000. }
  5001. return {
  5002. valid: allFieldsValid,
  5003. config: config
  5004. };
  5005. }
  5006.  
  5007. /**
  5008. * 表单合法性校验
  5009. */
  5010. static checkKeyValuePairs(structure, config) {
  5011. let errorKeys = [];
  5012. let allFieldsValid = true;
  5013.  
  5014. for (const key in structure) {
  5015. const { check, value } = structure[key];
  5016. const fieldValue = config[value];
  5017.  
  5018. // 如果字段没有值或校验类型不匹配,则跳过当前迭代
  5019. if (!fieldValue) continue;
  5020.  
  5021. let isValid = true;
  5022. switch (check) {
  5023. case 'keyValuePairs':
  5024. isValid = Validator.keyValuePairs(fieldValue);
  5025. break;
  5026. case 'dotSeparatedPath':
  5027. isValid = Validator.validateDotSeparatedPath(fieldValue);
  5028. break;
  5029. default:
  5030. // 没有匹配的校验类型
  5031. continue;
  5032. }
  5033.  
  5034. Validator.toggleErrorDisplay(key, isValid);
  5035. if (!isValid) {
  5036. allFieldsValid = false;
  5037. errorKeys.push(key);
  5038. }
  5039. }
  5040.  
  5041. return {
  5042. valid: allFieldsValid,
  5043. errorKeys: errorKeys
  5044. };
  5045. }
  5046.  
  5047. /**
  5048. * 切换错误信息的显示和隐藏
  5049. * @param {string} key - 字段的键
  5050. * @param {boolean} isValid - 字段值是否有效
  5051. */
  5052. static toggleErrorDisplay(key, isValid) {
  5053. const errorMessage = i18next.t('common.unValid', { ns: 'settings' });
  5054. const $errorSpan = $(key).prev('span.text-error');
  5055. if (!isValid) {
  5056. if (!$errorSpan.length) {
  5057. $(key).before(`<span class="text-error" style="color: red;">${errorMessage}</span>`);
  5058. }
  5059. } else {
  5060. $errorSpan.remove();
  5061. }
  5062. }
  5063.  
  5064. /**
  5065. * 键值对合法性校验
  5066. * @param {string} value
  5067. * @returns {boolean}
  5068. */
  5069. static keyValuePairs(value) {
  5070. const keyValuePairs = value.split('\n');
  5071. // 允许值中包含空格和冒号
  5072. const regex = /^[a-zA-Z0-9_-]+\s*:\s*.+$/;
  5073. return keyValuePairs.every(pair => regex.test(pair));
  5074. }
  5075.  
  5076.  
  5077. /**
  5078. * 点分隔符路径格式校验,允许加减运算
  5079. * @param {string} path
  5080. * @returns {boolean}
  5081. */
  5082. static validateDotSeparatedPath(path) {
  5083. // 正则表达式允许标识符之间有点号,标识符可以包含加减运算
  5084. const regex = /^([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*\.)*([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*)$/;
  5085. return regex.test(path);
  5086. }
  5087. }
  5088.  
  5089. /**
  5090. * 配置管理
  5091. */
  5092. class ConfigManager {
  5093. /**
  5094. * @param {HTMLElement} element - 挂载容器
  5095. * @param {string} prefix - 前缀
  5096. * @param {object} tempConfig - 配置内容
  5097. * @param {object} structure - 配置结构
  5098. * @param {object} configHTML - 配置编辑页面HTML
  5099. * @param {boolean} allowChoice - 是否允许选择列表项
  5100. */
  5101. constructor(element, prefix, tempConfig, structure, configHTML, allowChoice = true) {
  5102. /** @param 设置面板DIV */
  5103. this.settingMenuDiv = $('#OJBetter_setting_menu');
  5104. this.element = $(element);
  5105. this.prefix = prefix;
  5106. this.tempConfig = tempConfig;
  5107. this.structure = structure;
  5108. this.configHTML = configHTML;
  5109. this.allowChoice = allowChoice;
  5110.  
  5111. this.controlTip = null;
  5112. this.config_bar_list = null;
  5113. this.config_bar_ul = null;
  5114. this.config_add_button = null;
  5115. this.menu = null;
  5116. this.editItem = null;
  5117. this.deleteItem = null;
  5118.  
  5119. // 绑定方法
  5120. this.onAdd = this.onAdd.bind(this);
  5121. this.onEdit = this.onEdit.bind(this);
  5122. this.onDelete = this.onDelete.bind(this);
  5123. this.createListItemElement = this.createListItemElement.bind(this);
  5124.  
  5125. this.lastItemId = 0; // 列表中当前最后一个元素的id号
  5126. this.init();
  5127. }
  5128.  
  5129. init() {
  5130. this.createControlBar();
  5131. this.createContextMenu();
  5132. this.renderList();
  5133. }
  5134.  
  5135. /**
  5136. * 创建控制栏
  5137. */
  5138. createControlBar() {
  5139. this.controlTip = OJB_safeCreateJQElement(`<div id='${this.prefix}configControlTip' style='color:red;'></div>`);
  5140. this.config_bar_list = OJB_safeCreateJQElement(`<div class='config_bar_list' id='${this.prefix}config_bar_list'></div>`);
  5141. this.config_bar_ul = OJB_safeCreateJQElement(`<ul class='config_bar_ul' id='${this.prefix}config_bar_ul'></ul>`);
  5142. this.element.append(this.controlTip);
  5143. this.element.append(this.config_bar_list);
  5144. this.config_bar_list.append(this.config_bar_ul);
  5145. }
  5146.  
  5147. /**
  5148. * 创建右键菜单
  5149. */
  5150. createContextMenu() {
  5151. const menu = OJB_safeCreateJQElement(`<div id='config_bar_menu' style='display: none;'></div>`);
  5152. const editItem = OJB_safeCreateJQElement(`
  5153. <div class='config_bar_menu_item' id='config_bar_menu_edit'>
  5154. ${i18next.t('contextMenu.edit', { ns: 'translator' })}
  5155. </div>`);
  5156. const deleteItem = OJB_safeCreateJQElement(`
  5157. <div class='config_bar_menu_item' id='config_bar_menu_delete'>
  5158. ${i18next.t('contextMenu.delete', { ns: 'translator' })}
  5159. </div>`);
  5160. menu.append(editItem);
  5161. menu.append(deleteItem);
  5162. this.editItem = editItem;
  5163. this.deleteItem = deleteItem;
  5164. this.menu = menu;
  5165. this.settingMenuDiv.append(menu);
  5166. }
  5167.  
  5168. /**
  5169. * 关闭右键菜单
  5170. */
  5171. closeContextMenu() {
  5172. this.menu.css({ display: "none" });
  5173. }
  5174.  
  5175. /**
  5176. * 创建列表项
  5177. * @param {string} text - 列表项文本
  5178. * @returns {HTMLElement} - 列表项
  5179. */
  5180. createListItemElement(text) {
  5181. const id = OJB_getRandomNumber(4);
  5182. const li = $("<li></li>");
  5183. const radio = OJB_safeCreateJQElement(`<input type='radio' name='${this.prefix}config_item'></input>`)
  5184. .attr("value", text)
  5185. .attr("id", id)
  5186. .attr("prev_id", this.lastItemId)
  5187. .appendTo(li);
  5188. if (!this.allowChoice) {
  5189. radio.prop("disabled", true);
  5190. }
  5191. const label = OJB_safeCreateJQElement(`<label for='${id}' class='config_bar_ul_li_text'>${text}</label>`).appendTo(li);
  5192.  
  5193.  
  5194. this.lastItemId = id;
  5195.  
  5196. // 添加右键菜单
  5197. li.on("contextmenu", (event) => {
  5198. event.preventDefault();
  5199. this.menu.css({
  5200. display: "block",
  5201. left: event.pageX, top: event.pageY
  5202. });
  5203.  
  5204. const deleteItem = this.deleteItem;
  5205. const editItem = this.editItem;
  5206.  
  5207. // 移除旧事件
  5208. deleteItem.off("click");
  5209. editItem.off("click");
  5210.  
  5211. // 获取 li 在 ul 中的索引
  5212. const index = li.index();
  5213.  
  5214. deleteItem.on("click", () => this.onDelete(index, li));
  5215. editItem.on("click", () => this.onEdit(index, li));
  5216.  
  5217. $(document).one("click", (event) => {
  5218. if (!this.menu.get(0).contains(event.target)) {
  5219. this.closeContextMenu();
  5220. deleteItem.off("click", () => this.onDelete);
  5221. editItem.off("click", () => this.onEdit);
  5222. }
  5223. });
  5224. });
  5225.  
  5226. return li;
  5227. }
  5228.  
  5229. /**
  5230. * 渲染配置列表
  5231. */
  5232. renderList() {
  5233. const list = this.config_bar_ul;
  5234. list.empty(); // 清空
  5235. this.tempConfig.configurations.forEach((item) => {
  5236. list.append(this.createListItemElement(item['name']));
  5237. });
  5238.  
  5239. // 添加按钮
  5240. let addButton = OJB_safeCreateJQElement(`<li id='${this.prefix}add_button' class="tempConfig_add_button">
  5241. <span>+ ${i18next.t('add', { ns: 'common' })}</span>
  5242. </li>`);
  5243. this.config_add_button = addButton;
  5244. list.append(addButton);
  5245. addButton.on("click", this.onAdd);
  5246. }
  5247.  
  5248. /**
  5249. * 添加配置项
  5250. */
  5251. onAdd() {
  5252. const configMenu = this.createConfigHTML();
  5253. const structure = this.structure;
  5254.  
  5255. configMenu.on("click", "#tempConfig_save", () => {
  5256.  
  5257. // 检查必填字段
  5258. const { valid, config } = Validator.required(structure);
  5259. if (!valid) return;
  5260.  
  5261. // 检查键值对
  5262. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5263. if (!checkOk) return;
  5264.  
  5265. this.tempConfig.configurations.push(config);
  5266.  
  5267. this.createListItemElement(config.name).insertBefore(this.config_add_button);
  5268.  
  5269. configMenu.remove();
  5270. });
  5271.  
  5272. configMenu.on("click", ".btn-close", () => {
  5273. configMenu.remove();
  5274. });
  5275. }
  5276.  
  5277. /**
  5278. * 修改配置项
  5279. * @param {number} index - 配置项索引
  5280. * @param {HTMLElement} li - 配置项
  5281. * @returns {void}
  5282. */
  5283. onEdit(index, li) {
  5284. const configMenu = this.createConfigHTML();
  5285. const structure = this.structure;
  5286.  
  5287. this.closeContextMenu();
  5288.  
  5289. // 填充表单
  5290. for (const [key, { value, type }] of Object.entries(this.structure)) {
  5291. const configValue = this.tempConfig.configurations[index][value];
  5292. const $element = $(key);
  5293. if (type === 'checkbox') {
  5294. $element.prop("checked", configValue);
  5295. } else {
  5296. $element.val(configValue);
  5297. }
  5298. }
  5299.  
  5300. configMenu.on("click", "#tempConfig_save", () => {
  5301. // 检查必填字段
  5302. const { valid, config } = Validator.required(structure);
  5303. if (!valid) return;
  5304.  
  5305. // 检查键值对
  5306. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5307. if (!checkOk) return;
  5308.  
  5309. // 更新配置
  5310. this.tempConfig.configurations[index] = config;
  5311. li.find('label').text(config.name);
  5312.  
  5313. OJB_closeAndRemoveModal(configMenu);
  5314. });
  5315.  
  5316. configMenu.on("click", ".btn-close", () => {
  5317. OJB_closeAndRemoveModal(configMenu);
  5318. });
  5319. }
  5320.  
  5321. /**
  5322. * 删除配置项
  5323. * @param {number} index - 配置项索引
  5324. * @param {HTMLElement} li - 配置项
  5325. * @returns {void}
  5326. */
  5327. onDelete(index, li) {
  5328. this.closeContextMenu();
  5329. this.tempConfig.configurations.splice(index, 1);
  5330. li.remove();
  5331. }
  5332.  
  5333. /**
  5334. * 创建配置编辑页面
  5335. * @returns {JQuery<HTMLElement>} 返回配置编辑页面
  5336. */
  5337. createConfigHTML() {
  5338. const configMenu = OJB_safeCreateJQElement(this.configHTML);
  5339. this.settingMenuDiv.after(configMenu);
  5340. OJB_showModal(configMenu);
  5341. OJB_addDraggable(configMenu);
  5342. elementLocalize(configMenu);
  5343. return configMenu;
  5344. }
  5345.  
  5346. /**
  5347. * 获取配置内容
  5348. * @returns {object} - 配置内容
  5349. */
  5350. getTempConfig() {
  5351. return this.tempConfig;
  5352. }
  5353.  
  5354. /**
  5355. * 注册列表项选中改变监听
  5356. */
  5357. registerChoiceChange() {
  5358. this.config_bar_ul.on("change", "input[type='radio']", (event) => {
  5359. const value = event.target.value;
  5360. this.tempConfig.choice = value;
  5361. });
  5362. }
  5363. }
  5364.  
  5365. const OJBetter_setting_sidebar_HTML = `
  5366. <div class="OJBetter_setting_sidebar">
  5367. <ul>
  5368. <li><a href="#basic-settings" id="sidebar-basic-settings" class="active" data-i18n="settings:sidebar.basic"></a></li>
  5369. <li><a href="#l10n_settings" id="sidebar-l10n_settings" data-i18n="settings:sidebar.localization"></a></li>
  5370. <li><a href="#translation-settings" id="sidebar-translation-settings" data-i18n="settings:sidebar.translation"></a></li>
  5371. <li><a href="#clist_rating-settings" id="sidebar-clist_rating-settings" data-i18n="settings:sidebar.clist"></a></li>
  5372. <li><a href="#code_editor-settings" id="sidebar-code_editor-settings" data-i18n="settings:sidebar.monaco"></a></li>
  5373. <li><a href="#preference-settings" id="sidebar-preference-settings" data-i18n="settings:sidebar.preference"></a></li>
  5374. <li><a href="#dev-settings" id="sidebar-dev-settings" data-i18n="settings:sidebar.dev"></a></li>
  5375. <li><a href="#about-settings" id="sidebar-about-settings" data-i18n="settings:sidebar.about"></a></li>
  5376. </ul>
  5377. </div>
  5378. `;
  5379.  
  5380. const basic_settings_HTML = `
  5381. <div id="basic-settings" class="settings-page active">
  5382. <h3 data-i18n="settings:basic.title"></h3>
  5383. <hr>
  5384. <div class='OJBetter_setting_list' style="padding: 0px 10px;">
  5385. <span id="darkMode_span" data-i18n="settings:basic.darkMode.name"></span>
  5386. <div class="dark-mode-selection">
  5387. <label>
  5388. <input class="radio-input" type="radio" name="darkMode" value="dark" />
  5389. <span class="OJBetter_setting_menu_label_text"
  5390. data-i18n="settings:basic.darkMode.options.dark"></span>
  5391. <span class="radio-icon"> </span>
  5392. </label>
  5393. <label>
  5394. <input checked="" class="radio-input" type="radio" name="darkMode" value="light" />
  5395. <span class="OJBetter_setting_menu_label_text"
  5396. data-i18n="settings:basic.darkMode.options.light"></span>
  5397. <span class="radio-icon"> </span>
  5398. </label>
  5399. <label>
  5400. <input class="radio-input" type="radio" name="darkMode" value="follow" />
  5401. <span class="OJBetter_setting_menu_label_text"
  5402. data-i18n="settings:basic.darkMode.options.system"></span>
  5403. <span class="radio-icon"> </span>
  5404. </label>
  5405. </div>
  5406. </div>
  5407. <div class='OJBetter_setting_list'>
  5408. <label for="expandFoldingblocks" data-i18n="settings:basic.expandBlocks"></label>
  5409. <input type="checkbox" id="expandFoldingblocks" name="expandFoldingblocks">
  5410. </div>
  5411. <div class='OJBetter_setting_list'>
  5412. <label for="renderPerfOpt" data-i18n="settings:basic.renderOptimization.label"></label>
  5413. <div class="help_tip">
  5414. ${helpCircleHTML}
  5415. <div class="tip_text" data-i18n="[html]settings:basic.renderOptimization.helpText"></div>
  5416. </div>
  5417. <input type="checkbox" id="renderPerfOpt" name="renderPerfOpt">
  5418. </div>
  5419. <div class='OJBetter_setting_list'>
  5420. <label for="selectElementPerfOpt" data-i18n="settings:basic.selectElementOptimization.label"></label>
  5421. <div class="help_tip">
  5422. ${helpCircleHTML}
  5423. <div class="tip_text" data-i18n="[html]settings:basic.selectElementOptimization.helpText"></div>
  5424. </div>
  5425. <input type="checkbox" id="selectElementPerfOpt" name="selectElementPerfOpt">
  5426. </div>
  5427. <div class='OJBetter_setting_list'>
  5428. <label for="commentPaging" data-i18n="settings:basic.paging.label"></label>
  5429. <div class="help_tip">
  5430. ${helpCircleHTML}
  5431. <div class="tip_text" data-i18n="[html]settings:basic.paging.helpText"></div>
  5432. </div>
  5433. <input type="checkbox" id="commentPaging" name="commentPaging">
  5434. </div>
  5435. <div class='OJBetter_setting_list'>
  5436. <label for="showJumpToLuogu" data-i18n="settings:basic.luoguJump.label"></label>
  5437. <div class="help_tip">
  5438. ${helpCircleHTML}
  5439. <div class="tip_text" data-i18n="[html]settings:basic.luoguJump.helpText"></div>
  5440. </div>
  5441. <input type="checkbox" id="showJumpToLuogu" name="showJumpToLuogu">
  5442. </div>
  5443. <div class='OJBetter_setting_list'>
  5444. <label for="showCF2vjudge" data-i18n="settings:basic.vjudgeJump.label"></label>
  5445. <div class="help_tip">
  5446. ${helpCircleHTML}
  5447. <div class="tip_text" data-i18n="[html]settings:basic.vjudgeJump.helpText"></div>
  5448. </div>
  5449. <input type="checkbox" id="showCF2vjudge" name="showCF2vjudge">
  5450. </div>
  5451. <div class='OJBetter_setting_list'>
  5452. <label for="standingsRecolor" data-i18n="settings:basic.recolor.label"></label>
  5453. <div class="help_tip">
  5454. ${helpCircleHTML}
  5455. <div class="tip_text" data-i18n="[html]settings:basic.recolor.helpText"></div>
  5456. </div>
  5457. <input type="checkbox" id="standingsRecolor" name="standingsRecolor">
  5458. </div>
  5459. </div>
  5460. `;
  5461.  
  5462. const l10n_settings_HTML = `
  5463. <div id="l10n_settings" class="settings-page">
  5464. <h3 data-i18n="settings:localization.title"></h3>
  5465. <hr>
  5466. <div class='OJBetter_setting_list'>
  5467. <label for="scriptL10nLanguage" style="display: flex;" data-i18n="settings:localization.scriptLanguageLabel"></label>
  5468. <select id="scriptL10nLanguage" name="scriptL10nLanguage">
  5469. <option value="zh">简体中文</option>
  5470. <option value="zh-Hant">繁體中文</option>
  5471. <option value="en">English</option>
  5472. <option value="de">Deutsch</option>
  5473. <option value="fr">Français</option>
  5474. <option value="ko">한국어</option>
  5475. <option value="pt">Português</option>
  5476. <option value="ja">日本語</option>
  5477. <option value="es">Español</option>
  5478. <option value="it">Italiano</option>
  5479. <option value="hi">हिन्दी</option>
  5480. </select>
  5481. </div>
  5482. <div class='OJBetter_setting_list'>
  5483. <label for="localizationLanguage" style="display: flex;" data-i18n="settings:localization.websiteLanguageLabel"></label>
  5484. <select id="localizationLanguage" name="localizationLanguage">
  5485. <option value="initial">——</option>
  5486. <option value="zh">简体中文</option>
  5487. <option value="zh-Hant">繁體中文</option>
  5488. <option value="de">Deutsch</option>
  5489. <option value="fr">Français</option>
  5490. <option value="ko">한국어</option>
  5491. <option value="pt">Português</option>
  5492. <option value="ja">日本語</option>
  5493. <option value="es">Español</option>
  5494. <option value="it">Italiano</option>
  5495. <option value="hi">हिन्दी</option>
  5496. </select>
  5497. </div>
  5498. <div class='OJBetter_setting_list alert_tip'>
  5499. <div data-i18n="[html]settings:localization.notice.1"></div>
  5500. </div>
  5501. <div class='OJBetter_setting_list alert_tip'>
  5502. <div data-i18n="[html]settings:localization.notice.2"></div>
  5503. </div>
  5504. </div>
  5505. `;
  5506.  
  5507. const translation_settings_HTML = `
  5508. <div id="translation-settings" class="settings-page">
  5509. <h3 data-i18n="settings:translation.title"></h3>
  5510. <hr>
  5511. <h4 data-i18n="settings:translation.options.title"></h4>
  5512. <div class='OJBetter_setting_list'>
  5513. <label for="transTargetLang" style="display: flex;" data-i18n="settings:translation.preference.target.title"></label>
  5514. <div class="help_tip">
  5515. ${helpCircleHTML}
  5516. <div class="tip_text" data-i18n="[html]settings:translation.preference.target.helpText"></div>
  5517. </div>
  5518. <select id="transTargetLang" name="transTargetLang">
  5519. <option value="zh">简体中文</option>
  5520. <option value="zh-Hant">繁體中文</option>
  5521. <option value="de">Deutsch</option>
  5522. <option value="fr">Français</option>
  5523. <option value="ko">한국어</option>
  5524. <option value="pt">Português</option>
  5525. <option value="ja">日本語</option>
  5526. <option value="es">Español</option>
  5527. <option value="it">Italiano</option>
  5528. <option value="hi">हिन्दी</option>
  5529. </select>
  5530. </div>
  5531. <div id="translationServices">
  5532. <label>
  5533. <input type='radio' name='translation' value='deepl'>
  5534. <span class='OJBetter_setting_menu_label_text'
  5535. data-i18n="settings:translation.options.services.deepl"></span>
  5536. </label>
  5537. <label>
  5538. <input type='radio' name='translation' value='iflyrec'>
  5539. <span class='OJBetter_setting_menu_label_text'
  5540. data-i18n="settings:translation.options.services.iflyrec"></span>
  5541. </label>
  5542. <label>
  5543. <input type='radio' name='translation' value='youdao'>
  5544. <span class='OJBetter_setting_menu_label_text'
  5545. data-i18n="settings:translation.options.services.youdao"></span>
  5546. </label>
  5547. <label>
  5548. <input type='radio' name='translation' value='google'>
  5549. <span class='OJBetter_setting_menu_label_text'
  5550. data-i18n="settings:translation.options.services.google"></span>
  5551. </label>
  5552. <label>
  5553. <input type='radio' name='translation' value='caiyun'>
  5554. <span class='OJBetter_setting_menu_label_text'
  5555. data-i18n="settings:translation.options.services.caiyun"></span>
  5556. </label>
  5557. <label>
  5558. <input type='radio' name='translation' value='openai'>
  5559. <span class='OJBetter_setting_menu_label_text'
  5560. data-i18n="settings:translation.options.services.openai.name">
  5561. <div class="help_tip">
  5562. ${helpCircleHTML}
  5563. <div class="tip_text"
  5564. data-i18n="[html]settings:translation.options.services.openai.helpText"></div>
  5565. </div>
  5566. </span>
  5567. </label>
  5568. </div>
  5569. <hr>
  5570. <h4>DeepL</h4>
  5571. <div class='OJBetter_setting_list'>
  5572. <label for="deepl_type" style="display: flex;" data-i18n="settings:translation.deepl.mode.title"></label>
  5573. <div class="help_tip">
  5574. ${helpCircleHTML}
  5575. <div class="tip_text" data-i18n="[html]settings:translation.deepl.mode.helpText"></div>
  5576. </div>
  5577. <select id="deepl_type" name="deepl_type">
  5578. <option value="free" data-i18n="settings:translation.deepl.mode.select.free"></option>
  5579. <option value="api" data-i18n="settings:translation.deepl.mode.select.api"></option>
  5580. </select>
  5581. </div>
  5582. <div id="deepl_config" class="config"></div>
  5583. <div class='OJBetter_setting_list'>
  5584. <label for="enableEmphasisProtection" data-i18n="settings:translation.deepl.enableEmphasisProtection.title"></label>
  5585. <div class="help_tip" style="margin-right: initial;">
  5586. ${helpCircleHTML}
  5587. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableEmphasisProtection.helpText"></div>
  5588. </div>
  5589. <div class="badge">Official API Only</div>
  5590. <input type="checkbox" id="enableEmphasisProtection" name="enableEmphasisProtection">
  5591. </div>
  5592. <div class='OJBetter_setting_list'>
  5593. <label for="enableLinkProtection" data-i18n="settings:translation.deepl.enableLinkProtection.title"></label>
  5594. <div class="help_tip" style="margin-right: initial;">
  5595. ${helpCircleHTML}
  5596. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableLinkProtection.helpText"></div>
  5597. </div>
  5598. <div class="badge">Official API Only</div>
  5599. <input type="checkbox" id="enableLinkProtection" name="enableLinkProtection">
  5600. </div>
  5601. <hr>
  5602. <h4>ChatGPT</h4>
  5603. <div id="chatgpt_config" class="config"></div>
  5604. <div class='OJBetter_setting_list'>
  5605. <label for="openai_isStream" data-i18n="settings:translation.chatgpt.isStream.name"></label>
  5606. <div class="help_tip">
  5607. ${helpCircleHTML}
  5608. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.isStream.helpText"></div>
  5609. </div>
  5610. <input type="checkbox" id="openai_isStream" name="openai_isStream">
  5611. </div>
  5612. <hr>
  5613. <h4 data-i18n="settings:translation.preference.title"></h4>
  5614. <div class='OJBetter_setting_list'>
  5615. <label for="comment_translation_choice" style="display: flex;"
  5616. data-i18n="settings:translation.preference.comment_translation_choice.title">
  5617. </label>
  5618. <select id="comment_translation_choice" name="comment_translation_choice">
  5619. <option value="0" data-i18n="settings:translation.preference.comment_translation_choice.services.follow"></option>
  5620. <option value="deepl" data-i18n="settings:translation.preference.comment_translation_choice.services.deepl"></option>
  5621. <option value="iflyrec" data-i18n="settings:translation.preference.comment_translation_choice.services.iflyrec"></option>
  5622. <option value="youdao" data-i18n="settings:translation.preference.comment_translation_choice.services.youdao"></option>
  5623. <option value="google" data-i18n="settings:translation.preference.comment_translation_choice.services.google"></option>
  5624. <option value="caiyun" data-i18n="settings:translation.preference.comment_translation_choice.services.caiyun"></option>
  5625. <option value="openai" data-i18n="settings:translation.preference.comment_translation_choice.services.openai"></option>
  5626. </select>
  5627. </div>
  5628. <hr>
  5629. <h4 data-i18n="settings:translation.autoTranslation.title"></h4>
  5630. <div class='OJBetter_setting_list'>
  5631. <label for="autoTranslation" data-i18n="settings:translation.autoTranslation.enable"></label>
  5632. <div class="help_tip">
  5633. ${helpCircleHTML}
  5634. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.helpText"></div>
  5635. </div>
  5636. <input type="checkbox" id="autoTranslation" name="autoTranslation">
  5637. </div>
  5638. <div class='OJBetter_setting_list'>
  5639. <label for='shortTextLength'>
  5640. <div style="display: flex;align-items: center;"
  5641. data-i18n="settings:translation.autoTranslation.shortTextLength.name"></div>
  5642. </label>
  5643. <div class="help_tip">
  5644. ${helpCircleHTML}
  5645. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.shortTextLength.helpText">
  5646. </div>
  5647. </div>
  5648. <input type='number' id='shortTextLength' class='no_default' require=true data-i18n="[placeholder]settings:translation.autoTranslation.shortTextLength.placeholder">
  5649. <span data-i18n="settings:translation.autoTranslation.shortTextLength.end"></span>
  5650. </div>
  5651. <div class='OJBetter_setting_list'>
  5652. <label for="allowMixTrans" data-i18n="settings:translation.autoTranslation.allowMixTrans.name"></label>
  5653. <div class="help_tip">
  5654. ${helpCircleHTML}
  5655. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.allowMixTrans.helpText">
  5656. </div>
  5657. </div>
  5658. <input type="checkbox" id="allowMixTrans" name="allowMixTrans">
  5659. <div class='OJBetter_checkboxs'>
  5660. <input type="checkbox" id="deepl" name="mixedTranslation" value="deepl">
  5661. <label for="deepl" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.deepl"></label>
  5662. <input type="checkbox" id="iflyrec" name="mixedTranslation" value="iflyrec">
  5663. <label for="iflyrec" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.iflyrec"></label>
  5664. <input type="checkbox" id="youdao" name="mixedTranslation" value="youdao">
  5665. <label for="youdao" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.youdao"></label>
  5666. <input type="checkbox" id="google" name="mixedTranslation" value="google">
  5667. <label for="google" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.google">Google</label>
  5668. <input type="checkbox" id="caiyun" name="mixedTranslation" value="caiyun">
  5669. <label for="caiyun" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.caiyun"></label>
  5670. </div>
  5671. </div>
  5672. <hr>
  5673. <h4 data-i18n="settings:translation.advanced.name"></h4>
  5674. <div class='OJBetter_setting_list'>
  5675. <label for="comment_translation_mode" style="display: flex;" data-i18n="settings:translation.advanced.mode.name"></label>
  5676. <div class="help_tip">
  5677. ${helpCircleHTML}
  5678. <div class="tip_text" data-i18n="[html]settings:translation.advanced.mode.helpText"></div>
  5679. </div>
  5680. <select id="comment_translation_mode" name="comment_translation_mode">
  5681. <option value="0" data-i18n="settings:translation.advanced.mode.options.0"></option>
  5682. <option value="1" data-i18n="settings:translation.advanced.mode.options.1"></option>
  5683. <option value="2" data-i18n="settings:translation.advanced.mode.options.2"></option>
  5684. </select>
  5685. </div>
  5686. <div class='OJBetter_setting_list'>
  5687. <label for="memoryTranslateHistory" data-i18n="settings:translation.advanced.memory.name"></label>
  5688. <div class="help_tip">
  5689. ${helpCircleHTML}
  5690. <div class="tip_text" data-i18n="[html]settings:translation.advanced.memory.helpText"></div>
  5691. </div>
  5692. <input type="checkbox" id="memoryTranslateHistory" name="memoryTranslateHistory">
  5693. </div>
  5694. <div class='OJBetter_setting_list'>
  5695. <label for="translation_retransAction" style="display: flex;" data-i18n="settings:translation.advanced.retrans.name"></label>
  5696. <div class="help_tip">
  5697. ${helpCircleHTML}
  5698. <div class="tip_text" data-i18n="[html]settings:translation.advanced.retrans.helpText"></div>
  5699. </div>
  5700. <select id="translation_retransAction" name="translation_retransAction">
  5701. <option value=0 data-i18n="settings:translation.advanced.retrans.options.0"></option>
  5702. <option value=1 data-i18n="settings:translation.advanced.retrans.options.1"></option>
  5703. </select>
  5704. </div>
  5705. <div class='OJBetter_setting_list'>
  5706. <label for='transWaitTime'>
  5707. <div style="display: flex;align-items: center;" data-i18n="settings:translation.advanced.transWaitTime.name"></div>
  5708. </label>
  5709. <div class="help_tip">
  5710. ${helpCircleHTML}
  5711. <div class="tip_text" data-i18n="[html]settings:translation.advanced.transWaitTime.helpText"></div>
  5712. </div>
  5713. <input type='number' id='transWaitTime' class='no_default' require=true data-i18n="[placeholder]settings:translation.advanced.transWaitTime.placeholder">
  5714. <span data-i18n="settings:translation.advanced.transWaitTime.end"></span>
  5715. </div>
  5716. <div class='OJBetter_setting_list'>
  5717. <label for="translation_replaceSymbol" style="display: flex;" data-i18n="settings:translation.advanced.replaceSymbol.name"></label>
  5718. <div class="help_tip">
  5719. ${helpCircleHTML}
  5720. <div class="tip_text" data-i18n="[html]settings:translation.advanced.replaceSymbol.helpText"></div>
  5721. </div>
  5722. <select id="translation_replaceSymbol" name="translation_replaceSymbol">
  5723. <option value=2 data-i18n="settings:translation.advanced.replaceSymbol.options.2"></option>
  5724. <option value=1 data-i18n="settings:translation.advanced.replaceSymbol.options.1"></option>
  5725. <option value=3 data-i18n="settings:translation.advanced.replaceSymbol.options.3"></option>
  5726. </select>
  5727. </div>
  5728. <div class='OJBetter_setting_list'>
  5729. <label for="filterTextWithoutEmphasis" data-i18n="settings:translation.advanced.filterTextWithoutEmphasis.name"></label>
  5730. <div class="help_tip">
  5731. ${helpCircleHTML}
  5732. <div class="tip_text" data-i18n="[html]settings:translation.advanced.filterTextWithoutEmphasis.helpText"></div>
  5733. </div>
  5734. <input type="checkbox" id="filterTextWithoutEmphasis" name="filterTextWithoutEmphasis">
  5735. </div>
  5736. </div>
  5737. `;
  5738.  
  5739. const clist_rating_settings_HTML = `
  5740. <div id="clist_rating-settings" class="settings-page">
  5741. <h3 data-i18n="settings:clist.title"></h3>
  5742. <hr>
  5743. <h4 data-i18n="settings:clist.basics.name"></h4>
  5744. <div class='OJBetter_setting_list alert_tip'>
  5745. <div>
  5746. <p data-i18n="[html]settings:clist.basics.notice"></p>
  5747. </div>
  5748. </div>
  5749. <div class='OJBetter_setting_list'>
  5750. <label for='clist_Authorization'>
  5751. <div style="display: flex;align-items: center;">
  5752. <span class="input_label" data-i18n="settings:clist.basics.key.title"></span>
  5753. </div>
  5754. </label>
  5755. <div class="help_tip">
  5756. ${helpCircleHTML}
  5757. <div class="tip_text" data-i18n="[html]settings:clist.basics.key.helpText"></div>
  5758. </div>
  5759. <input type='text' id='clist_Authorization' class='no_default' required="true"
  5760. data-i18n="[placeholder]settings:clist.basics.key.keyPlaceholder">
  5761. </div>
  5762. <hr>
  5763. <h4 data-i18n="settings:clist.displayRating.title"></h4>
  5764. <div class='OJBetter_setting_list'>
  5765. <label for="showClistRating_contest"><span data-i18n="settings:clist.displayRating.contest.name"></span></label>
  5766. <input type="checkbox" id="showClistRating_contest" name="showClistRating_contest">
  5767. </div>
  5768. <div class='OJBetter_setting_list'>
  5769. <label for="showClistRating_problem"><span data-i18n="settings:clist.displayRating.problem.name"></span></label>
  5770. <input type="checkbox" id="showClistRating_problem" name="showClistRating_problem">
  5771. </div>
  5772. <div class='OJBetter_setting_list'>
  5773. <label for="showClistRating_problemset"><span data-i18n="settings:clist.displayRating.problemset.name"></span></label>
  5774. <input type="checkbox" id="showClistRating_problemset" name="showClistRating_problemset">
  5775. </div>
  5776. <hr>
  5777. <div class='OJBetter_setting_list'>
  5778. <label for="RatingHidden"><span data-i18n="settings:clist.spoilerProtection.title"></span></label>
  5779. <div class="help_tip">
  5780. ${helpCircleHTML}
  5781. <div class="tip_text" data-i18n="[html]settings:clist.spoilerProtection.helpText"></div>
  5782. </div>
  5783. <input type="checkbox" id="RatingHidden" name="RatingHidden">
  5784. </div>
  5785. </div>
  5786. `;
  5787.  
  5788. const code_editor_settings_HTML = `
  5789. <div id="code_editor-settings" class="settings-page">
  5790. <h3 data-i18n="settings:codeEditor.title"></h3>
  5791. <hr>
  5792. <h4 data-i18n="settings:codeEditor.basics"></h4>
  5793. <div class='OJBetter_setting_list'>
  5794. <label for="problemPageCodeEditor"><span
  5795. data-i18n="settings:codeEditor.problemPageCodeEditor.label"></span></label>
  5796. <div class="help_tip">
  5797. ${helpCircleHTML}
  5798. <div class="tip_text" data-i18n="settings:codeEditor.problemPageCodeEditor.helpText"></div>
  5799. </div>
  5800. <input type="checkbox" id="problemPageCodeEditor" name="problemPageCodeEditor">
  5801. </div>
  5802. <div class='OJBetter_setting_list'>
  5803. <label for="beautifyPreBlocks"><span
  5804. data-i18n="settings:codeEditor.beautifyPreBlocks.label"></span></label>
  5805. <div class="help_tip">
  5806. ${helpCircleHTML}
  5807. <div class="tip_text" data-i18n="settings:codeEditor.beautifyPreBlocks.helpText"></div>
  5808. </div>
  5809. <input type="checkbox" id="beautifyPreBlocks" name="beautifyPreBlocks">
  5810. </div>
  5811. <hr>
  5812. <h4 data-i18n="settings:codeEditor.preferences.title"></h4>
  5813. <div class='OJBetter_setting_list'>
  5814. <label for="isCodeSubmitConfirm"><span
  5815. data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.label"></span></label>
  5816. <div class="help_tip">
  5817. ${helpCircleHTML}
  5818. <div class="tip_text" data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.helpText"></div>
  5819. </div>
  5820. <input type="checkbox" id="isCodeSubmitConfirm" name="isCodeSubmitConfirm">
  5821. </div>
  5822. <div class='OJBetter_setting_list'>
  5823. <label for="alwaysConsumeMouseWheel"><span
  5824. data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.label"></span></label>
  5825. <div class="help_tip">
  5826. ${helpCircleHTML}
  5827. <div class="tip_text" data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.helpText"></div>
  5828. </div>
  5829. <input type="checkbox" id="alwaysConsumeMouseWheel" name="alwaysConsumeMouseWheel">
  5830. </div>
  5831. <div class='OJBetter_setting_list'>
  5832. <label for="submitButtonPosition"><span
  5833. data-i18n="settings:codeEditor.preferences.submitButtonPosition.label"></span></label>
  5834. <div class="help_tip">
  5835. ${helpCircleHTML}
  5836. <div class="tip_text" data-i18n="settings:codeEditor.preferences.submitButtonPosition.helpText"></div>
  5837. </div>
  5838. <select id="submitButtonPosition" name="submitButtonPosition">
  5839. <option value="bottom" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.bottom"></option>
  5840. <option value="top" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.top"></option>
  5841. </select>
  5842. </div>
  5843. <hr>
  5844. <h4 data-i18n="settings:codeEditor.onlineCodeExecution.title"></h4>
  5845. <label>
  5846. <input type='radio' name='compiler' value='official'>
  5847. <span class='OJBetter_setting_menu_label_text'
  5848. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.codeforces"></span>
  5849. </label>
  5850. <label>
  5851. <input type='radio' name='compiler' value='wandbox'>
  5852. <span class='OJBetter_setting_menu_label_text'
  5853. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.wandbox"></span>
  5854. </label>
  5855. <label>
  5856. <input type='radio' name='compiler' value='rextester'>
  5857. <span class='OJBetter_setting_menu_label_text'
  5858. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.rextester"></span>
  5859. </label>
  5860. <hr>
  5861. <h4 data-i18n="settings:codeEditor.lsp.title"></h4>
  5862. <div class='OJBetter_setting_list'>
  5863. <label for="useLSP"><span data-i18n="settings:codeEditor.lsp.useLSP.label"></span></label>
  5864. <div class="help_tip">
  5865. ${helpCircleHTML}
  5866. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.useLSP.helpText"></div>
  5867. </div>
  5868. <input type="checkbox" id="useLSP" name="useLSP">
  5869. </div>
  5870. <div class='OJBetter_setting_list'>
  5871. <label for='OJBetter_Bridge_WorkUri'>
  5872. <div style="display: flex;align-items: center;">
  5873. <span class="input_label" data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.label"></span>
  5874. </div>
  5875. </label>
  5876. <div class="help_tip">
  5877. ${helpCircleHTML}
  5878. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.helpText"></div>
  5879. </div>
  5880. <input type='text' id='OJBetter_Bridge_WorkUri' class='no_default'
  5881. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.placeholder">
  5882. </div>
  5883. <div class='OJBetter_setting_list'>
  5884. <label for='OJBetter_Bridge_SocketUrl'>
  5885. <div style="display: flex;align-items: center;">
  5886. <span class="input_label"
  5887. data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.label"></span>
  5888. </div>
  5889. </label>
  5890. <div class="help_tip">
  5891. ${helpCircleHTML}
  5892. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.helpText"></div>
  5893. </div>
  5894. <input type='text' id='OJBetter_Bridge_SocketUrl' class='no_default'
  5895. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.placeholder">
  5896. </div>
  5897. <hr>
  5898. <h4 data-i18n="settings:codeEditor.staticCompletionEnhancement.title"></h4>
  5899. <div class='OJBetter_setting_list'>
  5900. <label for="cppCodeTemplateComplete"><span
  5901. data-i18n="settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.label"></span></label>
  5902. <div class="help_tip">
  5903. ${helpCircleHTML}
  5904. <div class="tip_text" data-i18n="[html]settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.helpText"></div>
  5905. </div>
  5906. <input type="checkbox" id="cppCodeTemplateComplete" name="cppCodeTemplateComplete">
  5907. </div>
  5908. <hr>
  5909. <h5 data-i18n="settings:codeEditor.staticCompletionEnhancement.customization"></h5>
  5910. <div class='OJBetter_setting_list alert_warn'>
  5911. <div>
  5912. <p data-i18n="settings:codeEditor.staticCompletionEnhancement.performanceWarning"></p>
  5913. </div>
  5914. </div>
  5915. <div id="Complet_config" class="config"></div>
  5916. </div>
  5917. `;
  5918.  
  5919. const preference_settings_HTML = `
  5920. <div id="preference-settings" class="settings-page">
  5921. <h3 data-i18n="settings:preference.title"></h3>
  5922. <hr>
  5923. <div class='OJBetter_setting_list'>
  5924. <label for="showLoading" data-i18n="settings:preference.loadingInfo.label"></label>
  5925. <div class="help_tip">
  5926. ${helpCircleHTML}
  5927. <div class="tip_text" data-i18n="[html]settings:preference.loadingInfo.helpText" data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }'></div>
  5928. </div>
  5929. <input type="checkbox" id="showLoading" name="showLoading">
  5930. </div>
  5931. <div class='OJBetter_setting_list'>
  5932. <label for="hoverTargetAreaDisplay" data-i18n="settings:preference.targetArea.label"></label>
  5933. <div class="help_tip">
  5934. ${helpCircleHTML}
  5935. <div class="tip_text" data-i18n="[html]settings:preference.targetArea.helpText"></div>
  5936. </div>
  5937. <input type="checkbox" id="hoverTargetAreaDisplay" name="hoverTargetAreaDisplay">
  5938. </div>
  5939. <div class='OJBetter_setting_list'>
  5940. <label for='iconButtonSize'>
  5941. <div style="display: flex;align-items: center;" data-i18n="settings:preference.iconButtonSize.title"></div>
  5942. </label>
  5943. <div class="help_tip">
  5944. ${helpCircleHTML}
  5945. <div class="tip_text" data-i18n="[html]settings:preference.iconButtonSize.helpText"></div>
  5946. </div>
  5947. <input type='number' id='iconButtonSize' class='no_default' require=true data-i18n="[placeholder]settings:preference.iconButtonSize.placeholder">
  5948. <span>px</span>
  5949. </div>
  5950. </div>
  5951. `;
  5952.  
  5953. const dev_settings_HTML = `
  5954. <div id="dev-settings" class="settings-page">
  5955. <h3 data-i18n="settings:dev.title"></h3>
  5956. <hr>
  5957. <div class='OJBetter_setting_list alert_danger'>
  5958. <div>
  5959. <p data-i18n="[html]settings:dev.notice"></p>
  5960. </div>
  5961. </div>
  5962. <hr>
  5963. <h5 data-i18n="settings:dev.load.title"></h5>
  5964. <div class='OJBetter_setting_list'>
  5965. <label for="notWaiteLoaded"><span data-i18n="settings:dev.load.notWaiteLoaded.label"></span></label>
  5966. <div class="help_tip">
  5967. ${helpCircleHTML}
  5968. <div class="tip_text" data-i18n="[html]settings:dev.load.notWaiteLoaded.helpText"></div>
  5969. </div>
  5970. <input type="checkbox" id="notWaiteLoaded" name="notWaiteLoaded">
  5971. </div>
  5972. <hr>
  5973. <h5 data-i18n="settings:dev.l10n.title"></h5>
  5974. <div class='OJBetter_setting_list'>
  5975. <label><span data-i18n="settings:dev.l10n.refreshScrpitCache.label"></span></label>
  5976. <div class="help_tip">
  5977. ${helpCircleHTML}
  5978. <div class="tip_text" data-i18n="[html]settings:dev.l10n.refreshScrpitCache.helpText"></div>
  5979. </div>
  5980. <button type="button" id="l10n_refreshScrpitCacheButton" name="l10n_refreshScrpitCacheButton" data-i18n="settings:dev.l10n.refreshScrpitCache.button"></button>
  5981. </div>
  5982. <hr>
  5983. <h5 data-i18n="settings:dev.l10n_web.title"></h5>
  5984. <div class='OJBetter_setting_list'>
  5985. <label><span data-i18n="settings:dev.l10n_web.refreshScrpitCache.label"></span></label>
  5986. <div class="help_tip">
  5987. ${helpCircleHTML}
  5988. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.refreshScrpitCache.helpText"></div>
  5989. </div>
  5990. <button type="button" id="l10n_web_refreshScrpitCacheButton" name="l10n_web_refreshScrpitCacheButton" data-i18n="settings:dev.l10n_web.refreshScrpitCache.button"></button>
  5991. </div>
  5992. <div class='OJBetter_setting_list'>
  5993. <label for="isRuleMarkingEnabled"><span data-i18n="settings:dev.l10n_web.isRuleMarkingEnabled.label"></span></label>
  5994. <div class="help_tip">
  5995. ${helpCircleHTML}
  5996. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.isRuleMarkingEnabled.helpText"></div>
  5997. </div>
  5998. <input type="checkbox" id="isRuleMarkingEnabled" name="isRuleMarkingEnabled">
  5999. </div>
  6000. <hr>
  6001. <h5 data-i18n="settings:dev.indexedDB.title"></h5>
  6002. <div class='OJBetter_setting_list'>
  6003. <label><span data-i18n="settings:dev.indexedDB.clear.label"></span></label>
  6004. <div class="help_tip">
  6005. ${helpCircleHTML}
  6006. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.clear.helpText"></div>
  6007. </div>
  6008. <button type="button" id="indexedDB_clearButton" name="indexedDB_clearButton" data-i18n="settings:dev.indexedDB.clear.button"></button>
  6009. </div>
  6010. <div class='OJBetter_setting_list'>
  6011. <label><span data-i18n="settings:dev.indexedDB.inputOrExport.label"></span></label>
  6012. <div class="help_tip">
  6013. ${helpCircleHTML}
  6014. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.inputOrExport.helpText"></div>
  6015. </div>
  6016. <button type="button" id="indexedDB_exportButton" name="indexedDB_exportButton" data-i18n="settings:dev.indexedDB.inputOrExport.export"></button>
  6017. <button type="button" id="indexedDB_importButton" name="indexedDB_importButton" data-i18n="settings:dev.indexedDB.inputOrExport.import"></button>
  6018. </div>
  6019. <hr>
  6020. <h5 data-i18n="settings:dev.configuration.title"></h5>
  6021. <div class='OJBetter_setting_list'>
  6022. <label><span data-i18n="settings:dev.configuration.clear.label"></span></label>
  6023. <div class="help_tip">
  6024. ${helpCircleHTML}
  6025. <div class="tip_text" data-i18n="[html]settings:dev.configuration.clear.helpText"></div>
  6026. </div>
  6027. <button type="button" id="configuration_clearButton" name="configuration_clearButton" data-i18n="settings:dev.configuration.clear.button"></button>
  6028. </div>
  6029. <div class='OJBetter_setting_list'>
  6030. <label><span data-i18n="settings:dev.configuration.inputOrExport.label"></span></label>
  6031. <div class="help_tip">
  6032. ${helpCircleHTML}
  6033. <div class="tip_text" data-i18n="[html]settings:dev.configuration.inputOrExport.helpText"></div>
  6034. </div>
  6035. <button type="button" id="configuration_exportButton" name="configuration_exportButton" data-i18n="settings:dev.configuration.inputOrExport.export"></button>
  6036. <button type="button" id="configuration_importButton" name="configuration_importButton" data-i18n="settings:dev.configuration.inputOrExport.import"></button>
  6037. </div>
  6038. </div>
  6039. `;
  6040.  
  6041. const about_settings_HTML = `
  6042. <div id="about-settings" class="settings-page">
  6043. <h3 data-i18n="settings:about.title"></h3>
  6044. <hr>
  6045. <div class='versionInfo'>
  6046. <p>${OJBetter.state.name}</p>
  6047. <p><span data-i18n="settings:about.version"></span><span id="nowVersion">${OJBetter.state.version}</span></p>
  6048. <p> @北极小狐 <a target="_blank" href="https://github.com/beijixiaohu/OJBetter">Github</a>
  6049. <a target="_blank" href="https://greasyfork.org/zh-CN/scripts/465777">GreasyFork</a></p>
  6050. </div>
  6051. <hr>
  6052. <h5 data-i18n="settings:about.update.title"></h5>
  6053. <div id="thanksforDevChannelNotice" class='OJBetter_setting_list alert_info'>
  6054. <div data-i18n="[html]settings:about.update.thanksforDevChannelNotice"} data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }' ></div>
  6055. </div>
  6056. <div class='OJBetter_setting_list'>
  6057. <label for="updateChannel"><span data-i18n="settings:about.update.channel.label"></span></label>
  6058. <div class="help_tip">
  6059. ${helpCircleHTML}
  6060. <div class="tip_text" data-i18n="[html]settings:about.update.channel.helpText"></div>
  6061. </div>
  6062. <select id="updateChannel" name="updateChannel">
  6063. <option value="release" data-i18n="settings:about.update.channel.options.release"></option>
  6064. <option value="dev" data-i18n="settings:about.update.channel.options.dev"></option>
  6065. </select>
  6066. </div>
  6067. <div class='OJBetter_setting_list'>
  6068. <label for="updateSource"><span data-i18n="settings:about.update.source.label"></span></label>
  6069. <div class="help_tip">
  6070. ${helpCircleHTML}
  6071. <div class="tip_text" data-i18n="[html]settings:about.update.source.helpText"></div>
  6072. </div>
  6073. <select id="updateSource" name="updateSource">
  6074. <option value="greasyfork" data-i18n="settings:about.update.source.options.greasyfork"></option>
  6075. <option value="github" data-i18n="settings:about.update.source.options.github"></option>
  6076. <option value="aliyunoss" data-i18n="settings:about.update.source.options.aliyunoss"></option>
  6077. </select>
  6078. </div>
  6079. </div>
  6080. `;
  6081.  
  6082. const OJBetter_setting_content_HTML = `
  6083. <div class="OJBetter_setting_content">
  6084. ${basic_settings_HTML}
  6085. ${l10n_settings_HTML}
  6086. ${translation_settings_HTML}
  6087. ${clist_rating_settings_HTML}
  6088. ${code_editor_settings_HTML}
  6089. ${preference_settings_HTML}
  6090. ${dev_settings_HTML}
  6091. ${about_settings_HTML}
  6092. </div>
  6093. `;
  6094.  
  6095. // 设置界面HTML
  6096. const OJBetterSettingMenu_HTML = `
  6097. <dialog class='OJBetter_setting_menu' id='OJBetter_setting_menu'>
  6098. <div class="tool-box">
  6099. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6100. <i class="iconfont">&#xe614;</i>
  6101. </button>
  6102. </div>
  6103. <div class="OJBetter_setting_container">
  6104. ${OJBetter_setting_sidebar_HTML}
  6105. ${OJBetter_setting_content_HTML}
  6106. </div>
  6107. </dialog>
  6108. `;
  6109.  
  6110. const apiCustomConfigHTML = (prefix) => {
  6111. return `
  6112. <div class="OJBetter_setting_list">
  6113. <label for='${prefix}_header'>
  6114. <div style="display: flex;align-items: center;">
  6115. <span class="input_label" data-i18n="config:common.advanced.header.label"></span>
  6116. <div class="help_tip">
  6117. ${helpCircleHTML}
  6118. <div class="tip_text" data-i18n="[html]config:common.advanced.header.tipText"></div>
  6119. </div>
  6120. </div>
  6121. </label>
  6122. <textarea id="${prefix}_header" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.header.placeholder"></textarea>
  6123. </div>
  6124. <div class="OJBetter_setting_list">
  6125. <label for='${prefix}_data'>
  6126. <div style="display: flex;align-items: center;">
  6127. <span class="input_label" data-i18n="config:common.advanced.data.label"></span>
  6128. <div class="help_tip">
  6129. ${helpCircleHTML}
  6130. <div class="tip_text" data-i18n="[html]config:common.advanced.data.tipText"></div>
  6131. </div>
  6132. </div>
  6133. </label>
  6134. <textarea id="${prefix}_data" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.data.placeholder"></textarea>
  6135. </div>
  6136. `;
  6137. };
  6138.  
  6139. const apiQuotaConfigHTML = (prefix) => {
  6140. return `
  6141. <div class="OJBetter_setting_list">
  6142. <label for='${prefix}_quota_url'>
  6143. <div style="display: flex;align-items: center;">
  6144. <span class="input_label" data-i18n="config:common.quota.url.label"></span>
  6145. <div class="help_tip">
  6146. ${helpCircleHTML}
  6147. <div class="tip_text" data-i18n="[html]config:common.quota.url.tipText"></div>
  6148. </div>
  6149. </div>
  6150. </label>
  6151. <input type='text' id='${prefix}_quota_url' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.url.placeholder">
  6152. </div>
  6153. <div class="OJBetter_setting_list">
  6154. <label for="${prefix}_quota_method" style="display: flex;" data-i18n="config:common.quota.method.label"></label>
  6155. <div class="help_tip">
  6156. ${helpCircleHTML}
  6157. <div class="tip_text" data-i18n="[html]config:common.quota.method.tipText"></div>
  6158. </div>
  6159. <select id="${prefix}_quota_method" name="${prefix}_quota_method">
  6160. <option value="get">GET</option>
  6161. <option value="post">POST</option>
  6162. </select>
  6163. </div>
  6164. <div class="OJBetter_setting_list">
  6165. <label for='${prefix}_quota_header'>
  6166. <div style="display: flex;align-items: center;">
  6167. <span class="input_label" data-i18n="config:common.quota.header.label"></span>
  6168. <div class="help_tip">
  6169. ${helpCircleHTML}
  6170. <div class="tip_text" data-i18n="[html]config:common.quota.header.tipText"></div>
  6171. </div>
  6172. </div>
  6173. </label>
  6174. <textarea id="${prefix}_quota_header" placeholder='' require = false data-i18n="[placeholder]config:common.quota.header.placeholder"></textarea>
  6175. </div>
  6176. <div class="OJBetter_setting_list">
  6177. <label for='${prefix}_quota_data'>
  6178. <div style="display: flex;align-items: center;">
  6179. <span class="input_label" data-i18n="config:common.quota.data.label"></span>
  6180. <div class="help_tip">
  6181. ${helpCircleHTML}
  6182. <div class="tip_text" data-i18n="[html]config:common.quota.data.tipText"></div>
  6183. </div>
  6184. </div>
  6185. </label>
  6186. <textarea id="${prefix}_quota_data" placeholder='' require = false data-i18n="[placeholder]config:common.quota.data.placeholder"></textarea>
  6187. </div>
  6188. <div class="OJBetter_setting_list">
  6189. <div style="display: flex;align-items: center;">
  6190. <span class="input_label" data-i18n="config:common.quota.surplus.label"></span>
  6191. <div class="help_tip">
  6192. ${helpCircleHTML}
  6193. <div class="tip_text" data-i18n="[html]config:common.quota.surplus.tipText"></div>
  6194. </div>
  6195. </div>
  6196. </label>
  6197. <input type='text' id='${prefix}_quota_surplus' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.surplus.placeholder">
  6198. </div>
  6199. `;
  6200. }
  6201.  
  6202. const deeplConfigEditHTML = `
  6203. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6204. <div class='OJBetter_setting_content'>
  6205. <div class="tool-box">
  6206. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6207. <i class="iconfont">&#xe614;</i>
  6208. </button>
  6209. </div>
  6210. <h4 data-i18n="config:deepl.title"></h4>
  6211. <h5 data-i18n="config:deepl.basic.title"></h5>
  6212. <hr>
  6213. <div class="OJBetter_setting_list">
  6214. <label for='name'>
  6215. <span class="input_label" data-i18n="config:deepl.basic.name.label"></span>
  6216. </label>
  6217. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.name.placeholder">
  6218. </div>
  6219. <div class='OJBetter_setting_list'>
  6220. <label for="deepl_apiGenre" style="display: flex;" data-i18n="config:deepl.genre.label"></label>
  6221. <div class="help_tip">
  6222. ${helpCircleHTML}
  6223. <div class="tip_text" data-i18n="[html]config:deepl.genre.tipText"></div>
  6224. </div>
  6225. <select id="deepl_apiGenre" name="deepl_apiGenre">
  6226. <option value="api-free">api-free</option>
  6227. <option value="api-pro">api-pro</option>
  6228. <option value="deeplx">deeplx</option>
  6229. </select>
  6230. </div>
  6231. <div class="OJBetter_setting_list">
  6232. <label for='deepl_key'>
  6233. <div style="display: flex;align-items: center;">
  6234. <span class="input_label" data-i18n="config:deepl.basic.key.label"></span>
  6235. <div class="help_tip">
  6236. ${helpCircleHTML}
  6237. <div class="tip_text" data-i18n="[html]config:deepl.basic.key.tipText"></div>
  6238. </div>
  6239. </div>
  6240. </label>
  6241. <input type='text' id='deepl_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.key.placeholder">
  6242. </div>
  6243. <div class="OJBetter_setting_list">
  6244. <label for='deepl_proxy'>
  6245. <div style="display: flex;align-items: center;">
  6246. <span class="input_label" data-i18n="config:deepl.basic.proxy.label">Proxy API:</span>
  6247. <div class="help_tip">
  6248. ${helpCircleHTML}
  6249. <div class="tip_text" data-i18n="[html]config:deepl.basic.proxy.tipText"></div>
  6250. </div>
  6251. </div>
  6252. </label>
  6253. <input type='text' id='deepl_proxy' placeholder='' require = false>
  6254. </div>
  6255. <hr>
  6256. <details>
  6257. <summary data-i18n="config:common.advanced.title"></summary>
  6258. ${apiCustomConfigHTML('deepl')}
  6259. </details>
  6260. <details>
  6261. <summary data-i18n="config:common.quota.title"></summary>
  6262. ${apiQuotaConfigHTML('deepl')}
  6263. </details>
  6264. <button id='tempConfig_save' data-i18n="common:save"></button>
  6265. </div>
  6266. </dialog>
  6267. `;
  6268.  
  6269. const chatgptConfigEditHTML = `
  6270. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6271. <div class='OJBetter_setting_content'>
  6272. <div class="tool-box">
  6273. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6274. <i class="iconfont">&#xe614;</i>
  6275. </button>
  6276. </div>
  6277. <h4 data-i18n="config:chatgpt.title"></h4>
  6278. <h5 data-i18n="config:chatgpt.basic.title"></h5>
  6279. <hr>
  6280. <div class="OJBetter_setting_list">
  6281. <label for='name'>
  6282. <span class="input_label" data-i18n="config:chatgpt.basic.name.label"></span>
  6283. </label>
  6284. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.name.placeholder">
  6285. </div>
  6286. <div class="OJBetter_setting_list">
  6287. <label for='chatgpt_model'>
  6288. <div style="display: flex;align-items: center;">
  6289. <span class="input_label" data-i18n="[html]config:chatgpt.basic.model.label"></span>
  6290. <div class="help_tip">
  6291. ${helpCircleHTML}
  6292. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.model.tipText"></div>
  6293. </div>
  6294. </div>
  6295. </label>
  6296. <input type='text' id='chatgpt_model' placeholder='gpt-3.5-turbo' require = false>
  6297. </div>
  6298. <div class="OJBetter_setting_list">
  6299. <label for='chatgpt_key'>
  6300. <div style="display: flex;align-items: center;">
  6301. <span class="input_label" data-i18n="config:chatgpt.basic.key.label"></span>
  6302. <div class="help_tip">
  6303. ${helpCircleHTML}
  6304. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.key.tipText"></div>
  6305. </div>
  6306. </div>
  6307. </label>
  6308. <input type='text' id='chatgpt_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.key.placeholder">
  6309. </div>
  6310. <div class="OJBetter_setting_list">
  6311. <label for='chatgpt_proxy'>
  6312. <div style="display: flex;align-items: center;">
  6313. <span class="input_label" data-i18n="config:chatgpt.basic.proxy.label">Proxy API:</span>
  6314. <div class="help_tip">
  6315. ${helpCircleHTML}
  6316. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.proxy.tipText"></div>
  6317. </div>
  6318. </div>
  6319. </label>
  6320. <input type='text' id='chatgpt_proxy' placeholder='https://api.openai.com/v1/chat/completions' require = false>
  6321. </div>
  6322. <hr>
  6323. <details>
  6324. <summary data-i18n="config:common.advanced.title"></summary>
  6325. ${apiCustomConfigHTML('chatgpt')}
  6326. </details>
  6327. <details>
  6328. <summary data-i18n="config:common.quota.title"></summary>
  6329. ${apiQuotaConfigHTML('chatgpt')}
  6330. </details>
  6331. <button id='tempConfig_save' data-i18n="common:save"></button>
  6332. </div>
  6333. </dialog>
  6334. `;
  6335.  
  6336. const CompletConfigEditHTML = `
  6337. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6338. <div class='OJBetter_setting_content'>
  6339. <div class="tool-box">
  6340. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6341. <i class="iconfont">&#xe614;</i>
  6342. </button>
  6343. </div>
  6344. <h4 data-i18n="config:complet.title"></h4>
  6345. <hr>
  6346. <div class="OJBetter_setting_list">
  6347. <label for='name'>
  6348. <span class="input_label" data-i18n="config:complet.name.label"></span>
  6349. </label>
  6350. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.name.placeholder">
  6351. </div>
  6352. <div class='OJBetter_setting_list'>
  6353. <label for="complet_isChoose"><span id="loaded_span" data-i18n="config:complet.choose.label"></span></label>
  6354. <input type="checkbox" id="complet_isChoose" name="complet_isChoose" require = false>
  6355. </div>
  6356. <div class='OJBetter_setting_list'>
  6357. <label for="complet_genre" style="display: flex;" data-i18n="config:complet.genre.label"></label>
  6358. <div class="help_tip">
  6359. ${helpCircleHTML}
  6360. <div class="tip_text" data-i18n="[html]config:complet.genre.tipText"></div>
  6361. </div>
  6362. <select id="complet_genre" name="complet_genre">
  6363. <option value="monaco">monaco</option>
  6364. <option value="ace">ace</option>
  6365. </select>
  6366. </div>
  6367. <div class='OJBetter_setting_list'>
  6368. <label for="complet_language" style="display: flex;" data-i18n="config:complet.language.label"></label>
  6369. <select id="complet_language" name="complet_language">
  6370. <option value="cpp">cpp</option>
  6371. <option value="python">python</option>
  6372. <option value="java">java</option>
  6373. <option value="c">c</option>
  6374. </select>
  6375. </div>
  6376. <div class="OJBetter_setting_list">
  6377. <label for='complet_jsonUrl'>
  6378. <div style="display: flex;align-items: center;">
  6379. <span class="input_label">JSON URL:</span>
  6380. <div class="help_tip">
  6381. ${helpCircleHTML}
  6382. <div class="tip_text" data-i18n="[html]config:complet.jsonurl.tipText"></div>
  6383. </div>
  6384. </div>
  6385. </label>
  6386. <div class='OJBetter_setting_list alert_warn' data-i18n="[html]config:complet.jsonurl.alert"></div>
  6387. <input type='text' id='complet_jsonUrl' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.jsonurl.placeholder">
  6388. </div>
  6389. <button id='tempConfig_save' data-i18n="common:save"></button>
  6390. </div>
  6391. </dialog>
  6392. `;
  6393.  
  6394. /**
  6395. * 加载设置按钮面板
  6396. */
  6397. async function initSettingsPanel() {
  6398. /**
  6399. * 添加右上角设置按钮
  6400. * @param {string} location 位置选择器
  6401. * @param {string} method 插入方法
  6402. */
  6403. function insertOJBetterSettingButton(location, method) {
  6404. $(location)[method](`<button class='ojb_btn OJBetter_setting'>
  6405. ${OJBetter.state.name} ${i18next.t('settings', { ns: 'common' })}</button>`);
  6406. }
  6407.  
  6408. /**
  6409. * ============================================
  6410. * 该网站插入设置按钮的位置和方式
  6411. */
  6412. insertOJBetterSettingButton(".lang-chooser", "before");
  6413. insertOJBetterSettingButton(".enter-or-register-box", "after");
  6414. if (OJBetter.typeOfPage.is_completeProblemset) insertOJBetterSettingButton(".lang", "before");
  6415. /**
  6416. * ============================================
  6417. */
  6418.  
  6419. const $settingBtns = $(".OJBetter_setting");
  6420. $settingBtns.click(() => {
  6421. $settingBtns.prop("disabled", true).addClass("open");
  6422.  
  6423. // 设置面板div
  6424. const settingMenu = OJB_safeCreateJQElement(OJBetterSettingMenu_HTML);
  6425. $("body").append(settingMenu);
  6426.  
  6427. elementLocalize(settingMenu); // 加载i18n
  6428. OJB_showModal(settingMenu);
  6429. OJB_addDraggable($('#OJBetter_setting_menu')); // 窗口支持拖拽
  6430.  
  6431. // help帮助悬浮窗位置更新
  6432. $(document).on('mouseenter', '.help-icon', function (event) {
  6433. var menuOffset = $('.OJBetter_setting_menu:last').offset();
  6434. var mouseX = event.pageX - menuOffset.left;
  6435. var mouseY = event.pageY - menuOffset.top;
  6436.  
  6437. $('.tip_text').css({
  6438. 'top': mouseY + 'px',
  6439. 'left': mouseX + 'px'
  6440. });
  6441. });
  6442.  
  6443. // 选项卡切换
  6444. $('.OJBetter_setting_sidebar a').click(function (event) {
  6445. event.preventDefault();
  6446. $('.OJBetter_setting_sidebar a').removeClass('active');
  6447. $('.settings-page').removeClass('active');
  6448. $(this).addClass('active');
  6449. const targetPageId = $(this).attr('href').substring(1);
  6450. $('#' + targetPageId).addClass('active');
  6451. });
  6452.  
  6453. /**
  6454. * 更新单选按钮组的可用状态
  6455. * @param {string} selector 单选按钮组的选择器
  6456. * @param {string} targetLanguage 目标语言
  6457. * @param {Object} translationSupport 翻译支持的语言对应表
  6458. */
  6459. const updateRadioButtonsAvailability = (selector, targetLanguage) => {
  6460. Object.entries(OJBetter.supportList.translationSupport).forEach(([service, languages]) => {
  6461. const radioButton = $(selector).find(`input[value="${service}"]`);
  6462. const isEnabled = languages[targetLanguage];
  6463. $(radioButton).prop('disabled', !isEnabled);
  6464. if (!isEnabled) {
  6465. $(radioButton).prop('checked', false);
  6466. }
  6467. });
  6468. };
  6469.  
  6470. /**
  6471. * 检查下拉框选中项是否有效,若无效则清空
  6472. * @param {string} selector 下拉框的选择器
  6473. */
  6474. const validateSelectOption = (selector) => {
  6475. const selectedValue = $(selector).val();
  6476. if (!selectedValue) {
  6477. $(selector).val('');
  6478. }
  6479. };
  6480.  
  6481. /**
  6482. * 更新翻译目标语言下拉框的可用状态
  6483. * @param {string} selector 下拉框的选择器
  6484. * @param {string} targetLanguage 目标语言
  6485. * @param {Object} translationSupport 翻译支持的语言对应表
  6486. */
  6487. const updateSelectOptionsAvailability = (selector, targetLanguage) => {
  6488. $(selector).children('option').each(function () {
  6489. const optionValue = $(this).val();
  6490. const isEnabled = OJBetter.supportList.translationSupport[optionValue] ? OJBetter.supportList.translationSupport[optionValue][targetLanguage] : true;
  6491. $(this).prop('disabled', !isEnabled);
  6492. });
  6493. validateSelectOption(selector);
  6494. };
  6495.  
  6496. /**
  6497. * 更新翻译服务复选框的可用状态
  6498. * @param {string} selector 复选框的选择器
  6499. * @param {string} targetLanguage 目标语言
  6500. * @param {Object} translationSupport 翻译支持的语言对应表
  6501. */
  6502. const updateCheckboxesAvailability = (selector, targetLanguage) => {
  6503. $(selector).children('input').each(function () {
  6504. const checkboxValue = $(this).val();
  6505. const isEnabled = OJBetter.supportList.translationSupport[checkboxValue][targetLanguage];
  6506. $(this).prop('disabled', !isEnabled);
  6507. if (!isEnabled) {
  6508. $(this).prop('checked', false);
  6509. }
  6510. });
  6511. };
  6512.  
  6513. /**
  6514. * 更新更新源下拉框的可用状态
  6515. * @param {string} selector 下拉框的选择器
  6516. * @param {string} targetLanguage 目标语言
  6517. * @param {Object} translationSupport 翻译支持的语言对应表
  6518. */
  6519. const updateUpdateSourceSelectOptionsAvailability = (selector, updateChannel) => {
  6520. $(selector).children('option').each(function () {
  6521. const optionValue = $(this).val();
  6522. const isEnabled = OJBetter.supportList.updateSourceSupportList[optionValue][updateChannel];
  6523. $(this).prop('disabled', !isEnabled);
  6524. });
  6525. validateSelectOption(selector);
  6526. };
  6527.  
  6528. /**
  6529. * 创建配置结构
  6530. * @param {string} type - 该字段的在表单中的类型
  6531. * @param {string} value - 在配置中的键值
  6532. * @param {boolean} require - 是否是表单的必填项
  6533. * @param {string} [check=""] check - 调用的合法性检查
  6534. */
  6535. function createStructure(type, value, require, check = "") {
  6536. return { type, value, require, check };
  6537. }
  6538.  
  6539. // deepl配置
  6540. const deeplStructure = {
  6541. '#name': createStructure('text', 'name', true),
  6542. '#deepl_apiGenre': createStructure('text', 'apiGenre', true),
  6543. '#deepl_key': createStructure('text', 'key', false),
  6544. '#deepl_proxy': createStructure('text', 'proxy', false),
  6545. '#deepl_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6546. '#deepl_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6547. '#deepl_quota_url': createStructure('text', 'quota_url', false),
  6548. '#deepl_quota_method': createStructure('text', 'quota_method', false),
  6549. '#deepl_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6550. '#deepl_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6551. '#deepl_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6552. };
  6553. let tempConfig_deepl = GM_getValue('deepl_config'); // 获取配置信息
  6554. const configManager_deepl = new ConfigManager('#deepl_config', 'deepl_config_', tempConfig_deepl, deeplStructure, deeplConfigEditHTML);
  6555. configManager_deepl.registerChoiceChange();
  6556.  
  6557. // chatgpt配置
  6558. const chatgptStructure = {
  6559. '#name': createStructure('text', 'name', true),
  6560. '#chatgpt_model': createStructure('text', 'model', false),
  6561. '#chatgpt_key': createStructure('text', 'key', true),
  6562. '#chatgpt_proxy': createStructure('text', 'proxy', false),
  6563. '#chatgpt_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6564. '#chatgpt_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6565. '#chatgpt_quota_url': createStructure('text', 'quota_url', false),
  6566. '#chatgpt_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6567. '#chatgpt_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6568. '#chatgpt_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6569. '#chatgpt_quota_method': createStructure('text', 'quota_method', false),
  6570. };
  6571. let tempConfig_chatgpt = GM_getValue('chatgpt_config'); // 获取配置信息
  6572. const configManager_chatgpt = new ConfigManager('#chatgpt_config', 'chatgpt_config_', tempConfig_chatgpt, chatgptStructure, chatgptConfigEditHTML);
  6573. configManager_chatgpt.registerChoiceChange();
  6574.  
  6575. // Complet配置
  6576. const CompletStructure = {
  6577. '#name': createStructure('text', 'name', true),
  6578. '#complet_isChoose': createStructure('checkbox', 'isChoose', true),
  6579. '#complet_genre': createStructure('text', 'genre', true),
  6580. '#complet_language': createStructure('text', 'language', true),
  6581. '#complet_jsonUrl': createStructure('text', 'jsonUrl', true)
  6582. };
  6583. let tempConfig_Complet = GM_getValue('Complet_config'); // 获取配置信息
  6584. const configManager_complet = new ConfigManager('#Complet_config', 'complet_config_', tempConfig_Complet, CompletStructure, CompletConfigEditHTML, false);
  6585.  
  6586. // 状态更新
  6587. $("input[name='darkMode'][value='" + OJBetter.basic.darkMode + "']").prop("checked", true);
  6588. $("#showLoading").prop("checked", GM_getValue("showLoading") === true);
  6589. $("#expandFoldingblocks").prop("checked", GM_getValue("expandFoldingblocks") === true);
  6590. $("#renderPerfOpt").prop("checked", GM_getValue("renderPerfOpt") === true);
  6591. $("#selectElementPerfOpt").prop("checked", GM_getValue("selectElementPerfOpt") === true);
  6592. $("#commentPaging").prop("checked", GM_getValue("commentPaging") === true);
  6593. $("#standingsRecolor").prop("checked", GM_getValue("standingsRecolor") === true);
  6594. $("#showJumpToLuogu").prop("checked", GM_getValue("showJumpToLuogu") === true);
  6595. $("#showCF2vjudge").prop("checked", GM_getValue("showCF2vjudge") === true);
  6596. $("#hoverTargetAreaDisplay").prop("checked", GM_getValue("hoverTargetAreaDisplay") === true);
  6597. $("#showClistRating_contest").prop("checked", GM_getValue("showClistRating_contest") === true);
  6598. $("#showClistRating_problemset").prop("checked", GM_getValue("showClistRating_problemset") === true);
  6599. $("#showClistRating_problem").prop("checked", GM_getValue("showClistRating_problem") === true);
  6600. $("#RatingHidden").prop("checked", GM_getValue("RatingHidden") === true);
  6601. $('#scriptL10nLanguage').val(GM_getValue("scriptL10nLanguage"));
  6602. $('#localizationLanguage').val(GM_getValue("localizationLanguage"));
  6603. $("input[name='translation'][value='" + OJBetter.translation.choice + "']").prop("checked", true);
  6604. $("input[name='translation']").css("color", "gray");
  6605. $('#deepl_type').val(GM_getValue("deepl_type"));
  6606. $("#deepl_config_config_bar_ul").find(`input[name='deepl_config_config_item'][value='${tempConfig_deepl.choice}']`).prop("checked", true);
  6607. $('#enableEmphasisProtection').prop("checked", GM_getValue("enableEmphasisProtection") === true);
  6608. $('#enableLinkProtection').prop("checked", GM_getValue("enableLinkProtection") === true);
  6609. $("#chatgpt_config_config_bar_ul").find(`input[name='chatgpt_config_config_item'][value='${tempConfig_chatgpt.choice}']`).prop("checked", true);
  6610. $("#openai_isStream").prop("checked", GM_getValue("openai_isStream") === true);
  6611. $('#comment_translation_choice').val(GM_getValue("commentTranslationChoice"));
  6612. $('#iconButtonSize').val(GM_getValue("iconButtonSize"));
  6613. $("#autoTranslation").prop("checked", GM_getValue("autoTranslation") === true);
  6614. $('#shortTextLength').val(GM_getValue("shortTextLength"));
  6615. $("#allowMixTrans").prop("checked", GM_getValue("allowMixTrans") === true);
  6616. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6617. if (OJBetter.translation.auto.mixTrans.servers.indexOf($(this).val()) > -1) {
  6618. $(this).prop('checked', true);
  6619. }
  6620. });
  6621. // 翻译目标语言下拉框
  6622. $('#transTargetLang').change(function () {
  6623. var selectedLang = $(this).val();
  6624. updateRadioButtonsAvailability('#translationServices', selectedLang);
  6625. updateSelectOptionsAvailability('#comment_translation_choice', selectedLang);
  6626. updateCheckboxesAvailability('.OJBetter_checkboxs', selectedLang);
  6627. });
  6628. $('#transTargetLang').val(GM_getValue("transTargetLang"));
  6629. $('#transTargetLang').change();
  6630. //
  6631. $('#comment_translation_mode').val(GM_getValue("commentTranslationMode"));
  6632. $("#memoryTranslateHistory").prop("checked", GM_getValue("memoryTranslateHistory") === true);
  6633. $('#transWaitTime').val(GM_getValue("transWaitTime"));
  6634. $('#translation_replaceSymbol').val(GM_getValue("replaceSymbol"));
  6635. $("#filterTextWithoutEmphasis").prop("checked", GM_getValue("filterTextWithoutEmphasis") === true);
  6636. $('#translation_retransAction').val(GM_getValue("retransAction"));
  6637. $("#clist_Authorization").val(GM_getValue("clist_Authorization"));
  6638. $("#problemPageCodeEditor").prop("checked", GM_getValue("problemPageCodeEditor") === true);
  6639. $("#beautifyPreBlocks").prop("checked", GM_getValue("beautifyPreBlocks") === true);
  6640. $("#isCodeSubmitConfirm").prop("checked", GM_getValue("isCodeSubmitConfirm") === true);
  6641. $("#alwaysConsumeMouseWheel").prop("checked", GM_getValue("alwaysConsumeMouseWheel") === true);
  6642. $("#submitButtonPosition").val(GM_getValue("submitButtonPosition"));
  6643. $("#cppCodeTemplateComplete").prop("checked", GM_getValue("cppCodeTemplateComplete") === true);
  6644. $("#useLSP").prop("checked", GM_getValue("useLSP") === true);
  6645. $("#OJBetter_Bridge_WorkUri").val(GM_getValue("OJBetter_Bridge_WorkUri"));
  6646. $("#OJBetter_Bridge_SocketUrl").val(GM_getValue("OJBetter_Bridge_SocketUrl"));
  6647. $("input[name='compiler'][value='" + OJBetter.monaco.onlineCompilerChoice + "']").prop("checked", true);
  6648. $("input[name='compiler']").css("color", "gray");
  6649. // 调试
  6650. $("#notWaiteLoaded").prop("checked", GM_getValue("notWaiteLoaded") === true);
  6651. $('#l10n_refreshScrpitCacheButton').click(clearI18nextCache);
  6652. $('#l10n_web_refreshScrpitCacheButton').click(clearWebsiteL10nData);
  6653. $("#isRuleMarkingEnabled").prop("checked", GM_getValue("isRuleMarkingEnabled") === true);
  6654. $('#indexedDB_clearButton').click(async () => { await clearDatabase(); });
  6655. $('#indexedDB_exportButton').click(async () => { downloadDataAsFile(await exportDatabase(), 'database_export.json') });
  6656. $('#indexedDB_importButton').click(() => { readFileInput(async (fileContent) => { await importDatabase(fileContent); }); });
  6657. $('#configuration_clearButton').click(deleteAllConfigSettings);
  6658. $('#configuration_exportButton').click(() => { downloadDataAsFile(exportSettingsToJSON(), 'configuration_export.json') });
  6659. $('#configuration_importButton').click(() => { readFileInput((fileContent) => { importSettingsFromJSON(fileContent); }) });
  6660. // 关于
  6661. $('#updateChannel').val(GM_getValue("updateChannel"));
  6662. $('#updateSource').val(GM_getValue("updateSource"));
  6663. $('#updateChannel').change(function () {
  6664. var selectedLang = $(this).val();
  6665. updateUpdateSourceSelectOptionsAvailability('#updateSource', selectedLang);
  6666. if (selectedLang == "dev") $('#thanksforDevChannelNotice').show();
  6667. else $('#thanksforDevChannelNotice').hide();
  6668. });
  6669. $('#updateChannel').change();
  6670.  
  6671. // 关闭
  6672. const $settingMenu = $(".OJBetter_setting_menu");
  6673. $settingMenu.on("click", ".btn-close", async () => {
  6674. // 设置的数据
  6675. const settings = {
  6676. darkMode: $("input[name='darkMode']:checked").val(),
  6677. showLoading: $("#showLoading").prop("checked"),
  6678. hoverTargetAreaDisplay: $("#hoverTargetAreaDisplay").prop("checked"),
  6679. expandFoldingblocks: $("#expandFoldingblocks").prop("checked"),
  6680. renderPerfOpt: $("#renderPerfOpt").prop("checked"),
  6681. selectElementPerfOpt: $("#selectElementPerfOpt").prop("checked"),
  6682. commentPaging: $("#commentPaging").prop("checked"),
  6683. standingsRecolor: $("#standingsRecolor").prop("checked"),
  6684. showJumpToLuogu: $("#showJumpToLuogu").prop("checked"),
  6685. showCF2vjudge: $("#showCF2vjudge").prop("checked"),
  6686. scriptL10nLanguage: $('#scriptL10nLanguage').val(),
  6687. localizationLanguage: $('#localizationLanguage').val(),
  6688. transTargetLang: $('#transTargetLang').val(),
  6689. translation: $("input[name='translation']:checked").val(),
  6690. deepl_type: $('#deepl_type').val(),
  6691. enableEmphasisProtection: $("#enableEmphasisProtection").prop("checked"),
  6692. enableLinkProtection: $("#enableLinkProtection").prop("checked"),
  6693. openai_isStream: $("#openai_isStream").prop("checked"),
  6694. commentTranslationChoice: $('#comment_translation_choice').val(),
  6695. iconButtonSize: $('#iconButtonSize').val(),
  6696. autoTranslation: $("#autoTranslation").prop("checked"),
  6697. shortTextLength: $('#shortTextLength').val(),
  6698. allowMixTrans: $("#allowMixTrans").prop("checked"),
  6699. mixedTranslation: (() => {
  6700. let mixedTranslation = [];
  6701. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6702. if ($(this).is(":checked")) {
  6703. mixedTranslation.push($(this).val());
  6704. }
  6705. });
  6706. return mixedTranslation;
  6707. })(),
  6708. commentTranslationMode: $('#comment_translation_mode').val(),
  6709. memoryTranslateHistory: $('#memoryTranslateHistory').prop("checked"),
  6710. transWaitTime: $('#transWaitTime').val(),
  6711. replaceSymbol: $('#translation_replaceSymbol').val(),
  6712. filterTextWithoutEmphasis: $('#filterTextWithoutEmphasis').prop("checked"),
  6713. retransAction: $('#translation_retransAction').val(),
  6714. showClistRating_contest: $('#showClistRating_contest').prop("checked"),
  6715. showClistRating_problemset: $('#showClistRating_problemset').prop("checked"),
  6716. showClistRating_problem: $('#showClistRating_problem').prop("checked"),
  6717. RatingHidden: $('#RatingHidden').prop("checked"),
  6718. clist_Authorization: $('#clist_Authorization').val(),
  6719. problemPageCodeEditor: $("#problemPageCodeEditor").prop("checked"),
  6720. beautifyPreBlocks: $("#beautifyPreBlocks").prop("checked"),
  6721. isCodeSubmitConfirm: $("#isCodeSubmitConfirm").prop("checked"),
  6722. alwaysConsumeMouseWheel: $("#alwaysConsumeMouseWheel").prop("checked"),
  6723. submitButtonPosition: $('#submitButtonPosition').val(),
  6724. cppCodeTemplateComplete: $("#cppCodeTemplateComplete").prop("checked"),
  6725. useLSP: $("#useLSP").prop("checked"),
  6726. OJBetter_Bridge_WorkUri: $('#OJBetter_Bridge_WorkUri').val().replace(/\\/g, '/').replace(/\/$/, ''),
  6727. OJBetter_Bridge_SocketUrl: $('#OJBetter_Bridge_SocketUrl').val(),
  6728. onlineCompilerChoice: $("input[name='compiler']:checked").val(),
  6729. notWaiteLoaded: $("#notWaiteLoaded").prop("checked"),
  6730. isRuleMarkingEnabled: $("#isRuleMarkingEnabled").prop("checked"),
  6731. updateChannel: $('#updateChannel').val(),
  6732. updateSource: $('#updateSource').val()
  6733. };
  6734. // tempConfigs的数据
  6735. const tempConfigs = {
  6736. 'deepl_config': configManager_deepl.getTempConfig(),
  6737. 'chatgpt_config': configManager_chatgpt.getTempConfig(),
  6738. 'Complet_config': configManager_complet.getTempConfig()
  6739. }
  6740.  
  6741. // 判断是否改变
  6742. let changes = {};
  6743. const combinedConfigs = Object.assign({}, settings, tempConfigs); // 合并settings和tempConfigs对象
  6744. for (const [key, value] of Object.entries(combinedConfigs)) {
  6745. const storedValue = GM_getValue(key);
  6746. if (!OJB_deepEquals(value, storedValue)) {
  6747. changes[key] = { oldValue: storedValue, newValue: value };
  6748. }
  6749. }
  6750.  
  6751. // 如果changes对象不为空,则有变化
  6752. if (Object.keys(changes).length > 0) {
  6753. console.log("Changes detected:", changes);
  6754. const shouldSave = await OJB_createDialog(
  6755. i18next.t('saveSetting.title', { ns: 'dialog' }),
  6756. i18next.t('saveSetting.content', { ns: 'dialog' }),
  6757. [
  6758. i18next.t('saveSetting.buttons.0', { ns: 'dialog' }),
  6759. i18next.t('saveSetting.buttons.1', { ns: 'dialog' })
  6760. ]
  6761. ); // 配置改变保存确认
  6762. if (shouldSave) {
  6763. // 数据校验
  6764. // TODO
  6765. if (settings.deepl_type !== 'free') {
  6766. let selectedIndex = $('input[name="deepl_config_config_item"]:checked').length > 0;
  6767. if (!selectedIndex) {
  6768. $('.deepl_config a').removeClass('active');
  6769. $('.settings-page').removeClass('active');
  6770. $('#sidebar-translation-settings').addClass('active');
  6771. $('#translation-settings').addClass('active');
  6772.  
  6773. $('#deepl_config').addClass('missing');
  6774. return;
  6775. } else {
  6776. $('#deepl_config').removeClass('missing');
  6777. }
  6778. }
  6779. if (settings.translation === "openai") {
  6780. let selectedIndex = $('input[name="chatgpt_config_config_item"]:checked').length > 0;
  6781. if (!selectedIndex) {
  6782. $('.chatgpt_config a').removeClass('active');
  6783. $('.settings-page').removeClass('active');
  6784. $('#sidebar-translation-settings').addClass('active');
  6785. $('#translation-settings').addClass('active');
  6786.  
  6787. $('#chatgpt_config').addClass('missing');
  6788. return;
  6789. } else {
  6790. $('#chatgpt_config').removeClass('missing');
  6791. }
  6792. }
  6793. {
  6794. let selectedIndex = $('input[name="translation"]:checked').length > 0;
  6795. if (!selectedIndex) {
  6796. $('.OJBetter_setting_sidebar a').removeClass('active');
  6797. $('.settings-page').removeClass('active');
  6798. $('#sidebar-translation-settings').addClass('active');
  6799. $('#translation-settings').addClass('active');
  6800.  
  6801. $('#translationServices').addClass('missing');
  6802. return;
  6803. } else {
  6804. $('#translationServices').removeClass('missing');
  6805. }
  6806. }
  6807.  
  6808. // 保存数据
  6809. let refreshPage = false; // 是否需要刷新页面
  6810. for (const [key, value] of Object.entries(settings)) {
  6811. if (!refreshPage && !(key == 'translation' || key == 'darkMode' || key == 'commentTranslationChoice')) {
  6812. if (GM_getValue(key) != value) refreshPage = true;
  6813. }
  6814. GM_setValue(key, value);
  6815. }
  6816. for (const [key, value] of Object.entries(tempConfigs)) {
  6817. if (!refreshPage && (JSON.stringify(GM_getValue(key)) != JSON.stringify(value))) refreshPage = true;
  6818. GM_setValue(key, value);
  6819. }
  6820.  
  6821. if (refreshPage) location.reload();
  6822. else {
  6823. // 切换黑暗模式
  6824. if (OJBetter.basic.darkMode != settings.darkMode) {
  6825. OJBetter.basic.darkMode = settings.darkMode;
  6826. // 移除旧的事件监听器
  6827. changeEventListeners.forEach(listener => {
  6828. mediaQueryList.removeEventListener('change', listener);
  6829. });
  6830.  
  6831. if (OJBetter.basic.darkMode == "follow") {
  6832. changeEventListeners.push(handleColorSchemeChange);
  6833. mediaQueryList.addEventListener('change', handleColorSchemeChange);
  6834. $('html').removeAttr('data-theme');
  6835. } else if (OJBetter.basic.darkMode == "dark") {
  6836. $('html').attr('data-theme', 'dark');
  6837. if (OJBetter.monaco.editor) {
  6838. monaco.editor.setTheme('vs-dark');
  6839. }
  6840. } else {
  6841. $('html').attr('data-theme', 'light');
  6842. if (OJBetter.monaco.editor) {
  6843. monaco.editor.setTheme('vs');
  6844. }
  6845. // 移除旧的事件监听器
  6846. const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  6847. window.matchMedia('(prefers-color-scheme: dark)');
  6848. }
  6849. }
  6850. // 更新配置信息
  6851. OJBetter.translation.choice = settings.translation;
  6852. OJBetter.translation.comment.choice = settings.commentTranslationChoice;
  6853. }
  6854. }
  6855. }
  6856. OJB_closeAndRemoveModal(settingMenu);
  6857. $settingBtns.prop("disabled", false).removeClass("open");
  6858. });
  6859. });
  6860. };
  6861.  
  6862. /**
  6863. * 初始化html2markdown转换器
  6864. */
  6865. async function initHTML2MarkDown() {
  6866. OJBetter.common.turndownService = new TurndownService({ bulletListMarker: '-' });
  6867.  
  6868. // 保留原始
  6869. OJBetter.common.turndownService.keep(['del']);
  6870.  
  6871. // 丢弃
  6872. OJBetter.common.turndownService.addRule('remove-by-class', {
  6873. filter: function (node) {
  6874. return node.classList.contains('sample-tests') ||
  6875. node.classList.contains('header') ||
  6876. node.classList.contains('overlay') ||
  6877. node.classList.contains('html2md-panel') ||
  6878. node.classList.contains('likeForm') ||
  6879. node.classList.contains('monaco-editor');
  6880. },
  6881. replacement: function (content, node) {
  6882. return "";
  6883. }
  6884. });
  6885. OJBetter.common.turndownService.addRule('remove-script', {
  6886. filter: function (node, options) {
  6887. return node.tagName.toLowerCase() == "script" && node.type.startsWith("math/tex");
  6888. },
  6889. replacement: function (content, node) {
  6890. return "";
  6891. }
  6892. });
  6893.  
  6894. // inline math
  6895. OJBetter.common.turndownService.addRule('inline-math', {
  6896. filter: function (node, options) {
  6897. return node.tagName.toLowerCase() == "span" && node.className == "MathJax";
  6898. },
  6899. replacement: function (content, node) {
  6900. var latex = $(node).next().text();
  6901. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  6902. return "$" + latex + "$";
  6903. }
  6904. });
  6905.  
  6906. // block math
  6907. OJBetter.common.turndownService.addRule('block-math', {
  6908. filter: function (node, options) {
  6909. return node.tagName.toLowerCase() == "div" && node.className == "MathJax_Display";
  6910. },
  6911. replacement: function (content, node) {
  6912. var latex = $(node).next().text();
  6913. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  6914. return "\n$$\n" + latex + "\n$$\n";
  6915. }
  6916. });
  6917.  
  6918. // texFontStyle
  6919. OJBetter.common.turndownService.addRule('texFontStyle', {
  6920. filter: function (node) {
  6921. return (
  6922. node.nodeName === 'SPAN' &&
  6923. node.classList.contains('tex-font-style-bf')
  6924. )
  6925. },
  6926. replacement: function (content) {
  6927. return '**' + content + '**'
  6928. }
  6929. })
  6930.  
  6931. // sectionTitle
  6932. OJBetter.common.turndownService.addRule('sectionTitle', {
  6933. filter: function (node) {
  6934. return (
  6935. node.nodeName === 'DIV' &&
  6936. node.classList.contains('section-title')
  6937. )
  6938. },
  6939. replacement: function (content) {
  6940. return '**' + content + '**'
  6941. }
  6942. })
  6943.  
  6944. // property-title
  6945. OJBetter.common.turndownService.addRule('property-title', {
  6946. filter: function (node) {
  6947. return (
  6948. node.nodeName === 'DIV' &&
  6949. node.classList.contains('property-title')
  6950. )
  6951. },
  6952. replacement: function (content) {
  6953. return content + ': '
  6954. }
  6955. })
  6956.  
  6957. // pre
  6958. OJBetter.common.turndownService.addRule('pre', {
  6959. filter: function (node, options) {
  6960. return node.tagName.toLowerCase() == "pre";
  6961. },
  6962. replacement: function (content, node) {
  6963. if (!!node.querySelector('code.prettyprint')) {
  6964. return "";
  6965. } else {
  6966. return "```\n" + content + "```\n";
  6967. }
  6968. }
  6969. });
  6970.  
  6971. // bordertable
  6972. OJBetter.common.turndownService.addRule('bordertable', {
  6973. filter: 'table',
  6974. replacement: function (content, node) {
  6975. if (node.classList.contains('bordertable')) {
  6976. var output = [],
  6977. thead = '',
  6978. trs = node.querySelectorAll('tr');
  6979. if (trs.length > 0) {
  6980. var ths = trs[0].querySelectorAll('td,th');
  6981. if (ths.length > 0) {
  6982. thead = '| ' + Array.from(ths).map(th => OJBetter.common.turndownService.turndown(th.innerHTML.trim())).join(' | ') + ' |\n'
  6983. + '| ' + Array.from(ths).map(() => ' --- ').join('|') + ' |\n';
  6984. }
  6985. }
  6986. var rows = node.querySelectorAll('tr');
  6987. Array.from(rows).forEach(function (row, i) {
  6988. if (i > 0) {
  6989. var cells = row.querySelectorAll('td,th');
  6990. var trow = '| ' + Array.from(cells).map(cell => OJBetter.common.turndownService.turndown(cell.innerHTML.trim())).join(' | ') + ' |';
  6991. output.push(trow);
  6992. }
  6993. });
  6994. return thead + output.join('\n');
  6995. } else {
  6996. return content;
  6997. }
  6998. }
  6999. });
  7000. };
  7001.  
  7002. /**
  7003. * 任务队列
  7004. */
  7005. class TaskQueue {
  7006. constructor() {
  7007. this.taskQueues = {};
  7008. this.isProcessing = {}; // 处理状态
  7009. this.delays = {}; // 等待时间(毫秒)
  7010. }
  7011.  
  7012. getDelay(type) {
  7013. if (type === 'openai') {
  7014. return 0;
  7015. } else {
  7016. return OJBetter.translation.waitTime;
  7017. }
  7018. }
  7019.  
  7020. /**
  7021. * 添加任务
  7022. * @param {string} type 任务类型
  7023. * @param {function} fn 任务函数
  7024. * @param {boolean} isNonQueueTask 是否为非队列任务
  7025. */
  7026. addTask(type, fn, isNonQueueTask = false) {
  7027. if (!this.taskQueues[type]) {
  7028. this.taskQueues[type] = [];
  7029. }
  7030.  
  7031. if (isNonQueueTask) {
  7032. fn();
  7033. } else {
  7034. this.taskQueues[type].push(fn);
  7035.  
  7036. if (!this.isProcessing[type]) {
  7037. this.processQueue(type);
  7038. }
  7039. }
  7040. }
  7041.  
  7042. async processQueue(type) {
  7043. this.isProcessing[type] = true;
  7044.  
  7045. while (this.taskQueues[type].length > 0) {
  7046. const task = this.taskQueues[type].shift();
  7047. await task();
  7048.  
  7049. if (this.taskQueues[type].length > 0) {
  7050. await this.wait(this.getDelay(type));
  7051. }
  7052. }
  7053.  
  7054. this.isProcessing[type] = false;
  7055. }
  7056.  
  7057. wait(delay) {
  7058. return new Promise(resolve => {
  7059. setTimeout(resolve, delay);
  7060. });
  7061. }
  7062. }
  7063.  
  7064. /**
  7065. * 检测为空文本
  7066. * @param {string} text 待检测的文本
  7067. * @returns {boolean} 是否为空文本
  7068. */
  7069. const isEmptyText = text => text.trim() === '';
  7070.  
  7071. /**
  7072. * 加载按钮相关函数
  7073. */
  7074. async function initButtonFunc() {
  7075. // 鼠标悬浮时为目标元素区域添加一个覆盖层
  7076. $.fn.addHoverOverlay = function (target) {
  7077. let position = $(target).css('position');
  7078. let display = $(target).css('display');
  7079.  
  7080. this.hover(() => {
  7081. $(target)
  7082. .addClass('overlay')
  7083. .css('position', 'relative');
  7084. if (display == "inline" || display == "contents") {
  7085. $(target).css('display', 'block');
  7086. }
  7087. }, () => {
  7088. $(target)
  7089. .removeClass('overlay')
  7090. .css('position', position);
  7091. if (display == "inline" || display == "contents") {
  7092. $(target).css('display', display);
  7093. }
  7094. })
  7095. }
  7096.  
  7097. /**
  7098. * 为按钮设置图标
  7099. * @param {string} icon 图标
  7100. * @returns {JQuery<HTMLElement>} 按钮
  7101. */
  7102. $.fn.setButtonIcon = function (icon) {
  7103. let i = this.find("i");
  7104. if (i.length != 0 && i.hasClass("iconfont")) {
  7105. i.html(icon);
  7106. } else {
  7107. i = OJB_safeCreateJQElement(`<i>${icon}</i>`);
  7108. this.prepend(i);
  7109. }
  7110. return this;
  7111. }
  7112.  
  7113. /**
  7114. * 设置按钮为加载等待状态
  7115. */
  7116. $.fn.setButtonLoading = function () {
  7117. this.addClass("loading");
  7118. this.prop("disabled", true);
  7119. return this;
  7120. }
  7121.  
  7122. /**
  7123. * 解除按钮的加载等待状态
  7124. */
  7125. $.fn.setButtonLoaded = function () {
  7126. this.removeClass("loading");
  7127. this.prop("disabled", false);
  7128. return this;
  7129. }
  7130.  
  7131. /**
  7132. * 为按钮设置popover提示文本
  7133. * @param {string} text 文本
  7134. * @returns {JQuery<HTMLElement>} 按钮
  7135. */
  7136. $.fn.setButtonPopover = function (text) {
  7137. // find if has popover_content class element
  7138. let popover_content = this.find(".popover_content");
  7139. if (popover_content.length != 0) {
  7140. popover_content.text(text);
  7141. } else {
  7142. popover_content = OJB_safeCreateJQElement(`<span class="popover_content">${text}</span>`);
  7143. this.append(popover_content);
  7144. }
  7145. return this;
  7146. }
  7147.  
  7148. /**
  7149. * 获取MarkDown
  7150. * @returns {string} MarkDown
  7151. */
  7152. $.fn.getMarkdown = function () {
  7153. const markdown = this.data('markdown');
  7154. if (markdown === undefined) {
  7155. const htmlContent = this.html();
  7156. const newMarkdown = OJBetter.common.turndownService.turndown(htmlContent);
  7157. this.data('markdown', newMarkdown);
  7158. return newMarkdown;
  7159. }
  7160. return markdown;
  7161. }
  7162.  
  7163. // 设置按钮状态
  7164. $.fn.setButtonState = function (state, popoverText = null, disabled = false) {
  7165. this.data('buttonState', state)
  7166. .prop('disabled', disabled)
  7167. .css('cursor', disabled ? 'not-allowed' : 'pointer')
  7168. .removeClass('running success enabled error loading redo');
  7169. if (popoverText) this.setButtonPopover(popoverText);
  7170.  
  7171. if (state !== 'initial') this.addClass(state);
  7172. return this;
  7173. };
  7174.  
  7175. // 为按钮添加鼠标悬浮重试
  7176. $.fn.setHoverRedo = function () {
  7177. this.hover(() => {
  7178. prevState = this.getButtonState();
  7179. if (prevState !== "normal" && prevState !== "running") {
  7180. this.setButtonState('redo');
  7181. }
  7182. }, () => {
  7183. const currentState = this.getButtonState();
  7184. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7185. this.setButtonState(prevState);
  7186. prevState = null;
  7187. }
  7188. });
  7189. };
  7190.  
  7191. // 获取按钮状态
  7192. $.fn.getButtonState = function () {
  7193. return this.data('buttonState') || 'normal';
  7194. };
  7195.  
  7196. // 设置翻译按钮状态
  7197. $.fn.setTransButtonState = function (state, text = null) {
  7198. const popoverText = text || i18next.t(`trans.${state}`, { ns: 'button' });
  7199. const disabled = state === 'running' || state === 'loading';
  7200. this.setButtonState(state, popoverText, disabled);
  7201. return this;
  7202. };
  7203.  
  7204. // 存翻译结果
  7205. $.fn.pushResultToTransButton = function (result) {
  7206. let resultStack = this.data('resultStack');
  7207. if (!resultStack) resultStack = [];
  7208. resultStack.push(result);
  7209. this.data('resultStack', resultStack);
  7210. }
  7211.  
  7212. // 获取翻译结果
  7213. $.fn.getResultFromTransButton = function () {
  7214. return this.data('resultStack');
  7215. }
  7216.  
  7217. // 标记为不自动翻译
  7218. $.fn.setNotAutoTranslate = function () {
  7219. this.data('notAutoTranslate', true);
  7220. }
  7221.  
  7222. // 获取是否为不自动翻译
  7223. $.fn.getNotAutoTranslate = function () {
  7224. return this.data('notAutoTranslate');
  7225. }
  7226.  
  7227. // 判断是否已经翻译
  7228. $.fn.IsTranslated = function () {
  7229. if (this.hasAttr('translated')) {
  7230. return true;
  7231. } else {
  7232. return false;
  7233. }
  7234. }
  7235.  
  7236. // 判断是否为评论区按钮
  7237. $.fn.IsCommentButton = function () {
  7238. let isCommentButton = this.data('isCommentButton');
  7239. if (isCommentButton == undefined) {
  7240. this.parents('.comments').length > 0 ? isCommentButton = true : isCommentButton = false;
  7241. this.data('isCommentButton', isCommentButton);
  7242. }
  7243. return isCommentButton;
  7244. }
  7245.  
  7246. // 按钮点击效果
  7247. $(document).on('mousedown', '.ojb_btn', function () {
  7248. $(this).addClass('active').on('animationend', () => $(this).removeClass('active'));
  7249. });
  7250. }
  7251.  
  7252. /**
  7253. * 添加题目markdown转换/复制/翻译按钮面板
  7254. * @param {HTMLElement} element 需要添加按钮面板的元素
  7255. * @param {string} suffix 按钮面板id后缀
  7256. * @param {string} type 按钮面板添加位置
  7257. * @param {boolean} is_simple 是否是简单模式
  7258. * @returns {object} 返回按钮面板元素
  7259. */
  7260. function addButtonPanel(element, suffix, type, is_simple = false) {
  7261. let text;
  7262. if (OJBetter.translation.comment.transMode == "1") text = i18next.t('trans.segment', { ns: 'button' });
  7263. else if (OJBetter.translation.comment.transMode == "2") text = i18next.t('trans.select', { ns: 'button' });
  7264. else text = i18next.t('trans.normal', { ns: 'button' });
  7265.  
  7266. let panel = OJB_safeCreateJQElement(`<div class='html2md-panel input-output-copier ${is_simple ? 'is_simple' : ''}'></div>`);
  7267. let viewButton = OJB_safeCreateJQElement(`
  7268. <button class='ojb_btn ojb_btn_popover top' id='html2md-view${suffix}'>
  7269. <i class="iconfont">&#xe7e5;</i>
  7270. <span class="popover_content">${i18next.t('md.normal', { ns: 'button' })}</span>
  7271. </button>`);
  7272. let copyButton = OJB_safeCreateJQElement(`
  7273. <button class='ojb_btn ojb_btn_popover top' id='html2md-cb${suffix}'>
  7274. <i class="iconfont">&#xe608;</i>
  7275. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  7276. </button>`);
  7277. let translateButton = OJB_safeCreateJQElement(`
  7278. <button class='ojb_btn translateButton ojb_btn_popover top' id='translateButton${suffix}'>
  7279. <i class="iconfont">&#xe6be;</i>
  7280. <span class="popover_content">${text}</span>
  7281. </button>`);
  7282. if (!is_simple) panel.append(viewButton);
  7283. if (!is_simple) panel.append(copyButton);
  7284. panel.append(translateButton);
  7285. if (type === "this_level") {
  7286. $(element).before(panel);
  7287. } else if (type === "child_level") {
  7288. $(element).prepend(panel);
  7289. }
  7290.  
  7291. return {
  7292. panel: panel,
  7293. viewButton: viewButton,
  7294. copyButton: copyButton,
  7295. translateButton: translateButton
  7296. }
  7297. }
  7298.  
  7299. /**
  7300. * 添加MD视图按钮
  7301. * @param {JQuery<HTMLElement>} button 按钮
  7302. * @param {JQuery<HTMLElement>} element 目标元素
  7303. * @param {string} suffix id后缀
  7304. * @param {string} type 类型
  7305. * @returns {void}
  7306. */
  7307. async function addButtonWithHTML2MD(button, element, suffix, type) {
  7308. /**
  7309. * 改变按钮状态
  7310. * @param {string} state 状态
  7311. */
  7312. function changeButtonState(state) {
  7313. if (state == "loading") {
  7314. button.setButtonLoading();
  7315. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7316. } else if (state == "loaded") {
  7317. button.setButtonLoaded();
  7318. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7319. } else if (state == "normal") {
  7320. button.removeClass("enabled");
  7321. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7322. } else if (state == "mdView") {
  7323. button.addClass("enabled");
  7324. button.setButtonPopover(i18next.t('md.reduction', { ns: 'button' }));
  7325. } else if (state == "disabled") {
  7326. button.prop("disabled", true);
  7327. button.setButtonPopover(i18next.t('md.disabled', { ns: 'button' }));
  7328. }
  7329. }
  7330.  
  7331. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7332. changeButtonState("disabled");
  7333. return;
  7334. } else {
  7335. changeButtonState("loading");
  7336. await waitForMathJaxIdle();
  7337. changeButtonState("loaded");
  7338. }
  7339.  
  7340. button.click(OJB_debounce(function () {
  7341. var target = $(element).get(0);
  7342.  
  7343. /**
  7344. * 检查是否是MarkDown视图
  7345. * @returns {boolean} 是否是MarkDown视图
  7346. */
  7347. function checkViewmd() {
  7348. if ($(element).attr("viewmd") === "true") {
  7349. return true;
  7350. } else {
  7351. return false;
  7352. }
  7353. }
  7354.  
  7355. /**
  7356. * 设置是否是MarkDown视图
  7357. * @param {boolean} value 是否是MarkDown视图
  7358. * @returns {void}
  7359. */
  7360. function setViewmd(value) {
  7361. $(element).attr("viewmd", value);
  7362. if (value) {
  7363. changeButtonState("mdView");
  7364. } else {
  7365. changeButtonState("normal");
  7366. }
  7367. }
  7368.  
  7369. if (checkViewmd()) {
  7370. setViewmd(false);
  7371. $(element).next(".mdViewContent").remove();
  7372. $(element).show();
  7373. } else {
  7374. setViewmd(true);
  7375. var markdown = $(element).getMarkdown();
  7376. var mdViewContent = OJB_safeCreateJQElement(`<span class="mdViewContent" style="width:auto; height:auto;">${markdown}</span>`);
  7377. $(element).after(mdViewContent);
  7378. $(element).hide();
  7379. }
  7380. }));
  7381.  
  7382. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7383. button.addHoverOverlay($(element));
  7384. }
  7385. }
  7386.  
  7387. /**
  7388. * 添加复制按钮
  7389. * @param {JQuery<HTMLElement>} button 按钮
  7390. * @param {JQuery<HTMLElement>} element 目标元素
  7391. * @param {string} suffix 后缀
  7392. * @param {string} type 类型
  7393. */
  7394. async function addButtonWithCopy(button, element, suffix, type) {
  7395. /**
  7396. * 改变按钮状态
  7397. * @param {string} state 状态
  7398. */
  7399. function changeButtonState(state) {
  7400. if (state == "loading") {
  7401. button.setButtonLoading();
  7402. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7403. } else if (state == "loaded") {
  7404. button.setButtonLoaded();
  7405. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7406. } else if (state == "normal") {
  7407. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7408. } else if (state == "copied") {
  7409. button.setButtonPopover(i18next.t('copy.copied', { ns: 'button' }));
  7410. } else if (state == "disabled") {
  7411. button.prop("disabled", true);
  7412. button.setButtonPopover(i18next.t('copy.disabled', { ns: 'button' }));
  7413. }
  7414. }
  7415.  
  7416. // 等待MathJax队列完成
  7417. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7418. changeButtonState("disabled");
  7419. return;
  7420. } else {
  7421. changeButtonState("loading");
  7422. await waitForMathJaxIdle();
  7423. changeButtonState("loaded");
  7424. }
  7425.  
  7426. button.click(OJB_debounce(function () {
  7427. var target = $(element).get(0);
  7428.  
  7429. var markdown = $(element).getMarkdown();
  7430.  
  7431. GM_setClipboard(markdown);
  7432.  
  7433. $(this).addClass("success");
  7434. changeButtonState("copied");
  7435.  
  7436.  
  7437. // 更新复制按钮文本
  7438. setTimeout(() => {
  7439. $(this).removeClass("success");
  7440. changeButtonState("normal")
  7441. }, 2000);
  7442. }));
  7443.  
  7444. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7445. button.addHoverOverlay($(element));
  7446. }
  7447. }
  7448.  
  7449. /**
  7450. * 添加翻译按钮
  7451. * @param {JQuery<HTMLElement>} button 按钮
  7452. * @param {JQuery<HTMLElement>} element 目标元素
  7453. * @param {string} suffix 后缀
  7454. * @param {string} type 类型
  7455. * @param {boolean} is_comment 是否是评论
  7456. */
  7457. async function addButtonWithTranslation(button, element, suffix, type, is_comment = false) {
  7458. /**
  7459. * 添加可指定翻译服务的方法调用
  7460. * @param {string} translation 翻译服务
  7461. */
  7462. button.data("translatedItBy", function (translation) {
  7463. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7464. OJBetter.common.taskQueue.addTask(translation, () => transTask(button, element, type, is_comment, translation), translation == 'openai');
  7465. });
  7466.  
  7467. // 等待MathJax队列完成
  7468. button.setButtonLoading();
  7469. await waitForMathJaxIdle();
  7470. button.setButtonLoaded();
  7471.  
  7472. // 标记目标文本区域不自动翻译
  7473. {
  7474. let text;
  7475. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7476. text = $(element).html();
  7477. } else {
  7478. text = $(element).getMarkdown();
  7479. }
  7480. let length = text.length;
  7481. if (length > OJBetter.translation.auto.shortTextLength || isEmptyText(text) || $(element).find('.spoiler').length > 0) {
  7482. button.setNotAutoTranslate();
  7483. }
  7484. // button.after(`<span>${length}</span>`); // 显示字符数
  7485. }
  7486.  
  7487. button.click(OJB_debounce(async function () {
  7488. // 重新翻译
  7489. let resultStack = $(this).getResultFromTransButton();
  7490. if (resultStack) {
  7491. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected");
  7492. for (let item of resultStack) {
  7493. if (OJBetter.translation.retransAction == "0") {
  7494. // 选段翻译不直接移除旧结果
  7495. if (OJBetter.translation.comment.transMode == "2") {
  7496. // 只移除即将要翻译的段的结果
  7497. if (pElements.is(item.translateDiv.getDiv().prev())) {
  7498. item.translateDiv.close();
  7499. }
  7500. } else {
  7501. item.translateDiv.close();
  7502. $($(element)).find(".translate-problem-statement, .translate-problem-statement-panel").remove();
  7503. }
  7504. } else {
  7505. item.translateDiv.foldMainDiv();
  7506. }
  7507. }
  7508. }
  7509.  
  7510. // 翻译
  7511. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7512. OJBetter.common.taskQueue.addTask(OJBetter.translation.choice, () => transTask(button, element, type, is_comment), OJBetter.translation.choice == 'openai');
  7513. }));
  7514.  
  7515. // 重新翻译提示
  7516. let prevState;
  7517. button.hover(() => {
  7518. prevState = button.getButtonState();
  7519. if (prevState !== "normal" && prevState !== "running") {
  7520. button.setTransButtonState('redo');
  7521. }
  7522. }, () => {
  7523. const currentState = button.getButtonState();
  7524. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7525. button.setTransButtonState(prevState);
  7526. prevState = null;
  7527. }
  7528. });
  7529.  
  7530. // 目标区域指示
  7531. if (OJBetter.preference.hoverTargetAreaDisplay) {
  7532. button.addHoverOverlay($(element));
  7533. }
  7534.  
  7535. // 翻译右键切换菜单
  7536. $(document).on('contextmenu', '#translateButton' + suffix, function (e) {
  7537. e.preventDefault();
  7538.  
  7539. // 是否为评论的翻译
  7540. let is_comment = button.IsCommentButton();
  7541.  
  7542. // 移除旧的
  7543. if (!$(e.target).closest('.OJBetter_contextmenu').length) {
  7544. $('.OJBetter_contextmenu').remove();
  7545. }
  7546.  
  7547. var menu = $('<div class="OJBetter_contextmenu"></div>');
  7548. var translations = [
  7549. { value: 'deepl', name: i18next.t('translation.options.services.deepl', { ns: 'settings' }) },
  7550. { value: 'iflyrec', name: i18next.t('translation.options.services.iflyrec', { ns: 'settings' }) },
  7551. { value: 'youdao', name: i18next.t('translation.options.services.youdao', { ns: 'settings' }) },
  7552. { value: 'google', name: i18next.t('translation.options.services.google', { ns: 'settings' }) },
  7553. { value: 'caiyun', name: i18next.t('translation.options.services.caiyun', { ns: 'settings' }) },
  7554. { value: 'openai', name: i18next.t('translation.options.services.openai.name', { ns: 'settings' }) }
  7555. ];
  7556.  
  7557. // Function to check if the service supports the target language
  7558. function supportsTargetLanguage(service, targetLang) {
  7559. return OJBetter.supportList.translationSupport[service] && OJBetter.supportList.translationSupport[service][targetLang] !== undefined;
  7560. }
  7561.  
  7562. if (is_comment) {
  7563. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="0">
  7564. <span class="OJBetter_contextmenu_label_text">
  7565. ${i18next.t('translation.preference.comment_translation_choice.services.follow', { ns: 'settings' })}
  7566. </span></label>`);
  7567. menu.append(label);
  7568. }
  7569. translations.forEach(function (translation) {
  7570. if (supportsTargetLanguage(translation.value, OJBetter.translation.targetLang)) {
  7571. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="${translation.value}">
  7572. <span class="OJBetter_contextmenu_label_text">${translation.name}</span></label>`);
  7573. menu.append(label);
  7574. }
  7575. });
  7576.  
  7577. // 初始化
  7578. if (is_comment) {
  7579. menu.find(`input[name="translation"][value="${OJBetter.translation.comment.choice}"]`).prop('checked', true);
  7580. } else {
  7581. menu.find(`input[name="translation"][value="${OJBetter.translation.choice}"]`).prop('checked', true);
  7582. }
  7583. menu.css({
  7584. top: e.pageY + 'px',
  7585. left: e.pageX + 'px'
  7586. }).appendTo('body');
  7587.  
  7588. $(document).one('change', 'input[name="translation"]', function () {
  7589. if (is_comment) {
  7590. OJBetter.translation.comment.choice = $('input[name="translation"]:checked').val();
  7591. GM_setValue("commentTranslationChoice", OJBetter.translation.comment.choice);
  7592. } else {
  7593. OJBetter.translation.choice = $('input[name="translation"]:checked').val();
  7594. GM_setValue("translation", OJBetter.translation.choice);
  7595. }
  7596. $('.OJBetter_contextmenu').remove();
  7597. });
  7598.  
  7599. // 点击区域外关闭菜单
  7600. function handleClick(event) {
  7601. if (!$(event.target).closest('.OJBetter_contextmenu').length) {
  7602. $('.OJBetter_contextmenu').remove();
  7603. $(document).off('change', 'input[name="translation"]');
  7604. } else {
  7605. $(document).one('click', handleClick);
  7606. }
  7607. }
  7608. $(document).one('click', handleClick);
  7609. });
  7610. }
  7611.  
  7612. /**
  7613. * 创建翻译任务
  7614. * @param {JQuery<HTMLElement>} button 按钮
  7615. * @param {HTMLElement} element 目标元素
  7616. * @param {string} type 类型
  7617. * @param {boolean} is_comment 是否是评论
  7618. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7619. */
  7620. async function transTask(button, element, type, is_comment, overrideTrans) {
  7621. /** @type {HTMLElement} 目标元素 */
  7622. let target;
  7623. /**
  7624. * 错误计数数据结构
  7625. * @typedef {Object} count
  7626. * @property {number} errerNum 错误数量
  7627. * @property {number} skipNum 跳过数量
  7628. */
  7629. const count = {
  7630. errerNum: 0,
  7631. skipNum: 0
  7632. };
  7633. if (OJBetter.translation.comment.transMode == "1") {
  7634. // 分段翻译
  7635. let pElements = $(element).find("p:not(li p), li, .OJBetter_acmsguru");
  7636. for (let i = 0; i < pElements.length; i++) {
  7637. target = $(pElements[i]).eq(0).clone();
  7638. element_node = pElements[i];
  7639. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7640. }
  7641. } else if (OJBetter.translation.comment.transMode == "2") {
  7642. // 选段翻译
  7643. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected, .OJBetter_acmsguru");
  7644. for (let i = 0; i < pElements.length; i++) {
  7645. target = $(pElements[i]).eq(0).clone();
  7646. element_node = pElements[i];
  7647. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7648. }
  7649. $(element).find("p.block_selected:not(li p), li.block_selected").removeClass('block_selected');
  7650. } else {
  7651. // 普通翻译
  7652. target = $(element).eq(0).clone();
  7653. if (type === "child_level") $(target).children(':first').remove();
  7654. element_node = $($(element)).get(0);
  7655. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7656. }
  7657.  
  7658. // 翻译完成
  7659. if (!count.errerNum && !count.skipNum) {
  7660. button.setTransButtonState('success');
  7661. }
  7662. }
  7663.  
  7664. /**
  7665. * 翻译处理
  7666. * @param {JQuery<HTMLElement>} button 按钮
  7667. * @param {HTMLElement} target 目标元素
  7668. * @param {HTMLElement} element_node 目标节点
  7669. * @param {string} type 类型
  7670. * @param {boolean} is_comment 是否是评论
  7671. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7672. */
  7673. async function process(button, target, element_node, type, is_comment, count, overrideTrans) {
  7674. if (type === "child_level") {
  7675. let div = $("<div>");
  7676. $(element_node).append(div);
  7677. element_node = div.get(0);
  7678. }
  7679.  
  7680. //是否跳过折叠块
  7681. if ($(target).find('.spoiler').length > 0) {
  7682. const shouldSkip = await OJB_createDialog(
  7683. i18next.t('skipFold.title', { ns: 'dialog' }),
  7684. i18next.t('skipFold.content', { ns: 'dialog' }),
  7685. [
  7686. i18next.t('skipFold.buttons.0', { ns: 'dialog' }),
  7687. i18next.t('skipFold.buttons.1', { ns: 'dialog' })
  7688. ],
  7689. true
  7690. ); //跳过折叠块确认
  7691. if (shouldSkip) {
  7692. $(target).find('.spoiler').remove();
  7693. } else {
  7694. $(target).find('.html2md-panel').remove();
  7695. }
  7696. }
  7697.  
  7698. // 等待并获取结果
  7699. button.setTransButtonState('running');
  7700. const result = await blockProcessing(button, target, element_node, is_comment, overrideTrans);
  7701. button.pushResultToTransButton(result);
  7702.  
  7703. if (result.status == "error") count.errerNum += 1;
  7704. else if (result.status == "skip") count.skipNum += 1;
  7705. $(target).remove();
  7706. }
  7707.  
  7708. /**
  7709. * 块处理
  7710. * @param {JQuery<HTMLElement>} button
  7711. * @param {HTMLElement} target 目标元素
  7712. * @param {HTMLElement} element_node 目标节点
  7713. * @param {boolean} is_comment 是否是评论
  7714. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7715. * @returns {TranslateResult} 翻译结果对象
  7716. */
  7717. async function blockProcessing(button, target, element_node, is_comment, overrideTrans) {
  7718. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7719. target.markdown = $(target).html();
  7720. } else if (!target.markdown) {
  7721. target.markdown = OJBetter.common.turndownService.turndown($(target).html());
  7722. }
  7723.  
  7724. const result = await translateProblemStatement(target.markdown, element_node, is_comment, overrideTrans);
  7725. if (result.status == "skip") {
  7726. button.setTransButtonState('error', i18next.t('trans.tooLong', { ns: 'button' }));
  7727. result.translateDiv.close();
  7728. } else if (result.status == "error" || !result.rawData.done) {
  7729. result.translateDiv.setError();
  7730. result.translateDiv.setRawData(result.rawData);
  7731. result.translateDiv.showDebugButton();
  7732. button.setTransButtonState('error', i18next.t('trans.error', { ns: 'button' }));
  7733. $(target).remove();
  7734. }
  7735. return result;
  7736. }
  7737.  
  7738. /**
  7739. * 选段翻译支持
  7740. */
  7741. async function multiChoiceTranslation() {
  7742. GM_addStyle(`
  7743. .topic .content .ttypography {
  7744. overflow: initial;
  7745. }
  7746. `);
  7747.  
  7748. $(document).on('click', 'p, li:not(:has(.comment)), .OJBetter_acmsguru', function (e) {
  7749. let $this = $(this);
  7750. e.stopPropagation();
  7751. if ($this.hasClass('block_selected')) {
  7752. $this.removeClass('block_selected');
  7753. // 移除对应的按钮
  7754. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + $this.attr('OJBetter_p_id'));
  7755. } else {
  7756. let id = OJB_getRandomNumber(8);
  7757. $this.attr('OJBetter_p_id', id);
  7758. $this.addClass('block_selected');
  7759. // 添加按钮
  7760. let menu = OJB_safeCreateJQElement(`<div class="OJBetter_MiniTranslateButton" id='translateButton_selected_${id}'>${translateIcon}</div>`)
  7761. .css({
  7762. left: $($this).outerWidth(true) + $($this).position().left + 10 + 'px',
  7763. });
  7764. $this.before(menu);
  7765.  
  7766. $("#translateButton_selected_" + id).click(async function () {
  7767. // 处理旧的结果
  7768. if ($this.attr('translated')) {
  7769. let result = $this.data("resultData");
  7770. if (OJBetter.translation.retransAction == "0") {
  7771. result.translateDiv.close();
  7772. } else {
  7773. result.translateDiv.foldMainDiv();
  7774. }
  7775. }
  7776. // 翻译
  7777. let target = $this.eq(0).clone();
  7778. let result = await blockProcessing(OJBetter.translation.choice, target, $this.eq(0), $("#translateButton_selected_" + id), false);
  7779. $this.data("resultData", result);
  7780. $this.removeClass('block_selected');
  7781. // 移除对应的按钮
  7782. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + id);
  7783. $this.attr('translated', '1'); // 标记已翻译
  7784. });
  7785. }
  7786. });
  7787. }
  7788.  
  7789. /**
  7790. * 为acmsguru题面重新划分div
  7791. */
  7792. async function acmsguruReblock() {
  7793. if (OJBetter.translation.comment.transMode == '0') {
  7794. // 普通模式下的划分方式
  7795. var html = $('.ttypography').children().html();
  7796. var separator = /(<div align="left" style="margin-top: 1\.0em;"><b>.*?<\/b><\/div>)/g;
  7797. var result = html.split(separator); // 分割代码
  7798. var outputHtml = '';
  7799. var header = '';
  7800. for (var i = 0; i < result.length; i++) {
  7801. if (separator.test(result[i])) {
  7802. header = result[i];
  7803. continue;
  7804. }
  7805. outputHtml += '<div class="ttypography">' + header + result[i] + '</div>';
  7806. header = '';
  7807. }
  7808. $('.ttypography').html(outputHtml);
  7809. }
  7810. else {
  7811. // 分段/选段模式下的划分方式
  7812. $('.ttypography').children().each(function () {
  7813. var html = $(this).html();
  7814. var replacedHtml = html.replace(/(?:<\/div>|<br><br>)(?<text>[\s\S]+?)(?=<br><br>)/g,
  7815. '<div align="left" class="OJBetter_acmsguru" >$<text></div>');
  7816. $(this).html(replacedHtml);
  7817. });
  7818. }
  7819. }
  7820.  
  7821. /**
  7822. * 添加MD/复制/翻译按钮
  7823. */
  7824. async function addConversionButton() {
  7825. let promises = []; // 用于收集所有的 Promise
  7826.  
  7827. // 题目页添加按钮
  7828. if (OJBetter.typeOfPage.is_problem) {
  7829. let exContentsPageClasses = ["sample-tests"];
  7830. $('.problem-statement').children('div').each((i, e) => {
  7831. var className = $(e).attr('class');
  7832. if (!exContentsPageClasses.includes(className)) {
  7833. var id = "_problem_" + OJB_getRandomNumber(8);
  7834. let panel = addButtonPanel(e, id, "this_level");
  7835. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7836. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7837. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7838. if (i == 0) panel.translateButton.setNotAutoTranslate(); // 题目标题块跳过,不自动翻译
  7839. }
  7840. });
  7841. }
  7842. // 添加按钮到ttypography部分
  7843. $(".ttypography").each((i, e) => {
  7844. // 是否为评论
  7845. let is_comment = false;
  7846. if ($(e).parents('.comments').length > 0) is_comment = true;
  7847. // 题目页不添加
  7848. if (!OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_acmsguru) {
  7849. let id = "_ttypography_" + OJB_getRandomNumber(8);
  7850. let panel = addButtonPanel(e, id, "this_level");
  7851. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7852. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7853. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level", is_comment));
  7854. }
  7855. });
  7856.  
  7857. // 完整题目集页特殊处理
  7858. if (OJBetter.typeOfPage.is_completeProblemset) {
  7859. let exContentsPageClasses = ["sample-tests"];
  7860. $('.problem-statement').each(function () {
  7861. $(this).children('div').each((i, e) => {
  7862. var className = $(e).attr('class');
  7863. if (!exContentsPageClasses.includes(className)) {
  7864. var id = "_problem_" + OJB_getRandomNumber(8);
  7865. let panel = addButtonPanel(e, id, "this_level");
  7866. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7867. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7868. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7869. if (i == 0) panel.translateButton.setNotAutoTranslate(); // 题目标题块跳过,不自动翻译
  7870. }
  7871. });
  7872. });
  7873. }
  7874.  
  7875. // 添加按钮到spoiler部分
  7876. $('.spoiler-content').each((i, e) => {
  7877. if ($(e).find('.html2md-panel').length === 0) {
  7878. let id = "_spoiler_" + OJB_getRandomNumber(8);
  7879. let panel = addButtonPanel(e, id, "child_level");
  7880. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "child_level"));
  7881. promises.push(addButtonWithCopy(panel.copyButton, e, id, "child_level"));
  7882. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  7883. }
  7884. });
  7885.  
  7886. // 添加按钮到titled部分
  7887. (function () {
  7888. var elements = [".Virtual.participation", ".Attention", ".Practice"];//只为部分titled添加
  7889. $.each(elements, (i, e) => {
  7890. $(e).each(function () {
  7891. let id = "_titled_" + OJB_getRandomNumber(8);
  7892. let nextDiv = $(e).next().children().get(0);
  7893. if (!nextDiv) return;
  7894. let panel = addButtonPanel(nextDiv, id, "child_level", true);
  7895. promises.push(addButtonWithTranslation(panel.translateButton, nextDiv, id, "child_level"));
  7896. });
  7897. });
  7898. })();
  7899. if (OJBetter.typeOfPage.is_mSite) {
  7900. $("div[class='_IndexPage_notice']").each((i, e) => {
  7901. let id = "_titled_" + OJB_getRandomNumber(8);
  7902. let panel = addButtonPanel(e, id, "this_level", true);
  7903. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7904. });
  7905. }
  7906.  
  7907. // 添加按钮到比赛QA部分
  7908. $(".question-response").each((i, e) => {
  7909. let id = "_question_" + OJB_getRandomNumber(8);
  7910. let panel = addButtonPanel(e, id, "this_level", true);
  7911. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7912. });
  7913. if (OJBetter.typeOfPage.is_mSite) {
  7914. $("div._ProblemsPage_announcements table tbody tr:gt(0)").each((i, e) => {
  7915. var $nextDiv = $(e).find("td:first");
  7916. let id = "_question_" + OJB_getRandomNumber(8);
  7917. let panel = addButtonPanel($nextDiv, id, "this_level", true);
  7918. promises.push(addButtonWithTranslation(panel.translateButton, $nextDiv, id, "this_level"));
  7919. });
  7920. }
  7921.  
  7922. // 添加按钮到弹窗confirm-proto部分
  7923. $(".confirm-proto").each((i, e) => {
  7924. let id = "_titled_" + OJB_getRandomNumber(8);
  7925. var $nextDiv = $(e).children().get(0);
  7926. let panel = addButtonPanel($nextDiv, id, "this_level", true);
  7927. promises.push(addButtonWithTranslation(panel.translateButton, $nextDiv, id, "this_level"));
  7928. });
  7929.  
  7930. // 添加按钮到_CatalogHistorySidebarFrame_item部分
  7931. $("._CatalogHistorySidebarFrame_item").each((i, e) => {
  7932. let id = "_history_sidebar_" + OJB_getRandomNumber(8);
  7933. let panel = addButtonPanel(e, id, "this_level", true);
  7934. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7935. });
  7936.  
  7937. $(".problem-lock-link").on("click", function () {
  7938. $(".popup .content div").each((i, e) => {
  7939. let id = "_popup_" + OJB_getRandomNumber(8);
  7940. let panel = addButtonPanel(e, id, "this_level", true);
  7941. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7942. });
  7943. });
  7944.  
  7945. // 添加按钮到弹窗alert部分
  7946. $(".alert:not(.OJBetter_alert)").each((i, e) => {
  7947. let id = "_alert_" + OJB_getRandomNumber(8);
  7948. let panel = addButtonPanel(e, id, "child_level", true);
  7949. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  7950. });
  7951.  
  7952. // 添加按钮到talk-text部分
  7953. $(".talk-text").each((i, e) => {
  7954. let id = "_talk-text_" + OJB_getRandomNumber(8);
  7955. let panel = addButtonPanel(e, id, "child_level", true);
  7956. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  7957. });
  7958.  
  7959. return Promise.all(promises).catch(error => {
  7960. console.error("One or more of the Add Button operations failed: ", error);
  7961. });
  7962. };
  7963.  
  7964. /**
  7965. * 等待LaTeX渲染队列全部完成
  7966. * @returns {Promise} 完成渲染
  7967. */
  7968. function waitForMathJaxIdle() {
  7969. return new Promise((resolve, reject) => {
  7970. // 检查MathJax对象是否存在
  7971. const checkMathJaxExists = () => {
  7972. if (typeof MathJax === 'undefined') {
  7973. // 如果MathJax不存在,稍后再次检查
  7974. OJB_delay(100).then(checkMathJaxExists);
  7975. } else {
  7976. // MathJax存在,开始监视渲染队列
  7977. startMonitoringQueue();
  7978. }
  7979. };
  7980.  
  7981. // 开始监视MathJax渲染队列
  7982. const startMonitoringQueue = () => {
  7983. const intervalId = setInterval(() => {
  7984. const queue = MathJax.Hub.queue;
  7985. if (queue.pending === 0 && queue.running === 0) {
  7986. clearInterval(intervalId);
  7987. resolve();
  7988. }
  7989. }, 100);
  7990. };
  7991.  
  7992. // 开始检查MathJax对象
  7993. checkMathJaxExists();
  7994. });
  7995. }
  7996.  
  7997. /**
  7998. * 翻译结果面板
  7999. */
  8000. class TranslateDiv {
  8001. /**
  8002. * 构造函数
  8003. * @param {string} id 指定翻译框的id
  8004. */
  8005. constructor(id) {
  8006. this.id = id;
  8007. this.div = $('<div>').attr('id', id).addClass('translateDiv bounce-in');
  8008. if (!OJBetter.typeOfPage.is_completeProblemset) {
  8009. this.div.addClass('input-output-copier');
  8010. }
  8011. this.panelDiv = $('<div>').addClass('translate-problem-statement-panel');
  8012. this.div.append(this.panelDiv);
  8013.  
  8014. // 主要信息
  8015. this.mainDiv = $('<div>').addClass('translate-problem-statement');
  8016. this.span = $('<span>');
  8017. this.mainDiv.append(this.span);
  8018. this.div.append(this.mainDiv);
  8019. this.mainDivState = {
  8020. current: 'transHTML',
  8021. transHTML: '',
  8022. rawDataHTML: ''
  8023. };
  8024.  
  8025. // 顶栏信息
  8026. this.topText = $('<div>').addClass('topText');
  8027. this.panelDiv.append(this.topText);
  8028.  
  8029. // 右侧
  8030. this.rightDiv = $('<div>').css('display', 'flex');
  8031. this.panelDiv.append(this.rightDiv);
  8032. this.debugButton = OJB_safeCreateJQElement(`
  8033. <button class='ojb_btn ojb_btn_popover top'>
  8034. <i class="iconfont">&#xe641;</i>
  8035. <span class="popover_content">${i18next.t('rawData.normal', { ns: 'button' })}</span>
  8036. </button>`).hide();
  8037. this.rightDiv.append(this.debugButton);
  8038. this.queryBalanceButton = OJB_safeCreateJQElement(`
  8039. <button class='ojb_btn ojb_btn_popover top'>
  8040. <i class="iconfont">&#xe6ae;</i>
  8041. <span class="popover_content">${i18next.t('queryBalance.normal', { ns: 'button' })}</span>
  8042. </button>`).hide();
  8043. this.rightDiv.append(this.queryBalanceButton);
  8044. this.copyButton = OJB_safeCreateJQElement(`
  8045. <button class='ojb_btn ojb_btn_popover top'>
  8046. <i class="iconfont">&#xe608;</i>
  8047. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  8048. </button>`);
  8049. this.rightDiv.append(this.copyButton);
  8050. this.upButton = OJB_safeCreateJQElement(`
  8051. <button class='ojb_btn ojb_btn_popover top'>
  8052. <i class="iconfont">&#xe601;</i>
  8053. <span class="popover_content">${i18next.t('fold.normal', { ns: 'button' })}</span>
  8054. </button>`);
  8055. this.rightDiv.append(this.upButton);
  8056. this.closeButton = OJB_safeCreateJQElement(`
  8057. <button class='ojb_btn ojb_btn_popover top'>
  8058. <i class="iconfont">&#xe614;</i>
  8059. <span class="popover_content">${i18next.t('close.normal', { ns: 'button' })}</span>
  8060. </button>`);
  8061. this.rightDiv.append(this.closeButton);
  8062. }
  8063.  
  8064. /**
  8065. * 获取翻译框
  8066. * @returns {JQuery<HTMLElement>} 返回翻译框
  8067. */
  8068. getDiv() {
  8069. return this.div;
  8070. }
  8071.  
  8072. /**
  8073. * 设置翻译框顶部的文本
  8074. * @param {string} text 翻译框顶部的文本
  8075. */
  8076. setTopText(text) {
  8077. this.div.attr("data-topText", text);
  8078. this.topText.text(text);
  8079. }
  8080.  
  8081. /**
  8082. * 获取翻译框顶部的文本
  8083. * @returns {string} 返回翻译框顶部的文本
  8084. */
  8085. getTopText() {
  8086. return this.topText.text();
  8087. }
  8088.  
  8089. /**
  8090. * 渲染一个元素内的LaTeX公式
  8091. * @param {HTMLElement} element 元素
  8092. */
  8093. renderLaTeX(element) {
  8094. MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]);
  8095. }
  8096.  
  8097. /**
  8098. * 更新翻译框内容
  8099. * @param {string} text 文本内容
  8100. * @param {boolean} is_escapeHTML 是否转义HTML标签,为true则HTML标签将作为普通文本处理,默认为true
  8101. * @param {boolean} is_renderLaTeX 是否渲染LaTeX,为true则会渲染LaTeX,默认为true
  8102. */
  8103. updateTranslateDiv(text, is_escapeHTML = true, is_renderLaTeX = true,) {
  8104. // 渲染MarkDown
  8105. let md = window.markdownit({
  8106. html: !is_escapeHTML,
  8107. });
  8108. if (!text) text = "";
  8109. let html = md.render(text);
  8110. this.mainDiv.html(html);
  8111. // 渲染Latex
  8112. if (is_renderLaTeX) {
  8113. // MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.mainDiv.get(0)]);
  8114. this.renderLaTeX(this.mainDiv.get(0));
  8115. }
  8116. // // 渲染代码块中的公式 (AtCoder)
  8117. // this.mainDiv.find('pre code').each((index, element) => {
  8118. // const codeText = $(element).text();
  8119. // const latexPattern = /\$\$(\\.|[^\$])*?\$\$|\$(\\.|[^\$])*?\$/;
  8120. // if (latexPattern.test(codeText)) {
  8121. // this.renderLaTeX(element);
  8122. // }
  8123. // });
  8124. }
  8125.  
  8126. /**
  8127. * 关闭元素
  8128. */
  8129. close() {
  8130. this.closeButton.click();
  8131. }
  8132.  
  8133. /**
  8134. * 注册收起按钮事件
  8135. */
  8136. registerUpButtonEvent() {
  8137. this.upButton.on("click", () => {
  8138. // 如果没有reverse类,说明是展开状态
  8139. if (!this.upButton.hasClass("reverse")) {
  8140. // 执行收起操作
  8141. this.upButton.addClass("reverse");
  8142. this.upButton.setButtonState('initial', i18next.t('fold.unfold', { ns: 'button' }));
  8143. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8144. } else {
  8145. // 执行展开操作
  8146. this.upButton.removeClass("reverse");
  8147. this.upButton.setButtonState('initial', i18next.t('fold.normal', { ns: 'button' }));
  8148. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8149. }
  8150. });
  8151. }
  8152.  
  8153. /**
  8154. * 注册关闭按钮事件
  8155. */
  8156. registerCloseButtonEvent() {
  8157. this.closeButton.on("click", () => {
  8158. $(this.div).remove();
  8159. $(this.panelDiv).remove();
  8160. if (OJBetter.typeOfPage.is_problem && OJBetter.translation.memory.enabled) {
  8161. OJBetter.translation.memory.ttTree.rmTransResultMap(this.id); // 移除ttTree中的数据
  8162. OJBetter.translation.memory.ttTree.refreshNode(".ttypography");
  8163. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新DB中的数据
  8164. }
  8165. });
  8166. }
  8167.  
  8168. /**
  8169. * 注册复制按钮事件
  8170. * @param {string} text 复制的文本
  8171. */
  8172. registerCopyButtonEvent(text) {
  8173. this.copyButton.on("click", () => {
  8174. GM_setClipboard(text);
  8175. this.copyButton.setButtonState('success', i18next.t('copy.copied', { ns: 'button' }));
  8176. // 复制提示
  8177. setTimeout(() => {
  8178. this.copyButton.setButtonState('initial', i18next.t('copy.normal', { ns: 'button' }));
  8179. }, 2000);
  8180. });
  8181. }
  8182.  
  8183. /**
  8184. * 禁用复制按钮
  8185. */
  8186. disableCopyButton() {
  8187. this.copyButton.css({ 'fill': '#ccc' });
  8188. this.copyButton.off("click");
  8189. }
  8190.  
  8191. /**
  8192. * 设置面板为error状态
  8193. */
  8194. setError() {
  8195. this.div.addClass('error');
  8196. this.panelDiv.addClass('error');
  8197. this.mainDiv.addClass('error');
  8198. }
  8199.  
  8200. /**
  8201. * 设置原始数据数据
  8202. * @param {Object} Object 原始数据
  8203. */
  8204. setRawData(Object) {
  8205. this.mainDivState.rawDataHTML = $("<pre>").text(JSON.stringify(Object, null, 4)).get(0);
  8206. if (this.mainDivState.current === 'rawDataHTML') {
  8207. this.renderMainDiv();
  8208. }
  8209. }
  8210.  
  8211. /**
  8212. * 切换结果面板与原始数据面板
  8213. */
  8214. switchMainDiv() {
  8215. // 在切换之前,保存当前内容的状态
  8216. this.mainDivState[this.mainDivState.current] = this.mainDiv.html();
  8217. // 切换当前状态
  8218. this.debugButton.setButtonState(this.mainDivState.current === 'transHTML' ? 'enabled' : 'initial');
  8219. this.mainDivState.current = this.mainDivState.current === 'transHTML' ? 'rawDataHTML' : 'transHTML';
  8220. // 渲染新的当前状态
  8221. this.renderMainDiv();
  8222. }
  8223.  
  8224. // 渲染当前内容到 mainDiv
  8225. renderMainDiv() {
  8226. requestAnimationFrame(() => {
  8227. this.mainDiv.html(this.mainDivState[this.mainDivState.current]);
  8228. });
  8229. }
  8230.  
  8231. /**
  8232. * 注册debug按钮事件
  8233. */
  8234. registerDebugButtonEvent() {
  8235. this.debugButton.on("click", () => {
  8236. this.switchMainDiv();
  8237. });
  8238. }
  8239.  
  8240. /**
  8241. * 显示debug按钮
  8242. */
  8243. showDebugButton() {
  8244. this.debugButton.show();
  8245. this.registerDebugButtonEvent();
  8246. }
  8247.  
  8248. /**
  8249. * 注册查询余额按钮事件
  8250. * @param {function} callback 查询回调函数
  8251. */
  8252. registerQueryBalanceButtonEvent(callback) {
  8253. this.queryBalanceButton.on("click", async () => {
  8254. this.queryBalanceButton.setButtonState('loading', i18next.t('queryBalance.loading', { ns: 'button' }));
  8255. try {
  8256. const balance = await callback();
  8257. this.queryBalanceButton.setButtonState('success', `${i18next.t('queryBalance.success', { ns: 'button' })} ${balance}`);
  8258. } catch (error) {
  8259. this.queryBalanceButton.setButtonState('error', `${i18next.t('queryBalance.error', { ns: 'button' })} ${error.message}`);
  8260. }
  8261. });
  8262. }
  8263.  
  8264. /**
  8265. * 显示余额查询按钮
  8266. * @param {string} server 服务名称
  8267. */
  8268. showQueryBalanceButton(server) {
  8269. if (server == 'deepl') {
  8270. const quotaConfig = OJBetter.deepl.config.quota;
  8271. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8272. this.queryBalanceButton.show();
  8273. this.registerQueryBalanceButtonEvent(() => {
  8274. return queryServerBalance(OJBetter.deepl.config.quota);
  8275. });
  8276. }
  8277. } else if (server == 'openai') {
  8278. const quotaConfig = OJBetter.chatgpt.config.quota;
  8279. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8280. this.queryBalanceButton.show();
  8281. this.registerQueryBalanceButtonEvent(() => {
  8282. return queryServerBalance(OJBetter.chatgpt.config.quota);
  8283. });
  8284. }
  8285. }
  8286. }
  8287. }
  8288.  
  8289. // 元素关系树
  8290. class ElementsTree {
  8291. constructor(elements) {
  8292. this.node = [];
  8293. this.transResultMap = {};
  8294. this.index = 0;
  8295. this.tagNames = ["DIV", "P", "UL", "LI"]
  8296. this.init($(elements));
  8297. }
  8298.  
  8299. // Iterate through all elements, because there may be multiple ttypography
  8300. init(elements) {
  8301. elements.each((i, e) => {
  8302. this.node.push({}); // add one element
  8303. this.index = 0; // reset index
  8304. this.create(i, $(e));
  8305. });
  8306. }
  8307.  
  8308. // 刷新关系树
  8309. refreshNode(elements) {
  8310. this.node = [];
  8311. this.index = 0;
  8312. this.init($(elements));
  8313. }
  8314.  
  8315. // 创建节点间的关系树
  8316. create(i_, element) {
  8317. var prev = null;
  8318. var node = this.node[i_];
  8319. element.children().each((i, e) => {
  8320. // only add element with tagNames
  8321. if (this.tagNames.includes($(e).prop("tagName"))) {
  8322. prev = this.addNode(i_, prev, e);
  8323. }
  8324. // recursively child element
  8325. if ($(e).children().length > 0 && prev !== null) {
  8326. node[prev].firstChild = this.index;
  8327. this.create(i_, $(e));
  8328. }
  8329. });
  8330. }
  8331.  
  8332. // 向树中添加一个节点
  8333. addNode(i_, prev, e) {
  8334. let node = this.node[i_];
  8335. node[this.index] = {
  8336. prev: prev,
  8337. next: null,
  8338. firstChild: null,
  8339. type: $(e).prop("tagName"),
  8340. isTranslateDiv: $(e).hasClass("translateDiv"),
  8341. topText: $(e).attr("data-topText"),
  8342. id: $(e).attr("id"),
  8343. };
  8344.  
  8345. if (prev !== null) {
  8346. node[prev].next = this.index;
  8347. }
  8348.  
  8349. prev = this.index;
  8350.  
  8351. this.index++;
  8352. return prev;
  8353. }
  8354.  
  8355. getNodeData() {
  8356. return this.node;
  8357. }
  8358.  
  8359. setNodeData(node) {
  8360. this.node = node;
  8361. }
  8362.  
  8363. getTransResultMap() {
  8364. return this.transResultMap;
  8365. }
  8366.  
  8367. setTransResultMap(transResultMap) {
  8368. this.transResultMap = transResultMap;
  8369. }
  8370.  
  8371. rmTransResultMap(id) {
  8372. delete this.transResultMap[id];
  8373. }
  8374.  
  8375. addTransResultMap(id, text) {
  8376. this.transResultMap[id] = text;
  8377. }
  8378.  
  8379. getTranslateDivNum(ttTree) {
  8380. var num = 0;
  8381. for (var i in ttTree) {
  8382. if (ttTree[i].isTranslateDiv) {
  8383. num++;
  8384. }
  8385. }
  8386. return num;
  8387. }
  8388.  
  8389. // 恢复目标元素中的translateDiv
  8390. recover(elements) {
  8391. elements.each((i, e) => {
  8392. var ttTreeNode = this.node[i];
  8393. var missingTranslateDivs = this.getTranslateDivNum(ttTreeNode);
  8394. if (missingTranslateDivs > 0) {
  8395. this.recoverOneElement($(e), ttTreeNode);
  8396. }
  8397. });
  8398. }
  8399.  
  8400. recoverOneElement(element, ttTreeNode) {
  8401. this.recoverOneFork(element.children().eq(0), ttTreeNode, 0);
  8402. }
  8403.  
  8404. // 恢复一个分支
  8405. recoverOneFork(pElement, ttTreeNode, index) {
  8406. do {
  8407. // only recover element with tagNames
  8408. if (!this.tagNames.includes(pElement.prop("tagName"))) {
  8409. if (pElement.next().length > 0) {
  8410. pElement = pElement.next();
  8411. } else {
  8412. return;
  8413. }
  8414. }
  8415. if (!ttTreeNode[index] || pElement.prop("tagName") !== ttTreeNode[index].type) {
  8416. // console.warn(`元素不存在或类型不同, 元素结构可能已经发生了变化: \nindex: ${index}`, pElement);
  8417. return;
  8418. } else {
  8419. // recursively child element
  8420. var node = ttTreeNode[index];
  8421. if (node.firstChild !== null) {
  8422. this.recoverOneFork(
  8423. pElement.children().eq(0),
  8424. ttTreeNode,
  8425. node.firstChild
  8426. );
  8427. }
  8428. // check if next node is translateDiv
  8429. if (node.next !== null) {
  8430. index = node.next;
  8431.  
  8432. var ne_node = ttTreeNode[index];
  8433. if (ne_node.isTranslateDiv) {
  8434. var id = ne_node.id;
  8435. var topText = ne_node.topText;
  8436. var text = this.transResultMap[id];
  8437. // create element after pElement
  8438. this.reCreateTransDiv(pElement, id, text, topText);
  8439. }
  8440. pElement = pElement.next(); // go to next element
  8441. }
  8442. }
  8443. } while (node.next !== null);
  8444. }
  8445.  
  8446. // 重新创建translateDiv
  8447. reCreateTransDiv(pElement, id, translatedText, topText) {
  8448. const translateDiv = new TranslateDiv(id);
  8449. pElement.after(translateDiv.getDiv());
  8450. translateDiv.setTopText(topText);
  8451. translateDiv.registerUpButtonEvent();
  8452. translateDiv.registerCloseButtonEvent();
  8453. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8454. translateDiv.registerCopyButtonEvent(translatedText);
  8455. } else {
  8456. translateDiv.disableCopyButton();
  8457. }
  8458. translateDiv.updateTranslateDiv(translatedText, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru));
  8459. // 标记已翻译并添加到翻译按钮的结果栈中
  8460. let transButton = pElement.prev('.html2md-panel').find('.translateButton');
  8461. if (transButton.length == 0) {
  8462. // 如果没有找到,则应该是得在父元素中找到
  8463. transButton = pElement.parent().prev('.html2md-panel').find('.translateButton');
  8464. }
  8465. transButton.pushResultToTransButton({
  8466. translateDiv: translateDiv,
  8467. status: 0
  8468. });
  8469. transButton.setTransButtonState('success');
  8470. }
  8471. }
  8472.  
  8473. // 更新TransDB中的翻译数据
  8474. async function updateTransDBData(nodeDate, transResultMap) {
  8475. var url = window.location.href.replace(/#/, "");
  8476. try {
  8477. await OJBetter.common.database.translateData.put({ url, transResultMap, nodeDate });
  8478. return 'translateData saved successfully';
  8479. } catch (error) {
  8480. throw new Error(`Failed to save translateData: ${error}`);
  8481. }
  8482. }
  8483.  
  8484. // 获取TransDB中保存的翻译数据
  8485. async function getTransDBData() {
  8486. var url = window.location.href.replace(/#/, "");
  8487. try {
  8488. const result = await OJBetter.common.database.translateData.get(url);
  8489. return result;
  8490. } catch (error) {
  8491. throw new Error(`Failed to get translateData: ${error}`);
  8492. }
  8493. }
  8494.  
  8495. /**
  8496. * 翻译结果恢复功能初始化
  8497. * @returns
  8498. */
  8499. async function initTransResultsRecover() {
  8500. OJBetter.translation.memory.ttTree = new ElementsTree(".ttypography"); // 初始化当前页面.ttypography元素的结构树
  8501. let result = await getTransDBData();
  8502. if (!result) return;
  8503. OJBetter.translation.memory.ttTree.setNodeData(result.nodeDate);
  8504. OJBetter.translation.memory.ttTree.setTransResultMap(result.transResultMap);
  8505. OJBetter.translation.memory.ttTree.recover($(".ttypography"));
  8506. }
  8507.  
  8508. /**
  8509. * 自动翻译
  8510. */
  8511. async function initTransWhenViewable() {
  8512. await waitForMathJaxIdle();
  8513.  
  8514. const elements = $('.ttypography, .comments').find('.translateButton');
  8515. const observers = [];
  8516.  
  8517. // Use a single Intersection Observer for all elements
  8518. const observer = new IntersectionObserver((entries, obs) => {
  8519. entries.forEach((entry) => {
  8520. if (entry.isIntersecting) {
  8521. const button = $(entry.target);
  8522. const state = button.getButtonState();
  8523. const notAutoTranslate = button.getNotAutoTranslate();
  8524. // Check if the button meets the criteria
  8525. if (state === 'normal' && !notAutoTranslate) {
  8526. let trans = OJBetter.translation.choice;
  8527.  
  8528. if (OJBetter.translation.auto.mixTrans.enabled && button.IsCommentButton() && OJBetter.translation.auto.mixTrans.servers.length > 0) {
  8529. const randomIndex = Math.floor(Math.random() * OJBetter.translation.auto.mixTrans.servers.length);
  8530. trans = OJBetter.translation.auto.mixTrans.servers[randomIndex];
  8531. }
  8532. button.data("translatedItBy")(trans);
  8533. }
  8534.  
  8535. // Stop observing the element
  8536. obs.unobserve(entry.target);
  8537. }
  8538. });
  8539. });
  8540.  
  8541. // Observe each element
  8542. elements.each((i, e) => {
  8543. observer.observe(e);
  8544. });
  8545.  
  8546. // Store the observer in case you need to disconnect it later
  8547. observers.push(observer);
  8548. }
  8549.  
  8550. /**
  8551. * 翻译返回结果结构体
  8552. * @typedef {Object} TranslateResult
  8553. * @property {string} status 翻译状态
  8554. * @property {TranslateDiv} translateDiv 翻译结果面板
  8555. * @property {TransRawData} rawData 原始翻译数据
  8556. */
  8557.  
  8558. /**
  8559. * 翻译主方法
  8560. * @param {string} text 待翻译文本
  8561. * @param {HTMLElement} element_node 元素节点
  8562. * @param {Boolean} is_comment 是否为评论区文本
  8563. * @param {string} overrideTrans 覆盖全局翻译服务设定
  8564. * @returns {TranslateResult} 翻译结果对象
  8565. */
  8566. async function translateProblemStatement(text, element_node, is_comment, overrideTrans) {
  8567. /** @type {number} 翻译结果的ID*/
  8568. const id = OJB_getRandomNumber(8);
  8569. /** @type {TextBlockReplacer} 文本块替换/恢复实例*/
  8570. const textBlockReplacer = new TextBlockReplacer();
  8571. /** @type {string} 翻译结果文本*/
  8572. let translatedText = "";
  8573.  
  8574. /** @type {string} 当前实际应用的翻译服务 */
  8575. const realTransServer = overrideTrans ||
  8576. (is_comment && OJBetter.translation.comment.choice != "0" ?
  8577. OJBetter.translation.comment.choice :
  8578. OJBetter.translation.choice);
  8579.  
  8580. /** @type {TranslateResult} 翻译结果对象 */
  8581. const translateResult = {
  8582. status: "ok",
  8583. rawData: {
  8584. done: false
  8585. }
  8586. }
  8587.  
  8588. /**
  8589. * LaTeX替换
  8590. * @param {string} text 待翻译文本
  8591. * @returns {string} 处理后的文本
  8592. */
  8593. const replaceLatex = function (text) {
  8594. if (OJBetter.typeOfPage.is_oldLatex) {
  8595. const regex = /<span\s+class="tex-span">.*?<\/span>/gi;
  8596. text = textBlockReplacer.replace(text, regex);
  8597. text = text.replace(/<p>(.*?)<\/p>/g, "$1\n\n"); // <p/>标签换为换行
  8598. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8599. const regex = /<i>.*?<\/i>|<sub>.*?<\/sub>|<sup>.*?<\/sup>|<pre>.*?<\/pre>/gi;
  8600. text = textBlockReplacer.replace(text, regex);
  8601. } else if (realTransServer != "openai") {
  8602. // 使用GPT翻译时不必替换latex公式
  8603. const regex = /\$\$(\\.|[^\$])*?\$\$|\$(\\.|[^\$])*?\$/g;
  8604. text = textBlockReplacer.replace(text, regex);
  8605.  
  8606. // 替换行间代码块```
  8607. const regex2 = /```[\s\S]*?```/g;
  8608. text = textBlockReplacer.replace(text, regex2);
  8609. }
  8610. return text;
  8611. }
  8612.  
  8613. /**
  8614. * LaTeX恢复
  8615. * @param {string} text 已翻译的文本
  8616. * @returns {string} 恢复后的文本
  8617. */
  8618. const recoverLatex = function (text) {
  8619. // 两个公式之间加个空格,防止有些LaTeX解析器解析错误
  8620. let resultText = text
  8621. .replace(/】【/g, '】 【')
  8622. .replace(/\]\[/g, '] [')
  8623. .replace(/\}\{/g, '} {');
  8624.  
  8625. if (OJBetter.typeOfPage.is_oldLatex) {
  8626. resultText = resultText.replace(/(.+?)(\n\n|$)/g, "<p>$1</p>"); // 换行符还原为<p/>标签
  8627. resultText = textBlockReplacer.recover(resultText);
  8628. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8629. resultText = textBlockReplacer.recover(resultText);
  8630. } else if (realTransServer != "openai") {
  8631. resultText = textBlockReplacer.recover(resultText);
  8632. }
  8633. return resultText;
  8634. }
  8635.  
  8636. /**
  8637. * 格式化翻译结果
  8638. * @param {string} text
  8639. * @returns {string} 处理后的翻译结果
  8640. */
  8641. const formatText = function (text) {
  8642. // 转义LaTex中的特殊符号
  8643. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8644. // 先替换掉行间代码块
  8645. const replacer = new TextBlockReplacer();
  8646. text = replacer.replace(text, /```[\s\S]*?```/g);
  8647.  
  8648. // 处理LaTeX公式
  8649. const escapeRules = [
  8650. { pattern: /(?<!\\)>(?!\s)/g, replacement: " &gt; " }, // >符号
  8651. { pattern: /(?<!\\)</g, replacement: " &lt; " }, // <符号
  8652. { pattern: /(?<!\\)\*/g, replacement: " &#42; " }, // *符号
  8653. { pattern: /(?<!\\)_/g, replacement: " &#95; " }, // _符号
  8654. { pattern: /(?<!\\)\\\\(?=\s)/g, replacement: "\\\\\\\\" }, // \\符号
  8655. { pattern: /(?<!\\)\\(?![\\a-zA-Z0-9])/g, replacement: "\\\\" }, // \符号
  8656. ];
  8657.  
  8658. let latexMatches = [...text.matchAll(/\$\$([\s\S]*?)\$\$|\$(.*?)\$|\$([\s\S]*?)\$/g)];
  8659. for (const match of latexMatches) {
  8660. const matchedText = match[0];
  8661. let escapedText = matchedText;
  8662.  
  8663. for (const rule of escapeRules) {
  8664. escapedText = escapedText.replaceAll(rule.pattern, rule.replacement);
  8665. }
  8666. escapedText = escapedText.replace(/\$\$/g, "$$$$$$$$");// $$符号(因为后面需要作为replacement,双倍消耗)
  8667. text = text.replace(matchedText, escapedText);
  8668. }
  8669.  
  8670. // 恢复行间代码块
  8671. text = replacer.recover(text);
  8672. }
  8673.  
  8674. // 使符合mathjx的转换语法
  8675. const mathjaxRuleMap = [
  8676. { pattern: /\$/g, replacement: "$$$$$$" }, // $$ 行间
  8677. ];
  8678. mathjaxRuleMap.forEach(({ pattern, replacement }) => {
  8679. text = text.replace(pattern, replacement);
  8680. });
  8681.  
  8682. // markdown修正
  8683. const mdRuleMap = [
  8684. { pattern: /(\s_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: "$1 $2" }, // 斜体
  8685. { pattern: /(_[\u4e00-\u9fa5]+_\s)([\u4e00-\u9fa5]+)/g, replacement: " $1$2" },
  8686. { pattern: /(_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: " $1 $2" },
  8687. { pattern: /(([\s\S]*?))/g, replacement: "($1)" }, // 中文()
  8688. // { pattern: /:/g, replacement: ":" }, // 中文:
  8689. { pattern: /\*\* (.*?) \*\*/g, replacement: "\*\*$1\*\*" } // 加粗
  8690. ];
  8691. mdRuleMap.forEach(({ pattern, replacement }) => {
  8692. text = text.replace(pattern, replacement);
  8693. });
  8694.  
  8695. return text;
  8696. }
  8697.  
  8698. // 创建翻译结果元素并放在element_node的后面
  8699. translateResult.translateDiv = new TranslateDiv(id);
  8700. $(element_node).after(translateResult.translateDiv.getDiv());
  8701.  
  8702. // 顶栏左侧信息
  8703. translateResult.translateDiv.setTopText(i18next.t('servers.' + realTransServer, { ns: 'translator' }) +
  8704. i18next.t('translateDiv.topTextSuffix', { ns: 'translator' }));
  8705.  
  8706. // 注册按钮
  8707. translateResult.translateDiv.registerUpButtonEvent();
  8708. translateResult.translateDiv.registerCloseButtonEvent();
  8709. if (OJBetter.translation.choice == 'openai' || OJBetter.translation.choice == 'deepl') {
  8710. translateResult.translateDiv.showQueryBalanceButton(OJBetter.translation.choice); // 显示额度查询
  8711. }
  8712.  
  8713. // 翻译内容是否为空文本
  8714. if (isEmptyText(text)) {
  8715. const shouldContinue = await OJB_createDialog(
  8716. i18next.t('isEmptyText.title', { ns: 'dialog' }),
  8717. i18next.t('isEmptyText.content', { ns: 'dialog' }),
  8718. [
  8719. i18next.t('isEmptyText.buttons.0', { ns: 'dialog' }),
  8720. i18next.t('isEmptyText.buttons.1', { ns: 'dialog' })
  8721. ],
  8722. true
  8723. );
  8724. if (shouldContinue) {
  8725. translateResult.status = "skip";
  8726. return translateResult;
  8727. }
  8728. }
  8729.  
  8730. // 替换latex公式
  8731. text = replaceLatex(text);
  8732.  
  8733. // 过滤**号
  8734. if (OJBetter.translation.filterTextWithoutEmphasis && GM_getValue("translation") !== "openai") { // TODO
  8735. text = text.replace(/\*\*/g, "");
  8736. }
  8737.  
  8738. // 字符数上限
  8739. const translationLimits = {
  8740. deepl: 5000,
  8741. iflyrec: 2000,
  8742. youdao: 600,
  8743. google: 5000,
  8744. caiyun: 5000
  8745. };
  8746. if (translationLimits.hasOwnProperty(realTransServer) && text.length > translationLimits[realTransServer]) {
  8747. let textLength = translationLimits[realTransServer];
  8748. let realTextLength = text.length;
  8749. const shouldContinue = await OJB_createDialog(
  8750. i18next.t('transTextLimits.title', { ns: 'dialog' }),
  8751. i18next.t('transTextLimits.content', { ns: 'dialog', textLength: textLength, realTextLength: realTextLength }),
  8752. [
  8753. i18next.t('transTextLimits.buttons.0', { ns: 'dialog' }),
  8754. i18next.t('transTextLimits.buttons.1', { ns: 'dialog' })
  8755. ],
  8756. true
  8757. ); // 字数超限确认
  8758. if (shouldContinue) {
  8759. translateResult.status = "skip";
  8760. return translateResult;
  8761. }
  8762. }
  8763.  
  8764. /**
  8765. * 调用各个翻译服务
  8766. * @param {string} transServer 翻译服务
  8767. * @returns {TransRawData} 原始翻译数据
  8768. */
  8769. async function translate(transServer) {
  8770. const is_renderLaTeX = !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru);
  8771. const servername = i18next.t('servers.' + realTransServer, { ns: 'translator' });
  8772. /** @type {TransRawData} 原始翻译数据*/
  8773. let rawData = {};
  8774. try {
  8775. if (transServer == "deepl") {
  8776. if (OJBetter.deepl.config.type == 'free') {
  8777. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8778. rawData = await translate_deepl(text);
  8779. } else if (OJBetter.deepl.config.type == 'api') {
  8780. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.deeplApi', { ns: 'translator', deepl_configName: OJBetter.deepl.config.name })}`, is_renderLaTeX);
  8781. if (OJBetter.deepl.config.apiGenre == 'deeplx') {
  8782. rawData = await translate_deeplx(text);
  8783. } else {
  8784. if (OJBetter.deepl.enableEmphasisProtection) text = convertBoldMarkdownToHTML(text);
  8785. if (OJBetter.deepl.enableLinkProtection) text = convertLinksMarkdownToHTML(text);
  8786. if (OJBetter.deepl.config.apiGenre == 'api-free') {
  8787. rawData = await translate_deepl_api_free(text);
  8788. } else if (OJBetter.deepl.config.apiGenre == 'api-pro') {
  8789. rawData = await translate_deepl_api_pro(text);
  8790. }
  8791. if (OJBetter.deepl.enableEmphasisProtection) rawData.text = convertBoldHTMLToMarkdown(rawData.text);
  8792. if (OJBetter.deepl.enableLinkProtection) rawData.text = convertLinksHTMLToMarkdown(rawData.text);
  8793. }
  8794. }
  8795. } else if (transServer == "iflyrec") {
  8796. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8797. rawData = await translate_iflyrec(text);
  8798. } else if (transServer == "youdao") {
  8799. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8800. rawData = await translate_youdao_mobile(text);
  8801. } else if (transServer == "google") {
  8802. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8803. rawData = await translate_gg(text);
  8804. } else if (transServer == "caiyun") {
  8805. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8806. rawData = await translate_caiyun(text);
  8807. } else if (transServer == "openai") {
  8808. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.openai', { ns: 'translator', openai_name: OJBetter.chatgpt.config.name })}${!OJBetter.chatgpt.isStream
  8809. ? i18next.t('transingTip.openai_isStream', { ns: 'translator' }) : ""}`,
  8810. is_renderLaTeX);
  8811. if (OJBetter.chatgpt.isStream) {
  8812. // 流式传输
  8813. rawData = await translate_openai_stream(text, translateResult.translateDiv);
  8814. } else {
  8815. // 普通模式
  8816. rawData = await translate_openai(text);
  8817. }
  8818. }
  8819. translatedText = rawData.text;
  8820. if (!rawData.done) {
  8821. translateResult.status = "error";
  8822. }
  8823. } catch (e) {
  8824. translateResult.status = "error";
  8825. rawData.message = i18next.t('error.unexpected', { ns: 'translator' });
  8826. console.warn(e);
  8827. }
  8828. return rawData;
  8829. }
  8830. translateResult.rawData = await translate(realTransServer);
  8831.  
  8832. if (translateResult.status == "error") {
  8833. translateResult.translateDiv.updateTranslateDiv(translateResult.rawData.message);
  8834. return translateResult;
  8835. }
  8836.  
  8837. // 还原latex公式
  8838. translatedText = recoverLatex(translatedText);
  8839.  
  8840. // 注册结果复制按钮
  8841. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8842. translateResult.translateDiv.registerCopyButtonEvent(translatedText);
  8843. } else {
  8844. translateResult.translateDiv.disableCopyButton();
  8845. }
  8846.  
  8847. // 翻译结果格式化
  8848. translatedText = formatText(translatedText);
  8849.  
  8850. // 保存翻译结果
  8851. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  8852. OJBetter.translation.memory.ttTree.refreshNode(".ttypography"); // 刷新当前页面.ttypography元素的结构树实例
  8853. OJBetter.translation.memory.ttTree.addTransResultMap(id, translatedText);
  8854. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新翻译结果到transDB
  8855. }
  8856.  
  8857. // 翻译结果面板更新
  8858. translateResult.translateDiv.updateTranslateDiv(translatedText, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru));
  8859.  
  8860. return translateResult;
  8861. }
  8862.  
  8863. //弹窗翻译
  8864. function alertZh() {
  8865. // var _alert = window.alert;
  8866. // window.alert = async function (msg) {
  8867. // _alert(msg + "\n=========翻译=========\n" + await translate_deepl(msg));
  8868. // return true;
  8869. // }
  8870. };
  8871.  
  8872. /**
  8873. * 折叠块展开
  8874. */
  8875. function ExpandFoldingblocks() {
  8876. $('.spoiler').addClass('spoiler-open');
  8877. $('.spoiler-content').attr('style', '');
  8878. };
  8879.  
  8880. /**
  8881. * 折叠块渲染优化
  8882. */
  8883. function RenderPerfOpt() {
  8884. GM_addStyle(`
  8885. .spoiler-content {
  8886. contain: layout style;
  8887. }
  8888. `);
  8889. }
  8890.  
  8891. /**
  8892. * 下拉选择框性能优化
  8893. */
  8894. async function SelectElementPerfOpt() {
  8895. // TODO 10
  8896. // 加载库资源
  8897. await OJB_LoadJS("https://aowuucdn.oss-accelerate.aliyuncs.com/js/selectpage.min.js");
  8898. /**
  8899. * 将一个<select>元素转换为SelectPage控件
  8900. * @param {HTMLElement|string} selector - 要转换的<select>元素或其选择器
  8901. */
  8902. const OJB_transformSelectToSelectPage = (selector) => {
  8903. const $select = $(selector);
  8904. if ($select.length === 0 || !$select.is('select')) {
  8905. console.error('Invalid select element provided.');
  8906. return;
  8907. }
  8908.  
  8909. // 隐藏原生的<select>元素
  8910. $select.hide();
  8911.  
  8912. // 创建一个新的<input>元素用于SelectPage控件
  8913. const $inputForSelectPage = $('<input>', {
  8914. type: 'text',
  8915. class: 'selectpage-input',
  8916. autocomplete: 'off'
  8917. });
  8918. $select.after($inputForSelectPage);
  8919.  
  8920. // 准备SelectPage所需的数据格式
  8921. const data = $select.find('option').map((_, option) => ({
  8922. id: option.value,
  8923. text: option.text
  8924. })).get();
  8925.  
  8926. // 初始化SelectPage
  8927. $inputForSelectPage.selectPage({
  8928. showField: 'text',
  8929. keyField: 'id',
  8930. data,
  8931. lang: 'en',
  8932. // 当选中一个选项时,更新隐藏的<select>元素的值
  8933. eSelect: (data) => {
  8934. $select.val(data.id).trigger('change');
  8935. },
  8936. // 初始化时根据<select>的当前值设置SelectPage
  8937. initRecord: $select.val()
  8938. });
  8939. };
  8940.  
  8941. // 遍历页面上的所有select
  8942. $('select').each((_, select) => {
  8943. // 选项大于500才优化
  8944. if ($(select).find('option').length > 500) {
  8945. OJB_transformSelectToSelectPage(select);
  8946. }
  8947. });
  8948. }
  8949.  
  8950. /**
  8951. * 评论区分页
  8952. */
  8953. function CommentPagination() {
  8954. GM_addStyle(`
  8955. .comments > .comment {
  8956. display: none;
  8957. }
  8958. #next-page-btn, #prev-page-btn {
  8959. display: none;
  8960. }
  8961. #jump-input, #items-per-page{
  8962. height: 22px;
  8963. border: 1px solid #dcdfe6;
  8964. border-radius: 0.3rem;
  8965. }
  8966. #jump-input:focus-visible{
  8967. border-style: solid;
  8968. border-color: #3f51b5;
  8969. outline: none;
  8970. }
  8971. `);
  8972. $('.comments').after(`
  8973. <div id="pagBar" style="display: flex; align-items: center; justify-content: center; color: #606266;">
  8974. <label for="items-per-page">${i18next.t('perpage', { ns: 'comments' })}</label>
  8975. <select id="items-per-page" style="margin-right: 15px;">
  8976. <option value="5">5</option>
  8977. <option value="10">10</option>
  8978. <option value="20">20</option>
  8979. </select>
  8980. <div class="paging" style="margin-right: 15px;">
  8981. <span id="current-page">1</span> / <span id="total-pages"></span>
  8982. </div>
  8983. <input type="text" id="jump-input" placeholder="${i18next.t('jumpTo', { ns: 'comments' })}">
  8984. <button type="button" id="jump-btn" class="ojb_btn">${i18next.t('jump', { ns: 'comments' })}</button>
  8985. <button id="prev-page-btn" class="ojb_btn">${i18next.t('prev', { ns: 'comments' })}</button>
  8986. <button id="next-page-btn" class="ojb_btn">${i18next.t('next', { ns: 'comments' })}</button>
  8987. </div>
  8988. `);
  8989.  
  8990. let batchSize = 5;
  8991. let elements = $(".comments > .comment").slice(0, -1);
  8992. let start = 0;
  8993. let end = batchSize;
  8994. let currentPage = 1;
  8995. let displayedIndexes = []; // 存储已显示元素的索引
  8996.  
  8997. function showBatch(start, end) {
  8998. // 隐藏上一页
  8999. for (var i = 0; i < displayedIndexes.length; i++) {
  9000. elements.eq(displayedIndexes[i]).hide();
  9001. }
  9002.  
  9003. displayedIndexes = [];
  9004.  
  9005. // 显示当前页
  9006. elements.slice(start, end).each(function (index) {
  9007. $(this).show();
  9008. displayedIndexes.push(start + index);
  9009. });
  9010.  
  9011. // 更新页码和翻页按钮
  9012. $("#current-page").text(currentPage);
  9013. $("#total-pages").text(Math.ceil(elements.length / batchSize));
  9014.  
  9015. if (currentPage === 1) $("#prev-page-btn").hide();
  9016. else $("#prev-page-btn").show();
  9017.  
  9018. if (end >= elements.length) $("#next-page-btn").hide();
  9019. else $("#next-page-btn").show();
  9020. }
  9021.  
  9022. // 初始化
  9023. var commentID = null;
  9024. var pageURL = window.location.href;
  9025. if (pageURL.includes("#comment-")) {
  9026. // 带评论区锚点的链接
  9027. var startIndex = pageURL.lastIndexOf("#comment-") + 9;
  9028. commentID = pageURL.substring(startIndex);
  9029. var indexInComments = null;
  9030. $(".comments > .comment").each(function (index) {
  9031. $(this).find(".comment-table").each(function () {
  9032. var tableCommentID = $(this).attr("commentid");
  9033. if (tableCommentID === commentID) {
  9034. indexInComments = index;
  9035. return false;
  9036. }
  9037. });
  9038. });
  9039. let page = Math.ceil((indexInComments + 1) / batchSize);
  9040. currentPage = !page ? 1 : page;
  9041. showBatch((currentPage - 1) * batchSize, currentPage * batchSize);
  9042. setTimeout(function () {
  9043. window.location.href = pageURL;
  9044. }, 1000);
  9045. } else {
  9046. showBatch(0, batchSize);
  9047. }
  9048.  
  9049. $("#prev-page-btn").on("click", function () {
  9050. var itemsPerPage = parseInt($("#items-per-page").val());
  9051. start = (currentPage - 2) * itemsPerPage;
  9052. end = (currentPage - 1) * itemsPerPage;
  9053. currentPage--;
  9054. showBatch(start, end);
  9055. });
  9056.  
  9057. $("#next-page-btn").on("click", function () {
  9058. var itemsPerPage = parseInt($("#items-per-page").val());
  9059. start = currentPage * itemsPerPage;
  9060. end = (currentPage + 1) * itemsPerPage;
  9061. currentPage++;
  9062. showBatch(start, end);
  9063. });
  9064.  
  9065. $("#jump-btn").on("click", function () {
  9066. var inputPage = parseInt($("#jump-input").val());
  9067.  
  9068. if (inputPage >= 1 && inputPage <= Math.ceil(elements.length / parseInt($("#items-per-page").val()))) {
  9069. var itemsPerPage = parseInt($("#items-per-page").val());
  9070. start = (inputPage - 1) * itemsPerPage;
  9071. end = inputPage * itemsPerPage;
  9072. currentPage = inputPage; // 更新当前页码
  9073. showBatch(start, end);
  9074. }
  9075. });
  9076.  
  9077. $("#items-per-page").on("change", function () {
  9078. batchSize = parseInt($(this).val());
  9079. let page = Math.ceil(end / batchSize);
  9080. currentPage = !page ? 1 : page;
  9081. let maxPage = Math.ceil(elements.length / batchSize);
  9082. if (currentPage > maxPage) currentPage = maxPage;
  9083. showBatch((currentPage - 1) * batchSize, currentPage * batchSize);
  9084. });
  9085. }
  9086.  
  9087. /**
  9088. * 题目页相关链接栏
  9089. */
  9090. class ProblemPageLinkbar {
  9091. constructor() {
  9092. this.containerElement = this.createToolbar();
  9093. this.commandInvoker = new CommandInvoker();
  9094. }
  9095.  
  9096. /**
  9097. * 创建工具栏
  9098. */
  9099. createToolbar() {
  9100. const toolbarElement = $("<div>").attr("id", "problemToolbar").insertBefore($(".problemindexholder"));
  9101. return new DOMContainer(toolbarElement);
  9102. }
  9103.  
  9104. /**
  9105. * 添加按钮
  9106. * @param {string} id 按钮id
  9107. * @param {string} url 按钮链接
  9108. * @param {string} text 按钮文字
  9109. * @param {JQueryObject} icon 按钮图标
  9110. * @param {string} iconHeight 图标高度
  9111. * @returns {object} 按钮对象
  9112. */
  9113. addLinkButton(id, url, text, icon = $('<div>'), iconHeight = "22px") {
  9114. const linkElement = $("<a>")
  9115. .attr("href", url)
  9116. .attr("target", "_blank")
  9117. .addClass("ojb_btn")
  9118. .attr("id", id);
  9119.  
  9120. linkElement.append(icon);
  9121. icon.css("height", iconHeight);
  9122.  
  9123. const textSpan = $("<span>").html(text);
  9124. linkElement.append(textSpan);
  9125.  
  9126. this.commandInvoker.execute(new AddElementCommand(this.containerElement, linkElement));
  9127. return {
  9128. element: linkElement,
  9129. text: textSpan,
  9130. icon: icon
  9131. };
  9132. }
  9133.  
  9134. /**
  9135. * 更新链接
  9136. * @param {object} button 按钮对象
  9137. * @param {string} url 按钮链接
  9138. */
  9139. updateUrl(button, url) {
  9140. button.element.attr("href", url);
  9141. }
  9142.  
  9143. /**
  9144. * 更新文字
  9145. * @param {object} button 按钮对象
  9146. * @param {string} text 按钮文字
  9147. */
  9148. updateText(button, text) {
  9149. button.text.html(text);
  9150. }
  9151.  
  9152. /**
  9153. * 设置文字为粗体
  9154. * @param {object} button 按钮对象
  9155. */
  9156. setBold(button) {
  9157. button.text.css("font-weight", "bold");
  9158. }
  9159.  
  9160. /**
  9161. * 更新图标
  9162. * @param {object} button 按钮对象
  9163. * @param {JQueryObject} icon 按钮图标
  9164. * @param {string} iconHeight 图标高度
  9165. */
  9166. updateIcon(button, icon, iconHeight = "16px") {
  9167. button.icon.remove();
  9168. button.text.prepend(icon);
  9169. icon.css("height", iconHeight);
  9170. button.icon = icon;
  9171. }
  9172.  
  9173. /**
  9174. * 添加类
  9175. * @param {object} button 按钮对象
  9176. * @param {string} className 类名
  9177. */
  9178. addClass(button, className) {
  9179. button.element.addClass(className);
  9180. }
  9181.  
  9182. /**
  9183. * 禁用链接按钮
  9184. * @param {object} button 按钮对象
  9185. */
  9186. disableButton(button) {
  9187. button.element.addClass("disabled");
  9188. }
  9189.  
  9190. /**
  9191. * 启用链接按钮
  9192. * @param {object} button 按钮对象
  9193. */
  9194. enableButton(button) {
  9195. button.element.removeClass("disabled");
  9196. }
  9197. }
  9198.  
  9199. /**
  9200. * 获取题目的id
  9201. * @param {String} url 题目的链接
  9202. * @returns 题目的id,形如2000A
  9203. */
  9204. function getProblemId(url) {
  9205. const regex = url.includes('/contest/')
  9206. ? /\/contest\/(\d+)\/problem\/([A-Za-z\d]+)/
  9207. : /\/problemset\/problem\/(\d+)\/([A-Za-z\d]+)/;
  9208. const matchResult = url.match(regex);
  9209. return matchResult && matchResult.length >= 3 ? `${matchResult[1]}${matchResult[2]}` : '';
  9210. };
  9211.  
  9212. /**
  9213. * 跳转到洛谷
  9214. * @param {ProblemPageLinkbar} problemToolbar
  9215. */
  9216. async function CF2luogu(problemToolbar) {
  9217. const url = window.location.href;
  9218. const problemId = getProblemId(url);
  9219. const luoguButton = problemToolbar.addLinkButton(
  9220. "luoguButton",
  9221. "https://www.luogu.com.cn/",
  9222. i18next.t('state.loading', { ns: 'button' }),
  9223. $("<img>").attr("src", "https://cdn.luogu.com.cn/fe/logo.png")
  9224. );
  9225.  
  9226. const checkLinkExistence = async (url) => {
  9227. return OJB_promiseRetryWrapper(async () => {
  9228. const response = await OJB_GMRequest({
  9229. method: "GET",
  9230. url
  9231. });
  9232. return !response.responseText.match(/出错了/g);
  9233. }, {
  9234. maxRetries: 3,
  9235. retryInterval: 1000
  9236. });
  9237. };
  9238.  
  9239. const LuoguUrl = `https://www.luogu.com.cn/problem/CF${problemId}`;
  9240. try {
  9241. const result = await checkLinkExistence(LuoguUrl);
  9242. if (problemId && result) {
  9243. problemToolbar.updateText(luoguButton, "");
  9244. problemToolbar.updateUrl(luoguButton, LuoguUrl);
  9245. } else {
  9246. problemToolbar.updateText(luoguButton, i18next.t('state.404', { ns: 'button' }));
  9247. problemToolbar.disableButton(luoguButton);
  9248. }
  9249. } catch (error) {
  9250. if (error instanceof OJB_GMError && error.type == "error") {
  9251. problemToolbar.updateText(luoguButton, i18next.t('state.netError', { ns: 'button' }));
  9252. problemToolbar.disableButton(luoguButton);
  9253. }
  9254. }
  9255. }
  9256.  
  9257. /**
  9258. * 跳转到 Virtual Judge
  9259. * @param {ProblemPageLinkbar} problemToolbar
  9260. */
  9261. async function CF2vjudge(problemToolbar) {
  9262. const url = window.location.href;
  9263. const problemId = getProblemId(url);
  9264. const vjudgeButton = problemToolbar.addLinkButton(
  9265. "vjudgeButton",
  9266. "https://vjudge.net/",
  9267. i18next.t('state.loading', { ns: 'button' }),
  9268. $("<img>").attr("src", "https://aowuucdn.oss-accelerate.aliyuncs.com/vjudge.ico")
  9269. );
  9270.  
  9271. const checkLinkExistence = async (url) => {
  9272. return OJB_promiseRetryWrapper(async () => {
  9273. const response = await OJB_GMRequest({
  9274. method: "HEAD",
  9275. url: url,
  9276. });
  9277. if (response.status >= 200 && response.status < 300) return true;
  9278. else if (response.status == 404) return false;
  9279. else throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9280. }, {
  9281. maxRetries: 3,
  9282. retryInterval: 1000
  9283. });
  9284. };
  9285.  
  9286. const VjudgeUrl = `https://vjudge.net/problem/CodeForces-${problemId}`;
  9287. try {
  9288. const result = await checkLinkExistence(VjudgeUrl);
  9289. if (problemId && result) {
  9290. problemToolbar.updateText(vjudgeButton, "VJudge");
  9291. problemToolbar.updateUrl(vjudgeButton, VjudgeUrl);
  9292. } else {
  9293. problemToolbar.updateText(vjudgeButton, i18next.t('state.404', { ns: 'button' }));
  9294. problemToolbar.disableButton(vjudgeButton);
  9295. }
  9296. } catch (error) {
  9297. if (error instanceof OJB_GMError && error.type == "error") {
  9298. problemToolbar.updateText(vjudgeButton, i18next.t('state.netError', { ns: 'button' }));
  9299. problemToolbar.disableButton(vjudgeButton);
  9300. }
  9301. }
  9302. }
  9303.  
  9304. // RatingClass
  9305. const ratingClassMap = {
  9306. NaN: "rating_by_clist_colorNaN",
  9307. 0: "rating_by_clist_color0",
  9308. 1200: "rating_by_clist_color1",
  9309. 1400: "rating_by_clist_color2",
  9310. 1600: "rating_by_clist_color3",
  9311. 1900: "rating_by_clist_color4",
  9312. 2100: "rating_by_clist_color5",
  9313. 2300: "rating_by_clist_color6",
  9314. 2400: "rating_by_clist_color7",
  9315. 2600: "rating_by_clist_color8",
  9316. 3000: "rating_by_clist_color9"
  9317. };
  9318. const cssMap = {
  9319. "rating_by_clist_colorNaN": "#cccccc",
  9320. "rating_by_clist_color0": "#808080",
  9321. "rating_by_clist_color1": "#73e473",
  9322. "rating_by_clist_color2": "#77ddbb",
  9323. "rating_by_clist_color3": "#aaaaff",
  9324. "rating_by_clist_color4": "#ff88ff",
  9325. "rating_by_clist_color5": "#ffcc88",
  9326. "rating_by_clist_color6": "#ffbb55",
  9327. "rating_by_clist_color7": "#ff7777",
  9328. "rating_by_clist_color8": "#ff3333",
  9329. "rating_by_clist_color9": "#aa0000"
  9330. };
  9331. // TODO 7
  9332. /**
  9333. * clist 访问有效性检查
  9334. * @param {boolean} onlyCookie 是否只检查Cookie
  9335. * @returns {Promise<boolean>} 是否有效
  9336. */
  9337. async function validateClistConnection(onlyCookie = false) {
  9338. const clistApiUrl = "https://clist.by:443/api/v4/contest/?limit=1&resource_id=1";
  9339. const requestOptions = {
  9340. method: "GET",
  9341. url: clistApiUrl,
  9342. timeout: 5000,
  9343. };
  9344.  
  9345. // 尝试发送请求
  9346. async function tryRequest(options) {
  9347. try {
  9348. const response = await OJB_GMRequest(options);
  9349. if (response.status === 200) {
  9350. return { ok: true };
  9351. } else if (response.status === 401) {
  9352. throw new Error('unauthorized');
  9353. } else if (response.status === 404) {
  9354. throw new Error('not_found');
  9355. } else {
  9356. throw new Error('other_error');
  9357. }
  9358. } catch (error) {
  9359. console.warn(`Error accessing clist.by: ${error.message}`);
  9360. return { ok: false, error: error.message };
  9361. }
  9362. }
  9363.  
  9364. // 尝试携带Key发送请求
  9365. let result = await tryRequest(requestOptions);
  9366. if (!onlyCookie && !result.ok) {
  9367. requestOptions.headers = { "Authorization": OJBetter.clist.authorization };
  9368. result = await tryRequest(requestOptions);
  9369. }
  9370.  
  9371. // 根据结果显示错误信息
  9372. if (!result.ok) {
  9373. let errorType = result.error;
  9374. const loadingMessage = new LoadingMessage();
  9375. let state;
  9376. if (errorType === 'not_found') {
  9377. state = i18next.t('error.clist.404', { ns: 'alert' });
  9378. } else if (errorType === 'unauthorized') {
  9379. state = i18next.t('error.clist.cookie', { ns: 'alert' });
  9380. } else {
  9381. state = i18next.t('error.clist.other', { ns: 'alert' });
  9382. }
  9383. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${state}`, 'error');
  9384. }
  9385. return result.ok;
  9386. }
  9387.  
  9388. /**
  9389. * 创建Rating相关css
  9390. * @param {boolean} [hasBorder=true] 是否有边框
  9391. */
  9392. function creatRatingCss(hasBorder = true) {
  9393. const defaultBorderColor = '#dcdfe6';
  9394. let dynamicCss = "";
  9395. let hoverSelector = OJBetter.clist.ratingHidden ? ":hover" : "";
  9396. for (let cssClass in cssMap) {
  9397. dynamicCss += `a.${cssClass}${hoverSelector}, a.${cssClass}${hoverSelector}:link {\n`;
  9398. let borderColor = hasBorder ? cssMap[cssClass] : defaultBorderColor;
  9399. dynamicCss += ` color: ${cssMap[cssClass]} ${OJBetter.clist.ratingHidden ? "!important" : ""};\n`;
  9400. dynamicCss += `}\n`;
  9401. }
  9402. GM_addStyle(dynamicCss);
  9403. if (OJBetter.clist.ratingHidden) {
  9404. GM_addStyle(`
  9405. #clistButton {
  9406. color: #ffffff00;
  9407. }
  9408. `);
  9409. }
  9410. }
  9411.  
  9412. /**
  9413. * 模拟clist网页访问获取rating
  9414. * @param {string} problem 题目名称
  9415. * @param {string} problem_url 题目链接
  9416. * @param {string} contest 比赛名称
  9417. * @returns {Promise<{rating: number, problem: string}>} 题目难度
  9418. */
  9419. async function getRatingFromHTML(problem, problem_url, contest = null) {
  9420. // 去除题目名称中的括号,以及首尾的空白符
  9421. problem = problem.replace(/\([\s\S]*?\)/g, '').trim();
  9422.  
  9423. return OJB_promiseRetryWrapper(async () => {
  9424. const queryString = `search=${encodeURIComponent(problem)}&resource=1`;
  9425. const response = await OJB_GMRequest({
  9426. method: 'GET',
  9427. url: `https://clist.by/problems/?${queryString}`,
  9428. });
  9429.  
  9430. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9431. const html = response.responseText;
  9432. const cleanedHtml = html.replace(/src=(.|\s)*?"/g, '');
  9433. const parser = new DOMParser();
  9434. const doc = parser.parseFromString(cleanedHtml, 'text/html');
  9435. const trs = doc.querySelectorAll('table tbody tr');
  9436.  
  9437. for (let tr of trs) {
  9438. const rating = tr.querySelector('.problem-rating-column').textContent.trim();
  9439. const link = OJB_cleanLink(tr.querySelector('.problem-name-column a:nth-of-type(2)')?.href);
  9440.  
  9441. if (link === problem_url || link === problem_url + '/') {
  9442. return {
  9443. rating: parseInt(rating),
  9444. problem: problem
  9445. };
  9446. } else if (contest !== null) {
  9447. const contestTitles = [...tr.querySelectorAll('.problem-name-column .pull-right a[title], .problem-name-column .pull-right span[title]')].map(el => el.title);
  9448. if (contestTitles.includes(contest)) {
  9449. return {
  9450. rating: parseInt(rating),
  9451. problem: problem
  9452. };
  9453. }
  9454. }
  9455. }
  9456. console.warn(`No data found for the question: ${problem}`);
  9457. }, {
  9458. maxRetries: 3,
  9459. retryInterval: 500
  9460. });
  9461. }
  9462.  
  9463. /**
  9464. * 从clist API获取题目的rating
  9465. * @param {string} problem_name 题目名
  9466. * @param {string} problem_url 题目链接
  9467. * @returns {Promise<number>} 题目rating
  9468. *
  9469. * 使用两个Map对象来存储和快速访问题目信息:
  9470. * - problemsMap: 通过题目的URL作为键来存储题目信息。
  9471. * - nameMap: 通过题目的名称作为键来存储题目信息。
  9472. *
  9473. * 每个题目信息是一个对象,包含以下属性:
  9474. * @typedef {Object} ProblemInfo
  9475. * @property {string} name 题目名称
  9476. * @property {string} url 题目URL
  9477. * @property {number} rating 题目评分,如果没有评分信息则为NaN
  9478. */
  9479. async function getRatingFromApi_problem(problem_name, problem_url) {
  9480. return OJB_promiseRetryWrapper(async () => {
  9481. const response = await OJB_GMRequest({
  9482. method: "GET",
  9483. url: `https://clist.by:443/api/v4/problem/?name=${encodeURIComponent(problem_name)}&resource__regex=codeforces.com`,
  9484. headers: { "Authorization": OJBetter.clist.authorization }
  9485. });
  9486.  
  9487. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9488. let data = JSON.parse(response.responseText);
  9489. /**
  9490. * 使用题目的URL作为键来存储题目信息。
  9491. * @type {Map<string, ProblemInfo>}
  9492. */
  9493. let problemsMap = new Map();
  9494.  
  9495. /**
  9496. * 使用题目的名称作为键来存储题目信息。
  9497. * @type {Map<string, ProblemInfo>}
  9498. */
  9499. let nameMap = new Map();
  9500.  
  9501. data.objects.forEach(problem => {
  9502. /** @type {ProblemInfo} 题目信息*/
  9503. let problemInfo = {
  9504. name: problem.name,
  9505. url: problem.url,
  9506. rating: problem.rating ? problem.rating : NaN
  9507. };
  9508. problemsMap.set(OJB_cleanLink(problem.url), problemInfo);
  9509. nameMap.set(problem.name, problemInfo);
  9510. });
  9511.  
  9512. if (problemsMap.has(problem_url)) {
  9513. return problemsMap.get(problem_url).rating;
  9514. } else if (nameMap.has(problem_name)) {
  9515. return nameMap.get(problem_name).rating;
  9516. } else {
  9517. console.warn('Problem not found in the response');
  9518. }
  9519. }, {
  9520. maxRetries: 5,
  9521. retryInterval: 1000
  9522. });
  9523. }
  9524.  
  9525. /**
  9526. * 获取字符串中的关键词列表
  9527. * @param {string} text 字符串文本
  9528. * @returns {array<string>} 返回关键词列表
  9529. */
  9530. function getKeywords(text) {
  9531. // 定义要过滤掉的高频词
  9532. const highFrequencyWords = ['Educational', 'Codeforces', 'Round', 'Div'];
  9533.  
  9534. // 使用正则表达式替换掉特殊符号(保留空格以便分词)
  9535. const sanitizedText = text.replace(/[^\w\s]|_/g, '').replace(/\s+/g, ' ');
  9536.  
  9537. // 将字符串拆分为单词数组
  9538. const words = sanitizedText.split(' ');
  9539.  
  9540. // 过滤掉高频词和空字符串
  9541. const filteredWords = words.filter(word => {
  9542. return word && highFrequencyWords.indexOf(word) === -1;
  9543. });
  9544.  
  9545. // 返回关键词列表
  9546. return filteredWords;
  9547. }
  9548.  
  9549. /**
  9550. * 根据关键词从 Clist API 中获取实际比赛名称
  9551. * @param {string} contestName 比赛名
  9552. * @param {string} contestUrl 比赛链接
  9553. * @returns {string|null} 该比赛在Clist中的实际名字
  9554. */
  9555. async function getContestNameFromApi(contestName, contestUrl) {
  9556. return OJB_promiseRetryWrapper(async () => {
  9557. const options = {
  9558. method: "GET",
  9559. url: `https://clist.by:443/api/v4/contest/?resource_id=1&event__regex=${encodeURIComponent(contestName)}`,
  9560. headers: {
  9561. "Authorization": OJBetter.clist.authorization
  9562. }
  9563. };
  9564.  
  9565. let response = await OJB_GMRequest(options);
  9566.  
  9567. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9568.  
  9569. let data = JSON.parse(response.responseText);
  9570. let objects = data.objects;
  9571.  
  9572. if (objects.length > 0) {
  9573. for (const contest of objects) {
  9574. const href = contest.href.replace(/\/contests\//i, '/contest/'); // 链接可能是contests而不是contest,换回来
  9575. if (OJB_cleanLink(href) == contestUrl) {
  9576. return contest.event;
  9577. }
  9578. }
  9579. }
  9580. return null;
  9581. }, {
  9582. maxRetries: 5,
  9583. retryInterval: 1000
  9584. });
  9585. }
  9586.  
  9587. /**
  9588. * 获取在clist中的实际比赛名称
  9589. * @param {string} contestName 待搜索的比赛名称
  9590. * @param {string} contestUrl 比赛的url
  9591. * @returns {Promise<string|null>} 在clist中的实际比赛名称,如果没有找到,则返回null
  9592. */
  9593. async function getActualContestName(contestName, contestUrl) {
  9594. // 首先尝试使用完整的比赛名称进行搜索
  9595. let actualContestName = await getContestNameFromApi(contestName, contestUrl);
  9596. if (actualContestName) {
  9597. return actualContestName;
  9598. }
  9599.  
  9600. // 如果使用完整名称没有找到,则尝试使用关键词进行搜索
  9601. const keywords = getKeywords(contestName);
  9602. const maxKeywordAttempts = 1; // 最多尝试到第几个关键词(因为Clist API有频率限制)
  9603. for (let i = 0; i < Math.min(keywords.length, maxKeywordAttempts); i++) {
  9604. actualContestName = await getContestNameFromApi(keywords[i], contestUrl);
  9605. if (actualContestName) {
  9606. return actualContestName;
  9607. }
  9608. }
  9609.  
  9610. // 如果全部尝试后仍没有找到,返回null
  9611. return null;
  9612. }
  9613.  
  9614. /**
  9615. * 从clist API获取比赛题目集的rating
  9616. * @param {string} contestName 比赛名
  9617. * @returns {Promise<Map<string, number>>} 题目rating
  9618. */
  9619. async function getRatingFromApi_contest(contestName, contestUrl) {
  9620. const actualContestName = await getActualContestName(contestName, contestUrl);
  9621. return OJB_promiseRetryWrapper(async () => {
  9622. const options = {
  9623. method: "GET",
  9624. url: `https://clist.by:443/api/v4/contest/?resource_id=1&with_problems=true&event=${encodeURIComponent(actualContestName)}`,
  9625. headers: {
  9626. "Authorization": OJBetter.clist.authorization
  9627. }
  9628. };
  9629.  
  9630. let response = await OJB_GMRequest(options);
  9631.  
  9632. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9633.  
  9634. let data = JSON.parse(response.responseText);
  9635. let objects = data.objects;
  9636. let problemsMap = new Map();
  9637.  
  9638. if (objects.length > 0 && objects[0].problems) {
  9639. objects[0].problems.forEach(problem => {
  9640. problemsMap.set(OJB_cleanLink(problem.url), problem.rating ? problem.rating : NaN);
  9641. });
  9642. }
  9643.  
  9644. return problemsMap;
  9645. }, {
  9646. maxRetries: 5,
  9647. retryInterval: 1000
  9648. });
  9649. }
  9650.  
  9651. /**
  9652. * 根据rating获取对应的颜色class名
  9653. * @param {number} rating 题目rating
  9654. * @returns {string} 颜色class名
  9655. */
  9656. function getClassNameByRating(rating) {
  9657. let className = "rating_by_clist_color9";
  9658. if (Number.isNaN(rating)) {
  9659. className = "rating_by_clist_colorNaN";
  9660. } else {
  9661. let keys = Object.keys(ratingClassMap);
  9662. for (let i = 0; i < keys.length; i++) {
  9663. if (rating < keys[i]) {
  9664. className = ratingClassMap[keys[i - 1]];
  9665. break;
  9666. }
  9667. }
  9668. }
  9669. return className;
  9670. }
  9671.  
  9672. /**
  9673. * problem题目页显示Rating
  9674. * @param {ProblemPageLinkbar} problemToolbar
  9675. * @returns {Promise<void>}
  9676. */
  9677. async function showRatingByClist_problem(problemToolbar) {
  9678. // 题目名
  9679. const problem = $('.header .title').eq(0).text().replace(/[\s\S]*?. /, '');
  9680. if (OJBetter.typeOfPage.is_acmsguru) problem = $('h4').eq(0).text().replace(/[\s\S]*?. /, '');
  9681.  
  9682. // 创建Rating按钮元素
  9683. creatRatingCss(false);
  9684. // TODO
  9685. const clistButton = problemToolbar.addLinkButton(
  9686. 'clistButton',
  9687. `https://clist.by/problems/?search=${problem}&resource=1`,
  9688. i18next.t('state.wait', { ns: 'button' }),
  9689. $("<img>").attr("src", "https://clist.by/static/img/logo-48.png"),
  9690. "15px"
  9691. );
  9692.  
  9693. // 检测clist连接
  9694. if (!await validateClistConnection()) {
  9695. problemToolbar.updateText(clistButton, i18next.t('state.netError', { ns: 'button' }));
  9696. return;
  9697. }
  9698.  
  9699. // 题目链接
  9700. let problem_url = window.location.href;
  9701. if (problem_url.includes('/contest/')) {
  9702. problem_url = problem_url.replace(/\/contest\/(\d+)\/problem\/(\w+)[^\w]*/, '/contest/$1/problem/$2');
  9703. } else {
  9704. problem_url = problem_url.replace(/\/problemset\/problem\/(\d+)\/(\w+)/, '/contest/$1/problem/$2');
  9705. }
  9706. if (OJBetter.typeOfPage.is_mSite) problem_url = problem_url.replace(/\/\/(\w+).codeforces.com/, '//codeforces.com'); // 轻量站
  9707.  
  9708. // 比赛名
  9709. // let contest = $('#sidebar').children().first().find('.rtable th').first().text();
  9710.  
  9711. // rating
  9712. problemToolbar.updateText(clistButton, i18next.t('state.loading', { ns: 'button' }));
  9713. let rating = await getRatingFromApi_problem(problem, problem_url);
  9714. if (rating) {
  9715. let className = getClassNameByRating(rating);
  9716. problemToolbar.updateText(clistButton, rating);
  9717. problemToolbar.setBold(clistButton);
  9718. problemToolbar.addClass(clistButton, className);
  9719. } else {
  9720. problemToolbar.updateText(clistButton, i18next.t('state.404', { ns: 'button' }));
  9721. problemToolbar.disableButton(clistButton);
  9722. }
  9723. }
  9724.  
  9725. /**
  9726. * contest页显示Rating
  9727. * @returns {Promise<void>}
  9728. */
  9729. async function showRatingByClist_contest() {
  9730. // 创建Rating显示框
  9731. creatRatingCss();
  9732. let ratingBadges = {};
  9733. $('.datatable .id.left').each(function () {
  9734. let href = 'https://codeforces.com' + $(this).find('a').attr('href');
  9735. let badge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge">${i18next.t('state.wait', { ns: 'button' })}</a>`);
  9736. $(this).find('a').after(badge);
  9737. ratingBadges[href] = badge;
  9738. });
  9739.  
  9740. // 检测clist连接
  9741. if (!await validateClistConnection()) {
  9742. for (let href in ratingBadges) {
  9743. ratingBadges[href].text('error').addClass('ratingBadge_error');
  9744. }
  9745. return;
  9746. }
  9747.  
  9748. // 显示loading
  9749. for (let href in ratingBadges) {
  9750. ratingBadges[href].text(i18next.t('state.loading', { ns: 'button' })).addClass('ratingBadge_loading');
  9751. }
  9752.  
  9753. // 获取Rating
  9754. let contestName = $('#sidebar').children().first().find('.rtable th').first().text();
  9755. let contestUrl = OJB_cleanLink(window.location.href);
  9756. try {
  9757. let problemsMap = await getRatingFromApi_contest(contestName, contestUrl);
  9758.  
  9759. // 填充数据
  9760. for (let href in ratingBadges) {
  9761. if (problemsMap.has(href)) {
  9762. let rating = problemsMap.get(href);
  9763. let className = getClassNameByRating(rating);
  9764. ratingBadges[href].text(rating).addClass(className);
  9765. } else {
  9766. ratingBadges[href].text(i18next.t('state.404', { ns: 'button' })).addClass('ratingBadge_no');
  9767. }
  9768. }
  9769. } catch (error) {
  9770. // 填充数据
  9771. for (let href in ratingBadges) {
  9772. ratingBadges[href].text(i18next.t('state.netError', { ns: 'button' })).addClass('ratingBadge_no');
  9773. }
  9774. console.warn(error);
  9775. }
  9776. }
  9777.  
  9778. /**
  9779. * problemset页显示Rating
  9780. * @returns {Promise<void>}
  9781. */
  9782. async function showRatingByClist_problemset() {
  9783. creatRatingCss();
  9784. let ratingBadges = [];
  9785. const $problems = $('.problems');
  9786. const $trs = $problems.find('tbody tr:gt(0)');
  9787.  
  9788. // 先创建Rating显示框,并将关系存进数组ratingBadges
  9789. for (let i = 0; i < $trs.length; i++) {
  9790. const $tds = $($trs[i]).find('td');
  9791. const $firstDiv = $($tds[1]).find('div:first');
  9792. let problem = $firstDiv.text();
  9793. let problem_url = $firstDiv.find('a').attr('href');
  9794. problem_url = problem_url.replace(/^\/problemset\/problem\/(\d+)\/(\w+)/, 'https://codeforces.com/contest/$1/problem/$2');
  9795.  
  9796. const ratingBadge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge"></a>`);
  9797. const rating = OJB_safeCreateJQElement(`<span class="rating">${i18next.t('state.wait', { ns: 'button' })}</span>`);
  9798. ratingBadge.append(rating);
  9799. $($tds[0]).find('a').after(ratingBadge);
  9800. ratingBadges.push({ ratingBadge, rating, problem, problem_url });
  9801. }
  9802.  
  9803. // 检测clist连接
  9804. if (!await validateClistConnection()) {
  9805. for (let i = 0; i < rating.length; i++) {
  9806. ratingBadges[i].rating.text(i18next.t('state.netError', { ns: 'button' }));
  9807. }
  9808. return;
  9809. }
  9810.  
  9811. // 每次只获取3个rating
  9812. for (let i = 0; i < ratingBadges.length; i += 3) {
  9813. const promises = [];
  9814. const endIndex = Math.min(i + 3, ratingBadges.length);
  9815.  
  9816. for (let j = i; j < endIndex; j++) {
  9817. const ratingBadge = ratingBadges[j];
  9818. // 显示请求中
  9819. ratingBadge.rating.text(i18next.t('state.loading', { ns: 'button' }));
  9820. promises.push(getRatingFromHTML(ratingBadge.problem, ratingBadge.problem_url).catch(error => console.warn(error)));
  9821. }
  9822.  
  9823. const results = await Promise.all(promises);
  9824.  
  9825. for (let j = i; j < endIndex; j++) {
  9826. const result = results[j - i];
  9827. const ratingBadge = ratingBadges[j];
  9828. if (result) {
  9829. let className = getClassNameByRating(result.rating);
  9830. ratingBadge.ratingBadge.addClass(className);
  9831. ratingBadge.rating.text(result.rating);
  9832. } else {
  9833. ratingBadge.rating.text(i18next.t('state.404', { ns: 'button' }));
  9834. }
  9835. }
  9836. }
  9837. }
  9838.  
  9839. /**
  9840. * cf赛制榜单重新着色
  9841. */
  9842. async function recolorStandings() {
  9843. function getColorValue(value) {
  9844. value = Math.max(0, Math.min(1, value));
  9845.  
  9846. const scale = chroma.scale(['#b71c1c', '#ff9800', '#ffc107', '#00aa00']).mode('lch').domain([0, 0.45, 0.7, 1]);
  9847. return scale(value).hex();
  9848. }
  9849. var maxScores = $('.standings tr:first th:nth-child(n+5)')
  9850. .map(function () {
  9851. return $(this).find('span').text();
  9852. })
  9853. .get();
  9854. $('.standings tr:not(:first):not(:last)').each(function () {
  9855. var thElements = $(this).find('td:nth-child(n+5)');
  9856. thElements.each(function (index) {
  9857. var spanElement = $(this).find('span:first');
  9858. var value = parseInt(spanElement.text());
  9859. if (value <= 0 || /[a-zA-Z]/.test(maxScores[index])) return;
  9860. var colorValue = getColorValue(value / maxScores[index]);
  9861. spanElement.css('color', colorValue);
  9862. });
  9863. });
  9864. }
  9865.  
  9866. /**
  9867. * 存放编辑器语言select的值与Monaco语言对应关系的map.
  9868. * @type {Object.<string, string>}
  9869. */
  9870. const value_monacoLanguageMap = {
  9871. "4": "pascal", "6": "php", "7": "python", "9": "csharp", "13": "perl", "20": "scala", "31": "python",
  9872. "32": "go", "34": "javascript", "36": "java", "40": "python", "41": "python", "43": "cpp",
  9873. "50": "cpp", "51": "pascal", "52": "cpp", "54": "cpp", "55": "javascript", "59": "cpp", "60": "java",
  9874. "61": "cpp", "65": "csharp", "67": "ruby", "70": "python", "73": "cpp", "74": "java", "75": "rust",
  9875. "77": "kotlin", "79": "csharp", "80": "cpp", "83": "kotlin", "87": "java"
  9876. };
  9877.  
  9878. /**
  9879. * 更新代码提交页的HTML
  9880. * @param {string} submitUrl 提交页面的URL
  9881. * @param {string} cacheKey 本地缓存的键名
  9882. * @returns {Promise<jQuery<HTMLElement>>} 返回 jQuery 包装的 HTML 元素
  9883. */
  9884. async function CloneOriginalHTML(submitUrl, cacheKey) {
  9885. return OJB_promiseRetryWrapper(async () => {
  9886. const response = await OJB_GMRequest({
  9887. method: 'GET',
  9888. url: submitUrl
  9889. });
  9890. const html = response.responseText;
  9891. const parser = new DOMParser();
  9892. const doc = parser.parseFromString(html, 'text/html');
  9893. const cloneHTML = $(doc.body).html();
  9894. localStorage.setItem(cacheKey, html);
  9895. return $(cloneHTML);
  9896. }, {
  9897. maxRetries: 5,
  9898. retryInterval: 1000,
  9899. errorHandler: (err) => {
  9900. console.error('A network error occurred while retrieving the HTML for the code submission page.', submitUrl);
  9901. }
  9902. });
  9903. }
  9904.  
  9905. /**
  9906. * 获取代码提交页的HTML元素
  9907. * @param {string} submitUrl
  9908. * @returns {Promise<jQuery>}
  9909. */
  9910. async function getSubmitHTML(submitUrl) {
  9911. const cacheKey = 'OJBetter_CloneOriginalHTML';
  9912. const cookieKey = 'OJBetter_CloneOriginalHTML_time';
  9913. if (OJB_getCookie(cookieKey) === '1') {
  9914. // 存在缓存
  9915. CloneOriginalHTML(submitUrl, cacheKey);
  9916. // 校验
  9917. let cloneHTML = $(localStorage.getItem(cacheKey));
  9918. if (cloneHTML.find('form.submit-form').length > 0) {
  9919. return cloneHTML;
  9920. } else {
  9921. // 存在错误,更新缓存
  9922. console.warn(`Cache error detected!\nattempting to update, cache destination submitUrl:\n${submitUrl}`);
  9923. return await CloneOriginalHTML(submitUrl, cacheKey);
  9924. }
  9925.  
  9926. } else {
  9927. // 没有缓存,更新
  9928. document.cookie = `${cookieKey}=1; path=/`;
  9929. return await CloneOriginalHTML(submitUrl, cacheKey);
  9930. }
  9931. }
  9932.  
  9933. // 代码自动保存
  9934. async function saveCode(url, code) {
  9935. try {
  9936. await OJBetter.common.database.editorCode.put({ url, code });
  9937. return 'Code saved successfully';
  9938. } catch (error) {
  9939. throw new Error('Failed to save code');
  9940. }
  9941. }
  9942.  
  9943. async function getCode(url) {
  9944. try {
  9945. const result = await OJBetter.common.database.editorCode.get(url);
  9946. return result ? result.code : null;
  9947. } catch (error) {
  9948. throw new Error('Failed to get code');
  9949. }
  9950. }
  9951.  
  9952. // 创建代码编辑调试表单元素
  9953. async function createCodeEditorForm(submitUrl, cloneHTML) {
  9954. // 表单
  9955. let formDiv = $('<form method="post" id="OJBetter_SubmitForm" class="input-output-copier"></form>');
  9956. $('.ttypography').after(formDiv);
  9957. formDiv.attr('action', submitUrl + "?csrf_token=" + OJBetter.common.cf_csrf_token);
  9958.  
  9959. // 顶部区域
  9960. let topDiv = OJB_safeCreateJQElement(`<div class="topDiv"></div>`);
  9961. let selectLang = cloneHTML.find('select[name="programTypeId"]'); // 语言选择
  9962. selectLang.css({ 'margin': '10px 0px' }).attr('id', 'programTypeId');
  9963. topDiv.append(selectLang);
  9964. let topRightDiv = OJB_safeCreateJQElement(`<div class="topRightDiv"></div>`);
  9965. topDiv.append(topRightDiv);
  9966. formDiv.append(topDiv);
  9967.  
  9968. // 问题选择/编号
  9969. let selectProblem = $('<input name="submittedProblemIndex" style="display:none;"></input>');
  9970. let problemCode;
  9971. if (OJBetter.typeOfPage.is_acmsguru) {
  9972. problemCode = $('h4').eq(0).text();
  9973. let matchResult = problemCode.match(/([A-Z0-9]+)/);
  9974. problemCode = matchResult[0];
  9975. } else if (OJBetter.typeOfPage.is_problemset_problem) {
  9976. let match = window.location.href.match(/\/problem\/([0-9]+?)\/([A-Z0-9]+?)(?!=[A-Z0-9])/);
  9977. problemCode = match[1] + match[2];
  9978. selectProblem.attr('name', 'submittedProblemCode');
  9979. } else {
  9980. problemCode = $('.header .title').eq(0).text();
  9981. let matchResult = problemCode.match(/([A-Z0-9]+)/);
  9982. problemCode = matchResult[0];
  9983. }
  9984. selectProblem.val(problemCode);
  9985. formDiv.append(selectProblem);
  9986.  
  9987. // 隐藏的代码记录
  9988. let sourceDiv = $('<textarea id="sourceCodeTextarea" name="source" style="display: none;"></textarea>');
  9989. formDiv.append(sourceDiv);
  9990.  
  9991. // 代码编辑器
  9992. let editorDiv = $('<div id="OJBetter_editor"></div>');
  9993. formDiv.append(editorDiv);
  9994.  
  9995. // monaco
  9996. let monaco = $('<div id="OJBetter_monaco"></div>');
  9997. editorDiv.append(monaco);
  9998.  
  9999. // 自定义调试
  10000. let customTestDiv = OJB_safeCreateJQElement(`
  10001. <details id="customTestBlock">
  10002. <summary >${i18next.t('customTestBlock.title', { ns: 'codeEditor' })}</summary>
  10003. <div id="customTests" style="min-height: 30px;"></div>
  10004. <div id="control" style="display:flex;">
  10005. <div style="display: flex;margin: 5px;">
  10006. <input type="checkbox" id="onlyCustomTest"}><label for="onlyCustomTest">
  10007. ${i18next.t('customTestBlock.onlyCustom', { ns: 'codeEditor' })}
  10008. </label>
  10009. </div>
  10010. <div style="display: flex;margin: 5px;">
  10011. <input type="checkbox" id="DontShowDiff"}>
  10012. <label for="DontShowDiff">
  10013. ${i18next.t('customTestBlock.DontShowDiff', { ns: 'codeEditor' })}
  10014. </label>
  10015. </div>
  10016. <button type="button" id="addCustomTest">${i18next.t('customTestBlock.add', { ns: 'codeEditor' })}</button>
  10017. </div>
  10018. </details>
  10019. `)
  10020. formDiv.append(customTestDiv);
  10021.  
  10022. // 调试/提交
  10023. let submitDiv = $('<div id="OJBetter_submitDiv"></div>');
  10024. let CompilerArgsInput = $('<input type="text" id="CompilerArgsInput">');
  10025. submitDiv.append(CompilerArgsInput);
  10026.  
  10027. let runButton = OJB_safeCreateJQElement(`
  10028. <button type="button" id="RunTestButton" class="ojb_btn ojb_btn_popover top">
  10029. <i class="iconfont">&#xe6c1;</i>
  10030. <span class="popover_content">${i18next.t('runTestButton.initial', { ns: 'codeEditor' })}</span>
  10031. </button>
  10032. `);
  10033. let submitButton = OJB_safeCreateJQElement(`
  10034. <button id="SubmitButton" class="ojb_btn ojb_btn_popover top" type="submit">
  10035. <i class="iconfont">&#xe633;</i>
  10036. <span class="popover_content">${i18next.t('submitButton', { ns: 'codeEditor' })}</span>
  10037. </button>
  10038. `);
  10039. if (OJBetter.monaco.setting.submitButtonPosition == "bottom") {
  10040. // 添加测试/提交按钮到底部
  10041. submitDiv.append(runButton);
  10042. submitDiv.append(submitButton);
  10043. }
  10044.  
  10045. formDiv.append(submitDiv);
  10046. let CompilerSetting = OJB_safeCreateJQElement(`
  10047. <div id="CompilerSetting"></div>
  10048. `);
  10049. formDiv.append(CompilerSetting);
  10050. let statePanel = OJB_safeCreateJQElement(`
  10051. <div id="statePanel"></div>
  10052. `);
  10053. formDiv.append(statePanel);
  10054.  
  10055. let from = {
  10056. formDiv: formDiv,
  10057. selectLang: selectLang,
  10058. topRightDiv: topRightDiv,
  10059. sourceDiv: sourceDiv,
  10060. editorDiv: editorDiv,
  10061. monaco: monaco,
  10062. runButton: runButton,
  10063. submitButton: submitButton,
  10064. submitDiv: submitDiv,
  10065. CompilerSetting: CompilerSetting,
  10066. statePanel: statePanel
  10067. };
  10068. return from;
  10069. }
  10070.  
  10071. // 解析ace格式的补全规则(acwing)
  10072. function parseAceCompleter(rules, range) {
  10073. const suggestions = [];
  10074. if (rules && rules.templates && rules.templates.items) {
  10075. const items = rules.templates.items;
  10076. for (let i = 0; i < items.length; i++) {
  10077. const item = items[i];
  10078. const parts = item.caption.split(' ');
  10079. for (let i = 0; i < parts.length; i++) {
  10080. if (item.value.startsWith(parts[i])) {
  10081. item.value = item.value.replace(parts[i], parts.slice(0, i + 1).join(' '));
  10082. break;
  10083. }
  10084. }
  10085. const completionItem = {
  10086. label: item.caption,
  10087. kind: monaco.languages.CompletionItemKind.Function,
  10088. insertText: item.value,
  10089. range: range
  10090. };
  10091. suggestions.push(completionItem);
  10092. }
  10093. }
  10094. return { suggestions };
  10095. }
  10096.  
  10097. // 解析monaco格式的补全规则
  10098. function parseMonacoCompleter(rules, range) {
  10099. const suggestions_ = [];
  10100. if (rules && rules.suggestions) {
  10101. const suggestion = rules.suggestions;
  10102. for (let i = 0; i < rules.suggestions.length; i++) {
  10103. const item = suggestion[i];
  10104. const completionItem = {
  10105. ...item,
  10106. range: range
  10107. };
  10108. suggestions_.push(completionItem);
  10109. }
  10110. }
  10111. return { suggestions: suggestions_ };
  10112. }
  10113.  
  10114. /**
  10115. * 创建monaco编辑器的一个实例
  10116. */
  10117. async function createMonacoEditor(language, form, support) {
  10118. // 判断monacoLoader是否加载完毕
  10119. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  10120.  
  10121. /**
  10122. * 通用参数
  10123. */
  10124. var id = 0; // 协议中的id标识
  10125. var workspace = language + "_workspace";
  10126. var rootUri = OJBetter.monaco.lsp.workUri + "/" + workspace;
  10127. // 文件名
  10128. var InstanceID = OJB_getRandomNumber(8).toString();
  10129. var filename = language == "java" ? "hello/src/" + InstanceID : InstanceID;
  10130. // 后缀名
  10131. var fileExtension =
  10132. language === "cpp"
  10133. ? ".cpp"
  10134. : language === "python"
  10135. ? ".py"
  10136. : language === "java"
  10137. ? ".java"
  10138. : "";
  10139. var uri = rootUri + "/" + filename + fileExtension;
  10140. var initialized = false; // 是否已初始化
  10141. var serverInfo; // 服务器返回的支持信息
  10142. var model; // model
  10143. var OJBetter_monaco = {};
  10144. window.OJBetter_monaco = OJBetter_monaco; // 全局方法
  10145.  
  10146. /**
  10147. * 一些工具函数
  10148. */
  10149. // 将lsp格式的rang转换为Monaco格式
  10150. OJBetter_monaco.lspRangeToMonacoRange = function (range) {
  10151. const { start, end } = range;
  10152. return new monaco.Range(
  10153. start.line + 1,
  10154. start.character + 1,
  10155. end.line + 1,
  10156. end.character + 1
  10157. );
  10158. };
  10159. // 将Monaco格式的rang转为lsp格式
  10160. OJBetter_monaco.MonacoRangeTolspRange = function (range) {
  10161. return {
  10162. start: {
  10163. line: range.startLineNumber - 1,
  10164. character: range.startColumn - 1,
  10165. },
  10166. end: {
  10167. line: range.endLineNumber - 1,
  10168. character: range.endColumn - 1,
  10169. },
  10170. };
  10171. };
  10172. // 将Monaco格式的position转为lsp格式的
  10173. OJBetter_monaco.MonacoPositionTolspPosition = function (position) {
  10174. return {
  10175. line: position.lineNumber - 1,
  10176. character: position.column - 1,
  10177. };
  10178. };
  10179. // 将Monaco格式的severity转为lsp格式的
  10180. OJBetter_monaco.MonacoSeverityTolspSeverity = function (severity) {
  10181. switch (severity) {
  10182. case 8:
  10183. return 1;
  10184. case 1:
  10185. return 4;
  10186. case 2:
  10187. return 3;
  10188. case 4:
  10189. return 2;
  10190. default:
  10191. return severity;
  10192. }
  10193. };
  10194. // 将lsp格式的severity转为Monaco格式的
  10195. OJBetter_monaco.lspSeverityToMonacoSeverity = function (severity) {
  10196. switch (severity) {
  10197. case 1:
  10198. return 8;
  10199. case 4:
  10200. return 1;
  10201. case 3:
  10202. return 2;
  10203. case 2:
  10204. return 4;
  10205. default:
  10206. return severity;
  10207. }
  10208. };
  10209. // 收集Monaco数据中的rang数据
  10210. OJBetter_monaco.CollectRange = function (item) {
  10211. return {
  10212. startLineNumber: item.startLineNumber,
  10213. startColumn: item.startColumn,
  10214. endLineNumber: item.endLineNumber,
  10215. endColumn: item.endColumn,
  10216. };
  10217. };
  10218. // 收集Monaco position数据中的rang数据
  10219. OJBetter_monaco.CollectRangeByPosition = function (item) {
  10220. var word = model.getWordUntilPosition(item);
  10221. return {
  10222. startLineNumber: item.lineNumber,
  10223. endLineNumber: item.lineNumber,
  10224. startColumn: word.startColumn,
  10225. endColumn: word.endColumn,
  10226. };
  10227. };
  10228. // 将lsp格式的Edit转换为Monaco格式
  10229. OJBetter_monaco.lspEditToMonacoEdit = function (edit) {
  10230. const edits = [];
  10231.  
  10232. if (language == "python") {
  10233. for (const item1 of edit.documentChanges) {
  10234. for (const item2 of item1.edits) {
  10235. const newElement = {
  10236. textEdit: {
  10237. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10238. text: item2.newText,
  10239. },
  10240. resource: monaco.Uri.parse(item1.textDocument.uri),
  10241. versionId: model.getVersionId(),
  10242. };
  10243. edits.push(newElement);
  10244. }
  10245. }
  10246. } else if (language == "java") {
  10247. for (const item1 in edit.changes) {
  10248. edit.changes[item1].forEach((item2) => {
  10249. const newElement = {
  10250. textEdit: {
  10251. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10252. text: item2.newText,
  10253. },
  10254. resource: uri,
  10255. versionId: model.getVersionId(),
  10256. };
  10257. edits.push(newElement);
  10258. });
  10259. }
  10260. } else {
  10261. for (const key in edit.changes) {
  10262. const arr = edit.changes[key];
  10263. for (const item of arr) {
  10264. const newElement = {
  10265. textEdit: {
  10266. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  10267. text: item.newText,
  10268. },
  10269. resource: monaco.Uri.parse(key),
  10270. versionId: model.getVersionId(),
  10271. };
  10272. edits.push(newElement);
  10273. }
  10274. }
  10275. }
  10276. return { edits: edits };
  10277. };
  10278.  
  10279. /**
  10280. * 实例化一个editor
  10281. */
  10282. uri = monaco.Uri.file(uri);
  10283. model = monaco.editor.createModel('', language, uri);
  10284. OJBetter.monaco.editor = monaco.editor.create(document.getElementById("OJBetter_monaco"), {
  10285. model: model,
  10286. rootUri: rootUri,
  10287. fontSize: 15,
  10288. tabSize: 4,
  10289. theme: OJBetter.basic.darkMode == "dark" ? "vs-dark" : "vs",
  10290. bracketPairColorization: {
  10291. enabled: true,
  10292. independentColorPoolPerBracketType: true,
  10293. },
  10294. automaticLayout: true,
  10295. lineNumbersMinChars: 3,
  10296. matchOnWordStartOnly: false,
  10297. wordWrap: "on",
  10298. wrappingIndent: "same",
  10299. glyphMargin: true,
  10300. formatOnType: true,
  10301. scrollbar: {
  10302. verticalScrollbarSize: 10,
  10303. horizontalScrollbarSize: 10,
  10304. alwaysConsumeMouseWheel: OJBetter.monaco.setting.alwaysConsumeMouseWheel
  10305. },
  10306. suggest: {
  10307. selectionMode: 'never' // 代码建议不自动选择
  10308. }
  10309. });
  10310.  
  10311. /**
  10312. * 添加快捷功能
  10313. */
  10314. (OJBetter_monaco.addShortCuts = async () => {
  10315. // 从配置信息更新字体大小
  10316. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(OJBetter.monaco.setting.fontsize) });
  10317.  
  10318. // 调整字体大小
  10319. let changeSize = OJB_safeCreateJQElement(`
  10320. <div class="ojb_btn ojb_btn_popover top">
  10321. <input type="number" id="fontSizeInput" value="${OJBetter.monaco.setting.fontsize}">
  10322. <span class="popover_content">${i18next.t('fontSizeInput', { ns: 'codeEditor' })}</span>
  10323. </div>`)
  10324. form.topRightDiv.append(changeSize);
  10325. changeSize.find('input#fontSizeInput').on('input', function () {
  10326. var size = $(this).val();
  10327. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(size) });
  10328. GM_setValue('editorFontSize', size);
  10329. });
  10330.  
  10331. // 全屏按钮
  10332. let fullscreenButton = OJB_safeCreateJQElement(`
  10333. <button type="button" class="ojb_btn ojb_btn_popover top">
  10334. <i class="iconfont">&#xe606;</i>
  10335. <span class="popover_content">${i18next.t('fullscreenButton', { ns: 'codeEditor' })}</span>
  10336. </button>
  10337. `);
  10338. form.topRightDiv.append(fullscreenButton);
  10339. fullscreenButton.on('click', enterFullscreen);
  10340.  
  10341. // 固定到底部按钮
  10342. let fixToBottomButton = OJB_safeCreateJQElement(`
  10343. <button type="button" class="ojb_btn ojb_btn_popover top">
  10344. <i class="iconfont">&#xe607;</i>
  10345. <span class="popover_content">${i18next.t('fixToBottomButton', { ns: 'codeEditor' })}</span>
  10346. </button>
  10347. `);
  10348. form.topRightDiv.append(fixToBottomButton);
  10349. fixToBottomButton.on('click', fixToBottom);
  10350.  
  10351. // 固定到右侧按钮
  10352. let fixToRightButton = OJB_safeCreateJQElement(`
  10353. <button type="button" class="ojb_btn ojb_btn_popover top">
  10354. <i class="iconfont">&#xe605;</i>
  10355. <span class="popover_content">${i18next.t('fixToRightButton', { ns: 'codeEditor' })}</span>
  10356. </button>
  10357. `);
  10358. form.topRightDiv.append(fixToRightButton);
  10359. fixToRightButton.on('click', fixToRight);
  10360.  
  10361. // 添加测试/提交按钮到顶部
  10362. if (OJBetter.monaco.setting.submitButtonPosition == "top") {
  10363. form.topRightDiv.append(form.runButton);
  10364. form.topRightDiv.append(form.submitButton);
  10365. }
  10366.  
  10367. // 选择记忆
  10368. if (!OJBetter.monaco.setting.position_initialized) {
  10369. OJBetter.monaco.setting.position_initialized = true; // 标记是否已经初始化过
  10370. if (OJBetter.monaco.setting.position == "full") {
  10371. fullscreenButton.click();
  10372. } else if (OJBetter.monaco.setting.position == "bottom") {
  10373. fixToBottomButton.click();
  10374. } else if (OJBetter.monaco.setting.position == "right") {
  10375. fixToRightButton.click();
  10376. }
  10377. }
  10378.  
  10379. // 禁用按钮
  10380. function disableButtons() {
  10381. fullscreenButton.prop("disabled", true);
  10382. fixToBottomButton.prop("disabled", true);
  10383. fixToRightButton.prop("disabled", true);
  10384. }
  10385.  
  10386. // 启用按钮
  10387. function enableButtons() {
  10388. fullscreenButton.prop("disabled", false);
  10389. fixToBottomButton.prop("disabled", false);
  10390. fixToRightButton.prop("disabled", false);
  10391. }
  10392.  
  10393. // 进入全屏
  10394. function enterFullscreen() {
  10395. let editor = $('#OJBetter_editor');
  10396. editor.addClass('fullscreen');
  10397.  
  10398. // 取消按钮
  10399. let cancelButton = OJB_safeCreateJQElement(`
  10400. <button type="button" class="ojb_btn ojb_btn_popover top primary exit_button_bottom">
  10401. <i class="iconfont">&#xe60b;</i>
  10402. <span class="popover_content">${i18next.t('exitFullscreenButton', { ns: 'codeEditor' })}</span>
  10403. </button>
  10404. `).on('click', () => exitFullscreen(cancelButton));
  10405. $('body').append(cancelButton);
  10406.  
  10407. disableButtons();
  10408. GM_setValue("monacoEditor_position", "full");
  10409. }
  10410.  
  10411. // 退出全屏
  10412. const exitFullscreen = (cancelButton) => {
  10413. let editor = $('#OJBetter_editor');
  10414. editor.removeClass('fullscreen');
  10415. cancelButton.remove();
  10416. enableButtons();
  10417. GM_setValue("monacoEditor_position", "initial");
  10418. };
  10419.  
  10420. // 固定到底部
  10421. function fixToBottom() {
  10422. let editor = $('#OJBetter_editor');
  10423. editor.addClass('bottom');
  10424.  
  10425. let halfHeight = $(window).height() * 0.5;
  10426. let blankSpace = $('<div>', {
  10427. 'class': 'blank-space',
  10428. 'style': 'height: ' + (halfHeight + 30) + 'px;'
  10429. });
  10430. $('body').append(blankSpace);
  10431.  
  10432. let cancelButton = OJB_safeCreateJQElement(`
  10433. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10434. <i class="iconfont">&#xe625;</i>
  10435. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10436. </button>
  10437. `).on('click', () => cancelFixingToBottom(cancelButton, blankSpace));
  10438. $('body').append(cancelButton);
  10439.  
  10440. disableButtons();
  10441. GM_setValue("monacoEditor_position", "bottom");
  10442. }
  10443.  
  10444. // 取消固定到底部
  10445. const cancelFixingToBottom = (cancelButton, blankSpace) => {
  10446. let editor = $('#OJBetter_editor');
  10447. editor.removeClass('bottom');
  10448. cancelButton.remove();
  10449. blankSpace.remove();
  10450. enableButtons();
  10451. GM_setValue("monacoEditor_position", "initial");
  10452. };
  10453.  
  10454. // 固定到右侧边栏
  10455. function fixToRight() {
  10456. const sidebar = $('#sidebar').hide();
  10457.  
  10458. // 添加样式
  10459. const styleElement = GM_addStyle(`
  10460. #body {
  10461. min-width: 50vw;
  10462. max-width: 50vw;
  10463. max-height: 100vh;
  10464. overflow-x: hidden;
  10465. overflow-y: auto;
  10466. padding: 1rem;
  10467. box-sizing: border-box;
  10468. }
  10469. body {
  10470. margin: 0px;
  10471. }
  10472. .content-with-sidebar {
  10473. margin-right: 0px !important;
  10474. }
  10475. .menu-list li {
  10476. margin-right: 0.5em;
  10477. }
  10478. .menu-list li a {
  10479. font-size: 1.4rem;
  10480. }
  10481. #OJBetter_editor{
  10482. height: 100vh;
  10483. width: 50vw;
  10484. }
  10485. `);
  10486.  
  10487. // 包装一层div
  10488. $('#body').wrap('<div id="right-side-wrapper" style="display:flex; max-width: 100vw; overflow: hidden;"></div>');
  10489. const blankSpace = $('<div>').appendTo('#right-side-wrapper');
  10490.  
  10491. // 移动编辑器
  10492. const editor = $('#OJBetter_editor').prependTo(blankSpace).addClass('right-side');
  10493.  
  10494. // 取消按钮
  10495. const cancelButton = OJB_safeCreateJQElement(`
  10496. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10497. <i class="iconfont">&#xe625;</i>
  10498. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10499. </button>
  10500. `).on('click', () => cancelFixingToRight(sidebar, styleElement, editor, cancelButton)).appendTo('body');
  10501.  
  10502. disableButtons();
  10503. GM_setValue("monacoEditor_position", "right");
  10504.  
  10505. // 补丁:修复固定到右侧导致的样例元素.sample-test相关代码重复执行的问题(具体原因未查)
  10506. $('.sample-test').find('.title').each((i, e) => {
  10507. if ($(e).find('.input-output-copier').length > 1) {
  10508. $(e).find('.input-output-copier').first().remove();
  10509. }
  10510. });
  10511. darkModeStyleAdjustment();
  10512. }
  10513.  
  10514. const cancelFixingToRight = (sidebar, styleElement, editor, cancelButton) => {
  10515. sidebar.show();
  10516. // 移回来
  10517. editor.insertAfter(form.sourceDiv).removeClass('right-side');
  10518.  
  10519. // 移除包装
  10520. $('#body').unwrap();
  10521. cancelButton.remove();
  10522. styleElement.remove(); // 移除添加的样式
  10523.  
  10524. enableButtons();
  10525. GM_setValue("monacoEditor_position", "initial");
  10526. }
  10527.  
  10528. // 代码同步与保存
  10529. var nowUrl = window.location.href;
  10530. nowUrl = nowUrl.replace(/#/, ""); // 当页面存在更改时url会多出一个#,去掉
  10531. const code = await getCode(nowUrl);
  10532. if (code) {
  10533. OJBetter.monaco.editor.setValue(code); // 恢复代码
  10534. form.sourceDiv.val(code);
  10535. }
  10536. OJBetter.monaco.editor.onDidChangeModelContent(async () => {
  10537. // 将monaco editor的内容同步到sourceDiv
  10538. const code = OJBetter.monaco.editor.getValue();
  10539. form.sourceDiv.val(code);
  10540. await saveCode(nowUrl, code);
  10541. });
  10542. })();
  10543.  
  10544. /**
  10545. * 注册本地自动补全
  10546. */
  10547. (OJBetter_monaco.RegisterLocalComplet = async () => {
  10548. // 补全器注册函数
  10549. function registMyCompletionItemProvider(language, genre, rule) {
  10550. if (genre == "monaco") {
  10551. monaco.languages.registerCompletionItemProvider(language, {
  10552. provideCompletionItems: function (model, position) {
  10553. return parseMonacoCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10554. }
  10555. })
  10556. } else if (genre == "ace") {
  10557. monaco.languages.registerCompletionItemProvider(language, {
  10558. provideCompletionItems: function (model, position) {
  10559. return parseAceCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10560. }
  10561. })
  10562. }
  10563. }
  10564.  
  10565. // 注册acwing cpp 模板
  10566. if (language == "cpp" && OJBetter.monaco.complet.cppCodeTemplate) {
  10567. try {
  10568. var acwing_cpp_code_completer = JSON.parse(GM_getResourceText("acwing_cpp_code_completer"));
  10569. registMyCompletionItemProvider('cpp', 'ace', acwing_cpp_code_completer);
  10570. } catch (error) {
  10571. console.error("Error registering acwing cpp template:", error);
  10572. }
  10573. }
  10574.  
  10575. // 注册自定义的补全
  10576. let complet_length = OJBetter.monaco.complet.customConfig.configurations.length;
  10577. if (complet_length > 0) {
  10578. for (let i = 0; i < complet_length; i++) {
  10579. let item = OJBetter.monaco.complet.customConfig.configurations[i];
  10580. if (item.isChoose && item.language == language) {
  10581. try {
  10582. let rule = await OJB_getExternalJSON(item.jsonUrl);
  10583. registMyCompletionItemProvider(item.language, item.genre, rule);
  10584. } catch (error) {
  10585. console.error(`Error registering custom completer for ${item.language}:`, error);
  10586. }
  10587. }
  10588. }
  10589. }
  10590. })();
  10591.  
  10592. if (!support || !OJBetter.monaco.lsp.enabled) { return; } // 如果不支持lsp,则到此为止
  10593.  
  10594. /**
  10595. * LSP连接状态指示
  10596. */
  10597. const lspStateButton = OJB_safeCreateJQElement(`
  10598. <div id="lspStateDiv" class="ojb_btn ojb_btn_popover top loading">
  10599. <i class="iconfont">&#xe658;</i>
  10600. <span class="popover_content">${i18next.t('lsp.connect', { ns: 'codeEditor' })}</span>
  10601. </div>
  10602. `).on('click', () => {
  10603. OJB_showModal(LSPLogDiv);
  10604. LSPLogDiv.show();
  10605. });
  10606. form.topRightDiv.prepend(lspStateButton);
  10607.  
  10608. const LSPLogDiv = OJB_safeCreateJQElement(`
  10609. <dialog id="LSPLog" style="display: none;">
  10610. <button class="ojb_btn">${i18next.t('close', { ns: 'common' })}</button>
  10611. <div id="LSPLogList" style="overflow: auto;"></div>
  10612. <dialog>`);
  10613. $('body').append(LSPLogDiv);
  10614.  
  10615. const LSPLogList = $('<ul></ul>');
  10616. $('#LSPLogList').append(LSPLogList);
  10617.  
  10618. const closeButton = LSPLogDiv.find('button');
  10619. closeButton.on('click', function () {
  10620. OJB_closeModal(LSPLogDiv);
  10621. });
  10622.  
  10623. /**
  10624. * 推送新的消息到LSP日志中
  10625. * @param {'error' | 'warn' | 'info'} status
  10626. * @param {string} msg
  10627. * @param {boolean} data
  10628. */
  10629. function pushLSPLogMessage(status, msg, data) {
  10630. var li = $('<li>').text('[' + new Date().toLocaleString() + '] ' + msg);
  10631. if (status === 'error') {
  10632. li.attr('style', 'color: #f44336;');
  10633. } else if (status === 'warn') {
  10634. li.attr('style', 'color: #ff9800;');
  10635. } else if (status === 'info') {
  10636. li.attr('style', 'color: #616161;');
  10637. }
  10638. if (data) {
  10639. var jsonText = JSON.stringify(data, null, 2);
  10640. var details = $('<details>');
  10641. var summary = $('<summary>').text('Data');
  10642. var pre = $('<pre>').text(jsonText);
  10643. details.append(summary, pre);
  10644. li.append(details);
  10645. }
  10646. LSPLogList.append(li);
  10647. }
  10648.  
  10649. /**
  10650. * 添加状态底栏
  10651. */
  10652. var statusBar = $('<div id="OJBetter_statusBar">');
  10653. form.editorDiv.append(statusBar);
  10654.  
  10655. /**
  10656. * languageSocket
  10657. */
  10658. var url = OJBetter.monaco.lsp.socketUrl;
  10659. var languageSocket = new WebSocket(url + language);
  10660. OJBetter.monaco.lsp.socket.push(languageSocket);
  10661. var languageSocketState = false;
  10662. var responseHandlers = new Map(); // 映射表,需要等待返回数据的请求 -> 对应的事件触发函数
  10663.  
  10664. languageSocket.onopen = () => {
  10665. languageSocketState = true;
  10666. lspStateButton.setButtonPopover(i18next.t('lsp.waitingAnswer', { ns: 'codeEditor' }));
  10667. pushLSPLogMessage("info", `languageSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10668. };
  10669. languageSocket.onmessage = (event) => {
  10670. const message = JSON.parse(event.data);
  10671. if (message.id === 0 && message.result) {
  10672. // 初始化完成
  10673. lspStateButton.setButtonState('success', i18next.t('lsp.connected', { ns: 'codeEditor' }));
  10674. pushLSPLogMessage("info", `Initialization ${i18next.t('lsp.state.finished', { ns: 'logMessage' })}`);
  10675. serverInfo = message.result; // 存下服务器支持信息
  10676. OJBetter_monaco.openDocRequest(); // 打开文档
  10677. if (!OJBetter.monaco.setting.language.includes(language)) {
  10678. OJBetter.monaco.setting.language.push(language);
  10679. OJBetter_monaco.RegistrationAfterInit(); // 注册语言及功能
  10680. } else {
  10681. location.reload(); // 这里有问题,先贴个补丁
  10682. }
  10683. OJBetter_monaco.PassiveReceiveHandler(); // 注册被动接收函数
  10684. } else if (message.id === 0 && message.error) {
  10685. pushLSPLogMessage("warn", `Initialization ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`);
  10686. } else if (message.id !== undefined && responseHandlers.has(message.id)) {
  10687. // 如果收到带有id字段的消息,则回传给对应的事件触发函数
  10688. const handler = responseHandlers.get(message.id);
  10689. if (handler) {
  10690. handler(message);
  10691. responseHandlers.delete(message.id); // 删除已处理的事件触发函数
  10692. }
  10693. } else if (message.method == "textDocument/publishDiagnostics") {
  10694. // 接收代码诊断推送
  10695. OJBetter_monaco.updateMarkers(message);
  10696. } else if (message.method == "workspace/applyEdit") {
  10697. // 应用服务器推送的更改
  10698. OJBetter_monaco.applyEdit(message);
  10699. }
  10700. };
  10701. languageSocket.onerror = (error) => {
  10702. pushLSPLogMessage("error", `languageSocket ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`, error);
  10703. console.warn(`Error connecting to languageSocket: ${error}`)
  10704. };
  10705. languageSocket.onclose = (event) => {
  10706. languageSocketState = false;
  10707. lspStateButton.setButtonState('error', i18next.t('lsp.error', { ns: 'codeEditor' }));
  10708. pushLSPLogMessage("warn", `languageSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`);
  10709. };
  10710.  
  10711. /**
  10712. * 等待LanguageSocketState
  10713. */
  10714. async function waitForLanguageSocketState() {
  10715. return new Promise((resolve) => {
  10716. const checkInitialized = () => {
  10717. if (languageSocketState) {
  10718. resolve();
  10719. } else {
  10720. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10721. }
  10722. };
  10723. checkInitialized();
  10724. });
  10725. }
  10726.  
  10727. // 等待lsp响应初始化结果
  10728. async function waitForInitialized() {
  10729. return new Promise((resolve) => {
  10730. const checkInitialized = () => {
  10731. if (initialized) {
  10732. resolve();
  10733. } else {
  10734. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10735. }
  10736. };
  10737. checkInitialized();
  10738. });
  10739. }
  10740.  
  10741. /**
  10742. * 与languageSocket通信的包装方法
  10743. */
  10744. async function sendMessage(data, requiresResponse, callback) {
  10745. if (!initialized) {
  10746. await waitForInitialized(); // 等待initialized为真
  10747. }
  10748. if (requiresResponse) {
  10749. responseHandlers.set(data.id, callback) // 将事件触发函数与id关联起来
  10750. }
  10751. if (!languageSocketState) await waitForLanguageSocketState();
  10752. languageSocket.send(JSON.stringify(data));
  10753. }
  10754. // 发送消息并等待返回结果
  10755. function fetchData(params, callback) {
  10756. sendMessage(params, true, callback);
  10757. }
  10758. // 发送消息,不需要等待返回结果
  10759. function sendData(data) {
  10760. sendMessage(data, false);
  10761. }
  10762.  
  10763. /**
  10764. * 代码文件更新fileWebSocket
  10765. */
  10766. var fileWebSocket = new WebSocket(url + "file");
  10767. var fileWebSocketState = false;
  10768. OJBetter.monaco.lsp.socket.push(fileWebSocket);
  10769. fileWebSocket.onopen = () => {
  10770. fileWebSocketState = true;
  10771. pushLSPLogMessage("info", `fileWebSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10772. };
  10773. fileWebSocket.onclose = (ev) => {
  10774. fileWebSocketState = false;
  10775. pushLSPLogMessage("warn", `fileWebSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`, ev);
  10776. };
  10777. fileWebSocket.onmessage = (ev) => {
  10778. let message = JSON.parse(ev.data);
  10779. if (message.result !== "ok")
  10780. pushLSPLogMessage("error", `update file failed: ${ev}`);
  10781. };
  10782. fileWebSocket.onerror = (error) => {
  10783. console.warn(`Error connecting to fileWebSocket: ${error}`);
  10784. };
  10785. async function updateFile(workspace, filename, fileExtension, code) {
  10786. async function waitForfileWebSocketState() {
  10787. return new Promise((resolve) => {
  10788. const checkInitialized = () => {
  10789. if (fileWebSocketState) {
  10790. resolve();
  10791. } else {
  10792. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10793. }
  10794. };
  10795. checkInitialized();
  10796. });
  10797. }
  10798. if (!fileWebSocketState) await waitForfileWebSocketState();
  10799. fileWebSocket.send(
  10800. JSON.stringify({
  10801. type: "update",
  10802. workspace,
  10803. filename,
  10804. fileExtension,
  10805. code,
  10806. })
  10807. );
  10808. }
  10809.  
  10810. /**
  10811. * 发送初始化请求
  10812. */
  10813. OJBetter_monaco.Initialize = () => {
  10814. //初始化initialize
  10815. const capabilities = {
  10816. workspace: {
  10817. applyEdit: true,
  10818. },
  10819. textDocument: {
  10820. publishDiagnostics: {
  10821. relatedInformation: true,
  10822. versionSupport: false,
  10823. tagSupport: {
  10824. valueSet: [1, 2],
  10825. },
  10826. codeDescriptionSupport: true,
  10827. },
  10828. completion: {
  10829. contextSupport: true,
  10830. completionItem: {
  10831. snippetSupport: true,
  10832. commitCharactersSupport: true,
  10833. documentationFormat: ["markdown", "plaintext"],
  10834. deprecatedSupport: true,
  10835. preselectSupport: true,
  10836. tagSupport: {
  10837. valueSet: [1],
  10838. },
  10839. insertReplaceSupport: true,
  10840. resolveSupport: {
  10841. properties: [
  10842. "documentation",
  10843. "detail",
  10844. "additionalTextEdits",
  10845. ],
  10846. },
  10847. insertTextModeSupport: {
  10848. valueSet: [1, 2],
  10849. },
  10850. },
  10851. },
  10852. hover: {
  10853. dynamicRegistration: true,
  10854. contentFormat: ["markdown", "plaintext"],
  10855. },
  10856. signatureHelp: {
  10857. signatureInformation: {
  10858. documentationFormat: ["markdown", "plaintext"],
  10859. parameterInformation: {
  10860. labelOffsetSupport: true,
  10861. },
  10862. activeParameterSupport: true,
  10863. },
  10864. contextSupport: true,
  10865. },
  10866. definition: {
  10867. dynamicRegistration: true,
  10868. linkSupport: true,
  10869. },
  10870. references: {
  10871. dynamicRegistration: true,
  10872. },
  10873. documentHighlight: {
  10874. dynamicRegistration: true,
  10875. },
  10876. codeAction: {
  10877. codeActionLiteralSupport: {
  10878. codeActionKind: {
  10879. valueSet:
  10880. language == "java"
  10881. ? []
  10882. : [
  10883. "",
  10884. "quickfix",
  10885. "refactor",
  10886. "refactor.extract",
  10887. "refactor.inline",
  10888. "refactor.rewrite",
  10889. "source",
  10890. "source.organizeImports",
  10891. ],
  10892. },
  10893. },
  10894. },
  10895. rename: {
  10896. dynamicRegistration: true,
  10897. prepareSupport: true,
  10898. prepareSupportDefaultBehavior: 1,
  10899. honorsChangeAnnotations: true,
  10900. },
  10901. documentLink: {
  10902. tooltipSupport: true,
  10903. },
  10904. typeDefinition: {
  10905. dynamicRegistration: true,
  10906. linkSupport: true,
  10907. },
  10908. implementation: {
  10909. dynamicRegistration: true,
  10910. linkSupport: true,
  10911. },
  10912. colorProvider: {
  10913. dynamicRegistration: true,
  10914. },
  10915. foldingRange: {
  10916. dynamicRegistration: true,
  10917. rangeLimit: 5000,
  10918. lineFoldingOnly: true,
  10919. },
  10920. declaration: {
  10921. dynamicRegistration: true,
  10922. linkSupport: true,
  10923. },
  10924. semanticTokens: {
  10925. dynamicRegistration: true,
  10926. tokenTypes: [
  10927. "namespace",
  10928. "type",
  10929. "class",
  10930. "enum",
  10931. "interface",
  10932. "struct",
  10933. "typeParameter",
  10934. "parameter",
  10935. "variable",
  10936. "property",
  10937. "enumMember",
  10938. "event",
  10939. "function",
  10940. "method",
  10941. "macro",
  10942. "keyword",
  10943. "modifier",
  10944. "comment",
  10945. "string",
  10946. "number",
  10947. "regexp",
  10948. "operator",
  10949. ],
  10950. tokenModifiers: [
  10951. "declaration",
  10952. "definition",
  10953. "readonly",
  10954. "static",
  10955. "deprecated",
  10956. "abstract",
  10957. "async",
  10958. "modification",
  10959. "documentation",
  10960. "defaultLibrary",
  10961. ],
  10962. formats: ["relative"],
  10963. requests: {
  10964. range: true,
  10965. full: {
  10966. delta: true,
  10967. },
  10968. },
  10969. multilineTokenSupport: false,
  10970. overlappingTokenSupport: false,
  10971. },
  10972. callHierarchy: {
  10973. dynamicRegistration: true,
  10974. },
  10975. },
  10976. window: {
  10977. showMessage: {
  10978. messageActionItem: {
  10979. additionalPropertiesSupport: true,
  10980. },
  10981. },
  10982. showDocument: {
  10983. support: true,
  10984. },
  10985. workDoneProgress: true,
  10986. },
  10987. general: {
  10988. regularExpressions: {
  10989. engine: "ECMAScript",
  10990. version: "ES2020",
  10991. },
  10992. markdown: {
  10993. parser: "marked",
  10994. version: "1.1.0",
  10995. },
  10996. },
  10997. };
  10998.  
  10999. const initializeRequest = {
  11000. id: id++,
  11001. jsonrpc: "2.0",
  11002. method: "initialize",
  11003. params: {
  11004. processId: null,
  11005. clientInfo: {
  11006. name: "CFMonaco" + InstanceID,
  11007. },
  11008. locale: "zh-CN",
  11009. rootPath: null,
  11010. rootUri: null,
  11011. capabilities: capabilities,
  11012. trace: "off",
  11013. workspaceFolders: [
  11014. {
  11015. uri:
  11016. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  11017. name:
  11018. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  11019. },
  11020. ],
  11021. },
  11022. };
  11023. languageSocket.send(JSON.stringify(initializeRequest));
  11024.  
  11025. // 打开文档函数
  11026. OJBetter_monaco.openDocRequest = function () {
  11027. const initializ = {
  11028. jsonrpc: "2.0",
  11029. method: "initialized",
  11030. params: {},
  11031. };
  11032. languageSocket.send(JSON.stringify(initializ));
  11033. const openDocRequest = {
  11034. jsonrpc: "2.0",
  11035. method: "textDocument/didOpen",
  11036. params: {
  11037. textDocument: {
  11038. uri: model.uri.toString(),
  11039. languageId: language,
  11040. version: model.getVersionId(),
  11041. text: model.getValue(),
  11042. },
  11043. },
  11044. };
  11045. languageSocket.send(JSON.stringify(openDocRequest));
  11046. initialized = true; // 初始化完成,这里确认逻辑待完善
  11047. };
  11048.  
  11049. // 初始化更新文件
  11050. updateFile(workspace, filename, fileExtension, model.getValue());
  11051. }
  11052.  
  11053. /**
  11054. * 注册语言及功能
  11055. */
  11056. OJBetter_monaco.RegistrationAfterInit = () => {
  11057. // 注册语言
  11058. monaco.languages.register({ id: language });
  11059.  
  11060. // 注册"Command"
  11061. (function registerCommand() {
  11062. serverInfo.capabilities.executeCommandProvider.commands.forEach(
  11063. (item) => {
  11064. pushLSPLogMessage("info", `${i18next.t('lsp.server.regist', { ns: 'logMessage' })}`, item);
  11065. monaco.editor.registerCommand(item, (accessor, ...args) => {
  11066. sendData({
  11067. jsonrpc: "2.0",
  11068. id: id++,
  11069. method: "workspace/executeCommand",
  11070. params: {
  11071. command: item,
  11072. arguments: args,
  11073. },
  11074. });
  11075. });
  11076. }
  11077. );
  11078. })();
  11079.  
  11080. // 注册"增量更新"
  11081. model.onDidChangeContent((event) => {
  11082. updateFile(workspace, filename, fileExtension, model.getValue()); // 更新文件
  11083. const changeDocRequest = {
  11084. jsonrpc: "2.0",
  11085. method: "textDocument/didChange",
  11086. params: {
  11087. textDocument: {
  11088. uri: model.uri.toString(),
  11089. version: model.getVersionId(),
  11090. },
  11091. contentChanges: event.changes.map((change) => ({
  11092. range: OJBetter_monaco.MonacoRangeTolspRange(change.range),
  11093. rangeLength: change.rangeLength,
  11094. text: change.text,
  11095. })),
  11096. },
  11097. };
  11098. sendData(changeDocRequest);
  11099. });
  11100.  
  11101. //注册"自动补全"
  11102. monaco.languages.registerCompletionItemProvider(language, {
  11103. provideCompletionItems: (model, position, context) => {
  11104. const request = {
  11105. jsonrpc: "2.0",
  11106. id: id++,
  11107. method: "textDocument/completion",
  11108. params: {
  11109. textDocument: {
  11110. uri: model.uri.toString(),
  11111. },
  11112. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11113. context: {
  11114. triggerKind: context.triggerKind + 1, // 这里要+1,两边的定义不一样。。。
  11115. triggerCharacter: context.triggerCharacter,
  11116. },
  11117. },
  11118. };
  11119. return new Promise((resolve, reject) => {
  11120. fetchData(request, (response) => {
  11121. const result = response.result;
  11122. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11123. if (!result) return resolve(null);
  11124. const CompletionItems = {
  11125. suggestions: result.items.map(
  11126. ({
  11127. label,
  11128. kind,
  11129. filterText,
  11130. insertText,
  11131. insertTextFormat,
  11132. sortText,
  11133. textEdit,
  11134. documentation,
  11135. additionalTextEdits,
  11136. }) => ({
  11137. additionalTextEdits: additionalTextEdits
  11138. ? additionalTextEdits.map(({ newText, range }) => ({
  11139. text: newText,
  11140. range: OJBetter_monaco.lspRangeToMonacoRange(range),
  11141. }))
  11142. : [],
  11143. documentation: documentation ? documentation.value : "",
  11144. filterText,
  11145. insertText: insertText ? insertText : textEdit.newText,
  11146. insertTextRules:
  11147. insertTextFormat === 2
  11148. ? monaco.languages.CompletionItemInsertTextRule
  11149. .InsertAsSnippet
  11150. : monaco.languages.CompletionItemInsertTextRule
  11151. .KeepWhitespace,
  11152. kind,
  11153. label,
  11154. sortText,
  11155. range: textEdit
  11156. ? textEdit.range
  11157. ? OJBetter_monaco.lspRangeToMonacoRange(textEdit.range)
  11158. : OJBetter_monaco.lspRangeToMonacoRange(textEdit.insert)
  11159. : null,
  11160. })
  11161. ),
  11162. };
  11163. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, CompletionItems);
  11164. resolve(CompletionItems);
  11165. });
  11166. });
  11167. },
  11168. });
  11169.  
  11170. // 注册"代码修复"
  11171. monaco.languages.registerCodeActionProvider(language, {
  11172. provideCodeActions: (model, range, context) => {
  11173. const request = {
  11174. id: id++,
  11175. jsonrpc: "2.0",
  11176. method: "textDocument/codeAction",
  11177. params: {
  11178. textDocument: {
  11179. uri: model.uri.toString(),
  11180. },
  11181. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11182. context: {
  11183. diagnostics: context.markers.map((item) => ({
  11184. range: OJBetter_monaco.MonacoRangeTolspRange({
  11185. startLineNumber: item.startLineNumber,
  11186. startColumn: item.startColumn,
  11187. endLineNumber: item.endLineNumber,
  11188. endColumn: item.endColumn,
  11189. }),
  11190. severity: OJBetter_monaco.MonacoSeverityTolspSeverity(
  11191. item.severity
  11192. ),
  11193. code: item.code,
  11194. source: item.source,
  11195. message: item.message,
  11196. tags: item.tags,
  11197. relatedInformation: item.relatedInformation
  11198. ? item.relatedInformation.map((item) => ({
  11199. location: {
  11200. uri: item.resource.toString(),
  11201. range: OJBetter_monaco.MonacoRangeTolspRange({
  11202. startLineNumber: item.startLineNumber,
  11203. startColumn: item.startColumn,
  11204. endLineNumber: item.endLineNumber,
  11205. endColumn: item.endColumn,
  11206. }),
  11207. },
  11208. message: item.message,
  11209. }))
  11210. : null,
  11211. })),
  11212. only: context.only ? [context.only] : [],
  11213. triggerKind: context.trigger,
  11214. },
  11215. },
  11216. };
  11217.  
  11218. return new Promise((resolve, reject) => {
  11219. fetchData(request, (response) => {
  11220. const result = response.result;
  11221. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11222. if (!result) return resolve(null);
  11223. const codeAction = {
  11224. actions: result.map((item) => ({
  11225. title: item.title,
  11226. kind: item.kind ? item.kind : "quickfix",
  11227. command: item.command
  11228. ? item.command.command
  11229. ? {
  11230. id: item.command.command,
  11231. arguments: item.command.arguments,
  11232. title: item.command.title,
  11233. }
  11234. : null
  11235. : null,
  11236. diagnostics: item.diagnostics
  11237. ? item.diagnostics.map((item) => ({
  11238. code: item.code,
  11239. message: item.message,
  11240. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11241. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(
  11242. item.severity
  11243. ),
  11244. source: item.source,
  11245. }))
  11246. : null,
  11247. edit: item.edit
  11248. ? OJBetter_monaco.lspEditToMonacoEdit(item.edit)
  11249. : item.arguments
  11250. ? {
  11251. edits: item.arguments.flatMap(
  11252. (item1) => OJBetter_monaco.lspEditToMonacoEdit(item1).edits
  11253. ),
  11254. }
  11255. : null,
  11256. })),
  11257. dispose: () => { },
  11258. };
  11259. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, codeAction);
  11260.  
  11261. resolve(codeAction);
  11262. });
  11263. });
  11264. },
  11265. });
  11266.  
  11267. // 注册"hover提示"
  11268. monaco.languages.registerHoverProvider(language, {
  11269. provideHover: (model, position) => {
  11270. const request = {
  11271. jsonrpc: "2.0",
  11272. id: id++,
  11273. method: "textDocument/hover",
  11274. params: {
  11275. textDocument: {
  11276. uri: model.uri.toString(),
  11277. },
  11278. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11279. },
  11280. };
  11281.  
  11282. return new Promise((resolve, reject) => {
  11283. fetchData(request, (response) => {
  11284. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11285. const result = response.result;
  11286.  
  11287. if (!result) return resolve(null);
  11288. const Hover = {
  11289. range: result.range
  11290. ? OJBetter_monaco.lspRangeToMonacoRange(result.range)
  11291. : new monaco.Range(
  11292. position.lineNumber,
  11293. position.column,
  11294. position.lineNumber,
  11295. position.column
  11296. ),
  11297. contents: Array.isArray(result.contents)
  11298. ? result.contents.map((item) => ({
  11299. value: item.value ? item.value : item,
  11300. }))
  11301. : [
  11302. {
  11303. value: result.contents.value,
  11304. },
  11305. ],
  11306. };
  11307. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, Hover);
  11308. resolve(Hover);
  11309. });
  11310. });
  11311. },
  11312. });
  11313.  
  11314. // 注册"inlay提示"
  11315. if (language == "cpp" || language == "java")
  11316. monaco.languages.registerInlayHintsProvider(language, {
  11317. provideInlayHints: (model, range, token) => {
  11318. return new Promise((resolve, reject) => {
  11319. const request = {
  11320. jsonrpc: "2.0",
  11321. id: id++,
  11322. method: "textDocument/inlayHint",
  11323. params: {
  11324. textDocument: {
  11325. uri: model.uri.toString(),
  11326. },
  11327. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11328. },
  11329. };
  11330.  
  11331. fetchData(request, (response) => {
  11332. const result = response.result;
  11333. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11334.  
  11335. if (!result) return resolve(null);
  11336.  
  11337. const inlayHints = {
  11338. hints: result.map((item) => {
  11339. return {
  11340. kind: item.kind,
  11341. label: item.label,
  11342. paddingLeft: item.paddingLeft,
  11343. paddingRight: item.paddingRight,
  11344. position: {
  11345. lineNumber: item.position.line + 1,
  11346. column: item.position.character + 1,
  11347. },
  11348. };
  11349. }),
  11350. };
  11351. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, inlayHints);
  11352.  
  11353. resolve(inlayHints);
  11354. });
  11355. });
  11356. },
  11357. });
  11358.  
  11359. // 注册"转到定义"
  11360. monaco.languages.registerDefinitionProvider(language, {
  11361. provideDefinition: (model, position) => {
  11362. const request = {
  11363. jsonrpc: "2.0",
  11364. id: id++,
  11365. method: "textDocument/definition",
  11366. params: {
  11367. textDocument: {
  11368. uri: model.uri.toString(),
  11369. },
  11370. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11371. },
  11372. };
  11373.  
  11374. return new Promise((resolve, reject) => {
  11375. fetchData(request, (response) => {
  11376. const result = response.result;
  11377. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11378.  
  11379. if (result.length == 0) return resolve(null);
  11380. const definition = result.map((item) => ({
  11381. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11382. uri: monaco.Uri.parse(item.uri), //
  11383. }));
  11384. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, definition);
  11385.  
  11386. resolve(definition);
  11387. });
  11388. });
  11389.  
  11390. return null; // 如果没有内容,则返回null
  11391. },
  11392. });
  11393.  
  11394. // 注册"转到引用"
  11395. monaco.languages.registerReferenceProvider(language, {
  11396. provideReferences: (model, position, context) => {
  11397. const request = {
  11398. jsonrpc: "2.0",
  11399. id: id++,
  11400. method: "textDocument/references",
  11401. params: {
  11402. context: context,
  11403. textDocument: {
  11404. uri: model.uri.toString(),
  11405. },
  11406. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11407. },
  11408. };
  11409.  
  11410. return new Promise((resolve, reject) => {
  11411. fetchData(request, (response) => {
  11412. const result = response.result;
  11413. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11414.  
  11415. if (result.length == 0) return resolve([]);
  11416.  
  11417. const references = result.map((item) => ({
  11418. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11419. uri: monaco.Uri.parse(item.uri), //
  11420. }));
  11421. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, references);
  11422. resolve(references);
  11423. });
  11424. });
  11425. return []; // 如果没有内容,则返回空数组
  11426. },
  11427. });
  11428.  
  11429. // 注册"符号引用点击高亮"
  11430. monaco.languages.registerDocumentHighlightProvider(language, {
  11431. provideDocumentHighlights: (model, position) => {
  11432. const request = {
  11433. jsonrpc: "2.0",
  11434. id: id++,
  11435. method: "textDocument/documentHighlight",
  11436. params: {
  11437. textDocument: {
  11438. uri: model.uri.toString(),
  11439. },
  11440. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11441. },
  11442. };
  11443.  
  11444. return new Promise((resolve, reject) => {
  11445. fetchData(request, (response) => {
  11446. const result = response.result;
  11447. pushLSPLogMessage("info", `documentHighlight ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11448.  
  11449. if (!result || result.length == 0) return resolve([]);
  11450. const highlights = result.map((item) => ({
  11451. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11452. kind: item.kind,
  11453. }));
  11454. pushLSPLogMessage("info",
  11455. `documentHighlight ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11456. highlights
  11457. );
  11458.  
  11459. resolve(highlights);
  11460. });
  11461. });
  11462. return []; // 如果没有内容,则返回空数组
  11463. },
  11464. });
  11465.  
  11466. // 注册"文件链接"
  11467. if (language == "cpp" || language == "java")
  11468. monaco.languages.registerLinkProvider(language, {
  11469. provideLinks: (model) => {
  11470. const request = {
  11471. jsonrpc: "2.0",
  11472. id: id++,
  11473. method: "textDocument/documentLink",
  11474. params: {
  11475. textDocument: {
  11476. uri: model.uri.toString(),
  11477. },
  11478. },
  11479. };
  11480.  
  11481. return new Promise((resolve, reject) => {
  11482. fetchData(request, (response) => {
  11483. const result = response.result;
  11484. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11485.  
  11486. if (!result) return resolve(null);
  11487. const links = {
  11488. links: result.map((item) => ({
  11489. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11490. url: item.target.toString(),
  11491. tooltip: item.tooltip ? item.tooltip : null,
  11492. })),
  11493. };
  11494. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, links);
  11495. resolve(links);
  11496. });
  11497. });
  11498. },
  11499. });
  11500.  
  11501. // 注册"格式化"
  11502. monaco.languages.registerDocumentFormattingEditProvider(language, {
  11503. provideDocumentFormattingEdits: (model, options, token) => {
  11504. const request = {
  11505. jsonrpc: "2.0",
  11506. id: id++,
  11507. method: "textDocument/formatting",
  11508. params: {
  11509. textDocument: {
  11510. uri: model.uri.toString(),
  11511. },
  11512. options: options,
  11513. },
  11514. };
  11515.  
  11516. return new Promise((resolve, reject) => {
  11517. fetchData(request, (response) => {
  11518. const result = response.result;
  11519. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11520.  
  11521. const TextEdit = result.map((edit) => ({
  11522. range: OJBetter_monaco.lspRangeToMonacoRange(edit.range),
  11523. text: edit.newText,
  11524. }));
  11525. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, TextEdit);
  11526. resolve(TextEdit);
  11527. });
  11528. });
  11529. },
  11530. });
  11531.  
  11532. // 注册"部分格式化"
  11533. monaco.languages.registerDocumentRangeFormattingEditProvider(language, {
  11534. provideDocumentRangeFormattingEdits: (model, range, options) => {
  11535. const request = {
  11536. jsonrpc: "2.0",
  11537. id: id++,
  11538. method: "textDocument/rangeFormatting",
  11539. params: {
  11540. textDocument: {
  11541. uri: model.uri.toString(),
  11542. },
  11543. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11544. options,
  11545. },
  11546. };
  11547.  
  11548. return new Promise((resolve, reject) => {
  11549. fetchData(request, (response) => {
  11550. const result = response.result;
  11551. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11552.  
  11553. if (!result || result.length == 0) return resolve([]);
  11554. const edits = result.map((item) => ({
  11555. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11556. text: item.newText,
  11557. }));
  11558. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11559. resolve(edits);
  11560. });
  11561. });
  11562. },
  11563. });
  11564.  
  11565. // 注册"重命名"
  11566. monaco.languages.registerRenameProvider(language, {
  11567. provideRenameEdits: (model, position, newName, token) => {
  11568. const request = {
  11569. jsonrpc: "2.0",
  11570. id: id++,
  11571. method: "textDocument/rename",
  11572. params: {
  11573. textDocument: {
  11574. uri: model.uri.toString(),
  11575. },
  11576. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11577. newName: newName,
  11578. },
  11579. };
  11580.  
  11581. return new Promise((resolve, reject) => {
  11582. fetchData(request, (response) => {
  11583. const result = response.result;
  11584. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11585.  
  11586. const rename = OJBetter_monaco.lspEditToMonacoEdit(result);
  11587. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, rename);
  11588. resolve(rename);
  11589. });
  11590. });
  11591. },
  11592. });
  11593.  
  11594. // 注册"折叠范围分析"
  11595. monaco.languages.registerFoldingRangeProvider(language, {
  11596. provideFoldingRanges: (model) => {
  11597. const request = {
  11598. jsonrpc: "2.0",
  11599. id: id++,
  11600. method: "textDocument/foldingRange",
  11601. params: {
  11602. textDocument: {
  11603. uri: model.uri.toString(),
  11604. },
  11605. },
  11606. };
  11607.  
  11608. return new Promise((resolve, reject) => {
  11609. fetchData(request, (response) => {
  11610. const result = response.result;
  11611. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11612.  
  11613. if (!result) return resolve([]);
  11614. const foldingRanges = result.map((item) => ({
  11615. start: item.startLine + 1,
  11616. end: item.endLine + 1,
  11617. kind: monaco.languages.FoldingRangeKind.fromValue(item.kind),
  11618. }));
  11619. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, foldingRanges);
  11620. resolve(foldingRanges);
  11621. });
  11622. });
  11623. },
  11624. });
  11625.  
  11626. // 注册"方法签名提示"
  11627. monaco.languages.registerSignatureHelpProvider(language, {
  11628. signatureHelpTriggerCharacters:
  11629. serverInfo.capabilities.signatureHelpProvider.triggerCharacters,
  11630. provideSignatureHelp: (model, position, token, context) => {
  11631. const request = {
  11632. jsonrpc: "2.0",
  11633. id: id++,
  11634. method: "textDocument/signatureHelp",
  11635. params: {
  11636. textDocument: {
  11637. uri: model.uri.toString(),
  11638. },
  11639. position: {
  11640. line: position.lineNumber - 1,
  11641. character: position.column - 1,
  11642. },
  11643. context: {
  11644. triggerKind: context.triggerKind,
  11645. triggerCharacter: context.triggerCharacter,
  11646. isRetrigger: context.isRetrigger,
  11647. activeSignatureHelp: context.activeSignatureHelp,
  11648. },
  11649. },
  11650. };
  11651.  
  11652. return new Promise((resolve, reject) => {
  11653. fetchData(request, (response) => {
  11654. const result = response.result;
  11655.  
  11656. pushLSPLogMessage("info", `signatureHelp ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11657.  
  11658. if (!result) return resolve(null);
  11659. const SignatureHelpResult = {
  11660. value: {
  11661. activeParameter: result.activeParameter,
  11662. activeSignature: result.activeSignature,
  11663. signatures: result.signatures,
  11664. },
  11665. dispose: () => { },
  11666. };
  11667.  
  11668. pushLSPLogMessage("info",
  11669. `signatureHelp ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11670. SignatureHelpResult
  11671. );
  11672. resolve(SignatureHelpResult);
  11673. });
  11674. });
  11675. },
  11676. });
  11677.  
  11678. // 注册"渐进式自动格式化" 如果server有这个
  11679. if (serverInfo.capabilities.documentOnTypeFormattingProvider)
  11680. monaco.languages.registerOnTypeFormattingEditProvider(language, {
  11681. autoFormatTriggerCharacters: [
  11682. serverInfo.capabilities.documentOnTypeFormattingProvider
  11683. .firstTriggerCharacter,
  11684. ],
  11685. provideOnTypeFormattingEdits: (model, position, ch, options) => {
  11686. const request = {
  11687. jsonrpc: "2.0",
  11688. id: id++,
  11689. method: "textDocument/onTypeFormatting",
  11690. params: {
  11691. textDocument: {
  11692. uri: model.uri.toString(),
  11693. },
  11694. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11695. ch,
  11696. options,
  11697. },
  11698. };
  11699.  
  11700. return new Promise((resolve, reject) => {
  11701. fetchData(request, (response) => {
  11702. const result = response.result;
  11703. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11704.  
  11705. if (!result || result.length == 0) return resolve([]);
  11706.  
  11707. const edits = result.map((item) => ({
  11708. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11709. text: item.newText,
  11710. }));
  11711. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11712. resolve(edits);
  11713. });
  11714. });
  11715. },
  11716. });
  11717. };
  11718.  
  11719. /**
  11720. * 被动式接收处理
  11721. */
  11722. OJBetter_monaco.PassiveReceiveHandler = () => {
  11723.  
  11724. // "实时代码诊断"
  11725. OJBetter_monaco.updateMarkers = function (message) {
  11726. const params = message.params;
  11727. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11728.  
  11729. if (!params) return;
  11730. const markers = params.diagnostics.map((item1) => ({
  11731. code: item1.code,
  11732. message: item1.message,
  11733. ...OJBetter_monaco.lspRangeToMonacoRange(item1.range),
  11734. relatedInformation: item1.relatedInformation
  11735. ? item1.relatedInformation.map((item2) => ({
  11736. ...(item2.location.range
  11737. ? OJBetter_monaco.lspRangeToMonacoRange(item2.location.range)
  11738. : OJBetter_monaco.lspRangeToMonacoRange(item2.location)),
  11739. message: item2.message,
  11740. resource: monaco.Uri.parse(item2.location.uri),
  11741. }))
  11742. : null,
  11743. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(item1.severity),
  11744. source: item1.source,
  11745. }));
  11746.  
  11747. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, markers);
  11748. monaco.editor.setModelMarkers(model, "eslint", markers);
  11749.  
  11750. // 更新状态底栏信息
  11751. const nowMarks = monaco.editor.getModelMarkers();
  11752. warningCount = 0;
  11753. errorCount = 0;
  11754. for (const marker of nowMarks) {
  11755. if (marker.severity === monaco.MarkerSeverity.Warning) {
  11756. warningCount++;
  11757. } else if (marker.severity === monaco.MarkerSeverity.Error) {
  11758. errorCount++;
  11759. }
  11760. }
  11761. $('#OJBetter_statusBar').text(`Warnings: ${warningCount}, Errors: ${errorCount}`);
  11762. };
  11763.  
  11764. // "应用服务器推送的更改"(代码修复)
  11765. OJBetter_monaco.applyEdit = function (message) {
  11766. const params = message.params;
  11767. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11768.  
  11769. if (!params) return;
  11770. const operations = Object.values(params.edit.changes)
  11771. .flat()
  11772. .map((item) => ({
  11773. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11774. text: item.newText,
  11775. }));
  11776.  
  11777. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, operations);
  11778. model.pushEditOperations([], operations, () => null); // 入栈编辑操作
  11779. };
  11780. }
  11781.  
  11782. if (!languageSocketState) await waitForLanguageSocketState();
  11783. OJBetter_monaco.Initialize();
  11784. }
  11785.  
  11786. // 语言更改
  11787. function changeMonacoLanguage(form) {
  11788. let nowSelect = form.selectLang.val();
  11789. // 记忆更改
  11790. GM_setValue('compilerSelection', nowSelect);
  11791. // 销毁旧的编辑器
  11792. try {
  11793. if (OJBetter.monaco.editor) OJBetter.monaco.editor.dispose();
  11794. } catch (error) {
  11795. console.warn("Encountered an error while attempting to delete the old editor, but it should not affect your regular usage.", error)
  11796. }
  11797. // 关闭旧的socket
  11798. OJBetter.monaco.lsp.socket.forEach(socket => {
  11799. socket.close();
  11800. });
  11801. // 移除相关元素
  11802. form.topRightDiv.empty();
  11803. $('#LSPLog').remove();
  11804. $('#OJBetter_statusBar').remove();
  11805. // 创建新的编辑器
  11806. if (nowSelect in value_monacoLanguageMap) {
  11807. let language = value_monacoLanguageMap[nowSelect];
  11808. if (language == "python" || language == "cpp") {
  11809. createMonacoEditor(language, form, true);
  11810. } else {
  11811. createMonacoEditor(language, form, false);
  11812. }
  11813. } else {
  11814. createMonacoEditor(null, form, false);
  11815. }
  11816. // 更新在线编译器参数
  11817. changeCompilerArgs(nowSelect);
  11818. }
  11819.  
  11820. // 收集样例数据
  11821. function getTestData() {
  11822. let testData = {};
  11823.  
  11824. /**
  11825. * 从pre中获取文本信息
  11826. * @param {JQuery<HTMLElement>} node 元素
  11827. * @returns {string} 文本信息
  11828. */
  11829. function getTextFromPre(node) {
  11830. let text;
  11831. if (node.find("br").length > 0) {
  11832. text = node.html().replace(/<br>/g, "\n"); // <br>作换行符的情况
  11833. } else {
  11834. text = node.text();
  11835. }
  11836. return text;
  11837. }
  11838.  
  11839. $('.input').each(function (index) {
  11840. var inputText = '';
  11841. if ($(this).find('pre').find('div').length > 0) {
  11842. $(this).find('pre').find('div').each(function () {
  11843. inputText += getTextFromPre($(this)) + '\n';
  11844. });
  11845. } else {
  11846. inputText = getTextFromPre($(this).find('pre'));
  11847. }
  11848. var outputText = '';
  11849. if ($('.output').eq(index).find('pre').find('div').length > 0) {
  11850. $('.output').eq(index).find('pre').find('div').each(function () {
  11851. inputText += getTextFromPre($(this)) + '\n';
  11852. });
  11853. } else {
  11854. outputText = getTextFromPre($('.output').eq(index).find('pre'));
  11855. }
  11856.  
  11857. testData[index + 1] = {
  11858. input: inputText.trim(),
  11859. output: outputText.trim()
  11860. };
  11861. });
  11862. return testData;
  11863. }
  11864.  
  11865. // 初始化自定义测试数据面板
  11866. function CustomTestInit() {
  11867. const url = window.location.href;
  11868.  
  11869. restoreText();
  11870.  
  11871. // 添加
  11872. $('#addCustomTest').click(function () {
  11873. var sampleDiv = $('<div class="sampleDiv">');
  11874. var inputTextarea = $('<p style="padding: 0px 5px;">input</p><textarea class="dynamicTextarea inputTextarea"></textarea>');
  11875. var outputTextarea = $('<p style="padding: 0px 5px;">output</p><textarea class="dynamicTextarea outputTextarea"></textarea>');
  11876. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  11877. sampleDiv.append(deleteCustomTest);
  11878. sampleDiv.append(inputTextarea);
  11879. sampleDiv.append(outputTextarea);
  11880. $('#customTests').append(sampleDiv);
  11881. });
  11882.  
  11883. // 实时保存文本内容到 IndexedDB 中
  11884. $(document).on('input', '.inputTextarea, .outputTextarea', function () {
  11885. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  11886. var objectStore = OJBetter.common.database.samplesData;
  11887. var samples = {
  11888. url: url,
  11889. samples: []
  11890. };
  11891. var index = 0;
  11892. $('.sampleDiv').each(function () {
  11893. var $sampleDiv = $(this);
  11894. var inputTextarea = $sampleDiv.find('.inputTextarea');
  11895. var outputTextarea = $sampleDiv.find('.outputTextarea');
  11896. $sampleDiv.attr('data-index', index);
  11897. inputTextarea.attr('id', 'input' + index);
  11898. outputTextarea.attr('id', 'output' + index);
  11899. var sample = {
  11900. id: index,
  11901. input: inputTextarea.val(),
  11902. output: outputTextarea.val()
  11903. };
  11904. samples.samples.push(sample);
  11905. index++;
  11906. });
  11907. objectStore.put(samples);
  11908. });
  11909. });
  11910.  
  11911. // 删除
  11912. $(document).on('click', '.deleteCustomTest', function () {
  11913. var $sampleDiv = $(this).closest('.sampleDiv');
  11914. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  11915. var objectStore = OJBetter.common.database.samplesData;
  11916. var index = parseInt($sampleDiv.attr('data-index'));
  11917. if (!isNaN(index)) {
  11918. objectStore.get(url).then(row => {
  11919. let samples = row.samples;
  11920. samples.splice(index, 1); // 移除第index个元素
  11921. objectStore.put({
  11922. url: url,
  11923. samples: samples
  11924. });
  11925. })
  11926. }
  11927. $sampleDiv.remove();
  11928. });
  11929. });
  11930.  
  11931. // 恢复保存的内容
  11932. function restoreText() {
  11933. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  11934. return OJBetter.common.database.samplesData.get(url);
  11935. }).then(function (data) {
  11936. if (data.samples && data.samples.length > 0) {
  11937. data.samples.forEach(function (item, index) {
  11938. var sampleDiv = $('<div class="sampleDiv">');
  11939. var inputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">input</p><textarea id="input${index}" class="dynamicTextarea inputTextarea"></textarea>`);
  11940. var outputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">output</p><textarea id="output${index}" class="dynamicTextarea outputTextarea"></textarea>`);
  11941. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  11942.  
  11943. inputTextarea.val(item.input);
  11944. outputTextarea.val(item.output);
  11945.  
  11946. sampleDiv.append(deleteCustomTest);
  11947. sampleDiv.append(inputTextarea);
  11948. sampleDiv.append(outputTextarea);
  11949. sampleDiv.attr('data-index', index)
  11950. $('#customTests').append(sampleDiv);
  11951. });
  11952. }
  11953. });
  11954. }
  11955. }
  11956.  
  11957. // 获取自定义测试数据
  11958. function getCustomTestData() {
  11959. const url = window.location.href;
  11960.  
  11961. return new Promise(function (resolve) {
  11962. var customTestData = {};
  11963. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  11964. return OJBetter.common.database.samplesData.get(url);
  11965. }).then(function (data) {
  11966. if (!data) resolve(customTestData);
  11967. if (data.samples && data.samples.length > 0) {
  11968. data.samples.forEach(function (item, index) {
  11969. customTestData[index + 1] = {
  11970. input: item.input,
  11971. output: item.output
  11972. };
  11973. });
  11974. }
  11975. resolve(customTestData);
  11976. });
  11977. });
  11978. }
  11979.  
  11980. // codeforces编译器参数列表
  11981. let officialLanguage = "";
  11982. function officialCompilerArgsChange(nowSelect) {
  11983. officialLanguage = nowSelect;
  11984. $('#CompilerArgsInput').prop("disabled", true);
  11985. }
  11986.  
  11987. // codeforces编译器通信
  11988. async function officialCompiler(code, input) {
  11989. const data = new FormData();
  11990. data.append('csrf_token', OJBetter.common.cf_csrf_token);
  11991. data.append('source', code);
  11992. data.append('tabSize', '4');
  11993. data.append('programTypeId', officialLanguage);
  11994. data.append('input', input);
  11995. data.append('output', '');
  11996. data.append('communityCode', '');
  11997. data.append('action', 'submitSourceCode');
  11998. data.append('programTypeId', officialLanguage);
  11999. data.append('sourceCode', code);
  12000.  
  12001. const requestOptions = {
  12002. method: 'POST',
  12003. url: `${OJBetter.common.hostAddress}/data/customtest`,
  12004. data: data,
  12005. headers: {
  12006. 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  12007. }
  12008. };
  12009.  
  12010. const result = {
  12011. Errors: '',
  12012. Result: '',
  12013. Stats: ''
  12014. };
  12015.  
  12016. try {
  12017. const submitResponse = await OJB_GMRequest(requestOptions);
  12018. if (submitResponse.status !== 200 || !submitResponse.response) {
  12019. result.Errors = `${i18next.t('compiler.official.pushError', { ns: 'codeEditor' })}`;
  12020. return result;
  12021. }
  12022.  
  12023. const submitResult = JSON.parse(submitResponse.response);
  12024. const customTestSubmitId = submitResult.customTestSubmitId;
  12025.  
  12026. const verdictResponse = await OJB_promiseRetryWrapper(
  12027. getOfficialCompilerVerdict,
  12028. {
  12029. maxRetries: 10,
  12030. retryInterval: 500
  12031. },
  12032. customTestSubmitId
  12033. );
  12034. return verdictResponse;
  12035. } catch (error) {
  12036. result.Errors = error.message;
  12037. return result;
  12038. }
  12039. }
  12040.  
  12041. // 获取codeforces编译器的执行结果
  12042. async function getOfficialCompilerVerdict(customTestSubmitId) {
  12043. const newdata = new FormData();
  12044. newdata.append('csrf_token', OJBetter.common.cf_csrf_token);
  12045. newdata.append('action', 'getVerdict');
  12046. newdata.append('customTestSubmitId', customTestSubmitId);
  12047.  
  12048. const requestOptions = {
  12049. method: 'POST',
  12050. url: `${OJBetter.common.hostAddress}/data/customtest`,
  12051. data: newdata,
  12052. headers: {
  12053. 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  12054. }
  12055. };
  12056.  
  12057. const responseDetails = await OJB_GMRequest(requestOptions);
  12058. if (responseDetails.status !== 200 || !responseDetails.response) {
  12059. throw new Error(`${i18next.t('compiler.official.getResultError', { ns: 'codeEditor' })}`);
  12060. }
  12061.  
  12062. const response = JSON.parse(responseDetails.response);
  12063. if (!response.stat) {
  12064. throw new Error('Verdict not ready, retrying...');
  12065. }
  12066.  
  12067. return {
  12068. Errors: response.verdict === "OK" ? null : response.verdict + '<br>' + response.output,
  12069. Result: response.output.replace(/\r\n/g, "\n"),
  12070. Stats: `Status: ${response.stat}`
  12071. };
  12072. }
  12073.  
  12074. // rextester编译器参数列表
  12075. let rextesterLanguage = "";
  12076. function rextesterCompilerArgsChange(nowSelect) {
  12077. let LanguageChoiceList = {
  12078. "4": "9", "6": "8", "7": "5", "9": "1", "13": "13", "19": "42", "20": "21", "28": "30", "31": "24", "32": "20",
  12079. "34": "17", "36": "4", "43": "6", "45": "7", "46": "4", "50": "7", "51": "9", "52": "27", "54": "7", "55": "23", "60": "4",
  12080. "61": "7", "65": "1", "67": "12", "70": "5", "73": "7", "74": "4", "75": "46", "77": "43", "79": "1", "80": "27", "83": "43", "87": "4"
  12081. }
  12082. let CompilerArgsList = {
  12083. "6": "-Wall -std=gnu99 -O2 -o a.out source_file.c",
  12084. "7": "-Wall -std=c++14 -O2 -o a.out source_file.cpp",
  12085. "20": "-o a.out source_file.go",
  12086. "27": "-Wall -std=c++14 -stdlib=libc++ -O2 -o a.out source_file.cpp",
  12087. "30": "source_file.d -ofa.out"
  12088. }
  12089. if (nowSelect in LanguageChoiceList) {
  12090. $('#RunTestButton').prop("disabled", false);
  12091. rextesterLanguage = LanguageChoiceList[nowSelect];
  12092. } else {
  12093. $('#RunTestButton').prop("disabled", true);
  12094. }
  12095. if (rextesterLanguage in CompilerArgsList) {
  12096. const rextesterCompilerArgs = CompilerArgsList[rextesterLanguage];
  12097. $('#CompilerArgsInput').val(rextesterCompilerArgs);
  12098. } else {
  12099. $('#CompilerArgsInput').val("");
  12100. }
  12101. }
  12102.  
  12103. // rextester编译器通信
  12104. async function rextesterCompiler(code, input) {
  12105. try {
  12106. const result = await OJB_promiseRetryWrapper(rextesterCompilerRequest, {
  12107. maxRetries: 5,
  12108. retryInterval: 500,
  12109. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  12110. }, code, input);
  12111. return result;
  12112. } catch (error) {
  12113. return { Errors: error.message, Result: '', Stats: '' };
  12114. }
  12115. }
  12116.  
  12117. // rextester编译器请求方法
  12118. async function rextesterCompilerRequest(code, input) {
  12119. const data = new FormData();
  12120. data.append('LanguageChoiceWrapper', rextesterLanguage);
  12121. data.append('EditorChoiceWrapper', '1');
  12122. data.append('LayoutChoiceWrapper', '1');
  12123. data.append('Program', code);
  12124. data.append('CompilerArgs', $('#CompilerArgsInput').val());
  12125. data.append('Input', input);
  12126. data.append('ShowWarnings', 'false');
  12127. data.append('IsInEditMode', 'false');
  12128. data.append('IsLive', 'false');
  12129.  
  12130. const responseDetails = await OJB_GMRequest({
  12131. method: 'POST',
  12132. url: 'https://rextester.com/rundotnet/Run',
  12133. data: data
  12134. });
  12135.  
  12136. if (responseDetails.status !== 200 || !responseDetails.response) {
  12137. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12138. }
  12139.  
  12140. const response = JSON.parse(responseDetails.response);
  12141. return {
  12142. Errors: response.Errors || '',
  12143. Result: response.Result || '',
  12144. Stats: response.Stats || ''
  12145. };
  12146. }
  12147.  
  12148. // wandbox编译器参数列表
  12149. var wandboxlist = JSON.parse(GM_getResourceText("wandboxlist"));
  12150. function wandboxCompilerArgsChange(nowSelect) {
  12151. let LanguageChoiceList = {
  12152. "6": "PHP", "7": "Python", "9": "C#", "12": "Haskell", "13": "Perl", "19": "OCaml",
  12153. "20": "Scala", "28": "D", "31": "Python", "32": "Go", "34": "JavaScript", "36": "Java", "40": "Python", "41": "Python",
  12154. "43": "C++", "50": "C++", "51": "Pascal", "52": "C++", "54": "C++", "60": "Java", "61": "C++", "65": "C#", "67": "Ruby",
  12155. "70": "Python", "73": "C++", "74": "Java", "75": "Rust", "79": "C#", "80": "C++", "87": "Java"
  12156. }
  12157.  
  12158. // 移除旧的
  12159. $('#CompilerChange').remove();
  12160.  
  12161. if (nowSelect in LanguageChoiceList) {
  12162. $('#RunTestButton').prop("disabled", false);
  12163. const Languagefiltered = wandboxlist.filter(obj => obj.language === LanguageChoiceList[nowSelect]);
  12164.  
  12165. // 创建编译器下拉框
  12166. let CompilerChange = $('<select id="CompilerChange" style="width: 100%;"></select>');
  12167.  
  12168. $('#CompilerSetting').show().append(CompilerChange);
  12169. for (let i = 0; i < Languagefiltered.length; i++) {
  12170. let Compiler = Languagefiltered[i];
  12171. let op = $("<option></option>")
  12172. .val(Compiler.name)
  12173. .text(Compiler["display-name"] + " " + Compiler.version);
  12174. $("#CompilerChange").append(op);
  12175. }
  12176.  
  12177. // 编译器参数刷新
  12178. function refreshCompilerArgs() {
  12179. var flags = '';
  12180. $("#CompilerBox").find("*").each(function () {
  12181. if ($(this).is("input[type='checkbox']")) {
  12182. let flag = $(this).prop("checked") ? $(this).val() : '';
  12183. flags += flag + (flag ? ' ' : '');
  12184. } else if ($(this).is("select") || $(this).is("input") || $(this).is("textarea")) {
  12185. let flag = $(this).val();
  12186. flags += flag + (flag ? ' ' : '');
  12187. }
  12188. });
  12189. $("#CompilerArgsInput").val(flags);
  12190. $("#CompilerArgsInput").prop("readonly", true); // 只读
  12191. }
  12192.  
  12193. // 编译器切换监听
  12194. CompilerChange.change(function () {
  12195. let selectedName = CompilerChange.val();
  12196. let Compiler = Languagefiltered.find(
  12197. (obj) => obj.name === selectedName
  12198. );
  12199.  
  12200. $("#CompilerArgsInput").val(); // 初始化编译器输入框
  12201.  
  12202. $("#CompilerBox").remove();
  12203. let div = $("<div id='CompilerBox'></div>");
  12204.  
  12205. let display_compile_command = OJB_safeCreateJQElement(`<input id='${Compiler.name}' value='${Compiler['display-compile-command']}' style="display:none;"}></input>`);
  12206. div.append(display_compile_command);
  12207.  
  12208. let switches = Compiler.switches;
  12209. for (let i = 0; i < switches.length; i++) {
  12210. let switche = switches[i];
  12211.  
  12212. if (switche.type == "single") {
  12213. let single = OJB_safeCreateJQElement(`
  12214. <div>
  12215. <input type='checkbox' id='${switche.name}' value='${switche['display-flags']}' ${switche.default ? 'checked' : ''}></input>
  12216. <label for='${switche.name}'>${switche['display-name']}</label>
  12217. </div>
  12218. `);
  12219. div.append(single);
  12220. single.find("input").change(function () {
  12221. refreshCompilerArgs();
  12222. });
  12223. } else if (switche.type == "select") {
  12224. let select = OJB_safeCreateJQElement(`<select id='${switche.name}'></select>`);
  12225. select.data('previousValue', switche.options[0]['display-flags']);
  12226. div.append(select);
  12227. for (let i = 0; i < switche.options.length; i++) {
  12228. let option = switche.options[i];
  12229. let op = $("<option></option>")
  12230. .val(option['display-flags'])
  12231. .text(option['display-name']);
  12232. select.append(op);
  12233. }
  12234. select.change(function () {
  12235. refreshCompilerArgs();
  12236. });
  12237. }
  12238. }
  12239.  
  12240. if (Compiler['compiler-option-raw'] == true) {
  12241. let textarea = OJB_safeCreateJQElement(`<textarea id="compiler_option_raw" placeholder="Raw compiler options" style="resize: vertical;"></textarea>`);
  12242. div.append(textarea);
  12243. textarea.on('input', function () {
  12244. refreshCompilerArgs();
  12245. });
  12246. }
  12247. if (Compiler['runtime-option-raw'] == true) {
  12248. let textarea = OJB_safeCreateJQElement(`<textarea id="runtime_option_raw" placeholder="Raw runtime options" style="resize: vertical;"></textarea>`);
  12249. div.append(textarea);
  12250. textarea.on('input', function () {
  12251. refreshCompilerArgs();
  12252. });
  12253. }
  12254. $("#CompilerSetting").append(div);
  12255.  
  12256. refreshCompilerArgs(); // 初始化
  12257. });
  12258.  
  12259. CompilerChange.trigger("change"); // 初始化
  12260. } else {
  12261. $('#RunTestButton').prop("disabled", true);
  12262. }
  12263. }
  12264.  
  12265. // wandbox编译器通信
  12266. async function wandboxCompiler(code, input) {
  12267. try {
  12268. const result = await OJB_promiseRetryWrapper(wandboxCompilerRequest, {
  12269. maxRetries: 5,
  12270. retryInterval: 500,
  12271. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  12272. }, code, input);
  12273. return result;
  12274. } catch (error) {
  12275. return { Errors: error.message, Result: '', Stats: '' };
  12276. }
  12277. }
  12278.  
  12279. // wandbox编译器请求方法
  12280. async function wandboxCompilerRequest(code, input) {
  12281. const data = {
  12282. code: code,
  12283. codes: [],
  12284. compiler: $('#CompilerChange').val().replace($('#compiler_option_raw').val(), '').replace($('#runtime_option_raw').val(), ''),
  12285. 'compiler-option-raw': $('#compiler_option_raw').val(),
  12286. 'runtime-option-raw': $('#runtime_option_raw').val(),
  12287. options: $("#CompilerArgsInput").val(),
  12288. description: '',
  12289. stdin: input,
  12290. title: ''
  12291. };
  12292.  
  12293. const responseDetails = await OJB_GMRequest({
  12294. method: 'POST',
  12295. url: 'https://wandbox.org/api/compile.json',
  12296. data: JSON.stringify(data),
  12297. headers: {
  12298. 'Content-Type': 'application/json'
  12299. }
  12300. });
  12301.  
  12302. if (responseDetails.status !== 200 || !responseDetails.response) {
  12303. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12304. }
  12305.  
  12306. const response = JSON.parse(responseDetails.response);
  12307. return {
  12308. Errors: response.compiler_error === "" ? response.signal : response.compiler_error,
  12309. Result: response.program_output || '',
  12310. Stats: response.status === "0" ? "Finish" : "Error"
  12311. };
  12312. }
  12313.  
  12314. // 更改编译器参数
  12315. function changeCompilerArgs(nowSelect) {
  12316. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12317. officialCompilerArgsChange(nowSelect);
  12318. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12319. rextesterCompilerArgsChange(nowSelect);
  12320. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12321. wandboxCompilerArgsChange(nowSelect);
  12322. }
  12323. }
  12324.  
  12325. // 在线编译器通信
  12326. async function onlineCompilerConnect(code, input) {
  12327. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12328. return await officialCompiler(code, input);
  12329. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12330. return await rextesterCompiler(code, input);
  12331. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12332. return await wandboxCompiler(code, input);
  12333. }
  12334. }
  12335.  
  12336. // 差异对比
  12337. function codeDiff(expectedText, actualText) {
  12338. // 将文本按行拆分
  12339. const expectedLines = expectedText ? expectedText.split('\n') : [];
  12340. const actualLines = actualText ? actualText.split('\n') : [];
  12341.  
  12342. const output = document.createElement('div');
  12343.  
  12344. const createLineElement = (lineNo, contentElement) => {
  12345. const lineDiv = document.createElement('div');
  12346. lineDiv.className = 'diffLine';
  12347.  
  12348. const lineNoDiv = document.createElement('div');
  12349. lineNoDiv.className = 'lineNo';
  12350. lineNoDiv.textContent = lineNo;
  12351.  
  12352. lineDiv.appendChild(lineNoDiv);
  12353. lineDiv.appendChild(contentElement);
  12354.  
  12355. return lineDiv;
  12356. };
  12357.  
  12358. const createContentElement = (isEquals = true, expected = null, removed = null) => {
  12359. const contentDiv = document.createElement('div');
  12360. contentDiv.className = 'lineContent';
  12361.  
  12362. if (isEquals) {
  12363. const span = document.createElement('span');
  12364. span.textContent = expected;
  12365. contentDiv.appendChild(span);
  12366. } else {
  12367. if (removed != null) {
  12368. const removedSpan = document.createElement('span');
  12369. removedSpan.className = 'removed';
  12370. removedSpan.textContent = removed;
  12371. contentDiv.appendChild(removedSpan);
  12372. }
  12373. if (expected != null) {
  12374. const addedSpan = document.createElement('span');
  12375. addedSpan.className = 'added';
  12376. addedSpan.textContent = expected;
  12377. contentDiv.appendChild(addedSpan);
  12378. }
  12379. }
  12380.  
  12381. return contentDiv;
  12382. };
  12383.  
  12384. let index = 1;
  12385.  
  12386. expectedLines.forEach((expectedLine, i) => {
  12387. const actualLine = actualLines[i];
  12388. if (actualLine === undefined) {
  12389. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine)));
  12390. } else if (expectedLine === actualLine) {
  12391. output.appendChild(createLineElement(index++, createContentElement(true, expectedLine)));
  12392. } else {
  12393. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine, actualLine)));
  12394. }
  12395. });
  12396.  
  12397. // 处理多余的 actualLines
  12398. for (let i = expectedLines.length; i < actualLines.length; i++) {
  12399. output.appendChild(createLineElement(index++, createContentElement(false, null, actualLines[i])));
  12400. }
  12401.  
  12402. return output.innerHTML;
  12403. }
  12404.  
  12405. // 内容类型常量
  12406. const TestCaseContentType = {
  12407. TERMINAL: 'terminal',
  12408. DIFF: 'diff',
  12409. NO_DIFF: 'no_diff',
  12410. SUCCESS: 'success'
  12411. };
  12412.  
  12413. // 样例测试状态类
  12414. class TestCaseStatus {
  12415. constructor(item, prefix) {
  12416. this.testCase = OJB_safeCreateJQElement(`<div class="test-case"></div>`);
  12417. this.item = item;
  12418. this.prefix = prefix;
  12419. this.titleElement = OJB_safeCreateJQElement(`<div class="test-case-title">${this.prefix} ${this.item}</div>`);
  12420. this.statusElement = OJB_safeCreateJQElement(`<div class="test-case-status"></div>`);
  12421. this.contentElement = OJB_safeCreateJQElement(`<div class="test-case-content"></div>`);
  12422. this.judgeElement = OJB_safeCreateJQElement(`<div class="test-case-judge"></div>`);
  12423. this.testCase.append(this.titleElement, this.statusElement, this.contentElement, this.judgeElement);
  12424. $('#statePanel').append(this.testCase);
  12425. this.setStatus('Queued', 'queued');
  12426. }
  12427.  
  12428. setTitle(title) {
  12429. this.titleElement.text(title);
  12430. }
  12431.  
  12432. setStatus(text, status) {
  12433. this.statusElement.text(text).removeClass('queued running success error').addClass(status);
  12434. }
  12435.  
  12436. setContent(content, type) {
  12437. // 如果内容类型为SUCCESS,隐藏内容元素并提前返回
  12438. if (type === TestCaseContentType.SUCCESS) {
  12439. this.contentElement.hide();
  12440. return;
  12441. }
  12442.  
  12443. // 根据内容类型创建内容元素
  12444. const createContentElementByType = (content, type) => {
  12445. let contentElement;
  12446. switch (type) {
  12447. case TestCaseContentType.TERMINAL:
  12448. // 为TERMINAL类型创建一个新的终端容器
  12449. contentElement = OJB_safeCreateJQElement(`<div class="terminal-container" style="overflow: auto;"></div>`);
  12450. break;
  12451. case TestCaseContentType.DIFF:
  12452. case TestCaseContentType.NO_DIFF:
  12453. // 为DIFF和NO_DIFF类型创建相应的内容元素,并添加差异说明
  12454. const className = type === TestCaseContentType.DIFF ? "output_diff" : "output_no_diff";
  12455. contentElement = OJB_safeCreateJQElement(`<pre class="${className}">${content}</pre>`);
  12456. appendDiffNote();
  12457. break;
  12458. default:
  12459. throw new Error("Unsupported content type.");
  12460. }
  12461. return contentElement;
  12462. };
  12463.  
  12464. // 初始化终端
  12465. const initializeTerminal = (content, contentElement) => {
  12466. const term = new Terminal({ rows: 10, cols: 150 });
  12467. term.setOption('theme', { background: '#2d2e2c' });
  12468. term.setOption('convertEol', true); // 将换行符\n转换为\r\n
  12469. term.write(content);
  12470. term.open(contentElement.get(0));
  12471. };
  12472.  
  12473. // 添加差异说明
  12474. const appendDiffNote = () => {
  12475. const diffNote = OJB_safeCreateJQElement(`<div class="diff_note">${i18next.t('resultBlock.diffNote', { ns: 'codeEditor' })}</div>`);
  12476. this.testCase.append(diffNote);
  12477. };
  12478.  
  12479. // 创建并追加内容元素
  12480. const contentElement = createContentElementByType(content, type);
  12481. this.contentElement.append(contentElement);
  12482.  
  12483. // 如果内容类型为TERMINAL,初始化并打开终端
  12484. if (type === TestCaseContentType.TERMINAL) {
  12485. initializeTerminal(content, contentElement);
  12486. }
  12487. }
  12488.  
  12489. setJudge(judge) {
  12490. this.judgeElement.text(judge);
  12491. }
  12492. }
  12493.  
  12494. // 样例测试函数
  12495. async function runCode(event, runButton, sourceDiv, submitDiv) {
  12496. event.preventDefault();
  12497. const statePanel = $('#statePanel').show().empty();
  12498. const testData = getTestData();
  12499. const customTestData = await getCustomTestData();
  12500. const totalTests = Object.keys(customTestData).length + Object.keys(testData).length;
  12501.  
  12502. let passedTests = 0;
  12503. let failedTests = 0;
  12504. let hasError = false;
  12505.  
  12506. // 定义一个对象队列,包括创建的样例块实例和对应的样例数据
  12507. const queue = [];
  12508.  
  12509. // 先生成各个样例的块,并显示排队中,将创建的各个对象存到队列中,以便后面更新
  12510. for (const [item, data] of Object.entries(customTestData)) {
  12511. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.custom', { ns: 'codeEditor' }));
  12512. queue.push({ testCase, data });
  12513. }
  12514.  
  12515. if (!$('#onlyCustomTest').prop('checked')) {
  12516. for (const [item, data] of Object.entries(testData)) {
  12517. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.sample', { ns: 'codeEditor' }));
  12518. queue.push({ testCase, data });
  12519. }
  12520. }
  12521.  
  12522. // 测试函数
  12523. const runTest = async (testCase, data, index) => {
  12524. runButton.setButtonState('running', `${index}/${totalTests}`);
  12525.  
  12526. testCase.setStatus('Running', 'running');
  12527. const result = await onlineCompilerConnect(sourceDiv.val(), data.input);
  12528.  
  12529. if (result.Errors) {
  12530. testCase.setStatus('Compilation error or Time limit', 'error');
  12531. testCase.setContent(result.Errors, TestCaseContentType.TERMINAL);
  12532. hasError = true;
  12533. } else if (result.Result.trim() === data.output.trim()) {
  12534. testCase.setStatus('Accepted', 'success');
  12535. testCase.setContent('The output is correct.', TestCaseContentType.SUCCESS);
  12536. passedTests++;
  12537. } else {
  12538. testCase.setStatus('Wrong Answer', 'error');
  12539. const diffContent = $('#DontShowDiff').prop('checked') ? result.Result.trim() : codeDiff(data.output.trim(), result.Result.trim());
  12540. const contentType = $('#DontShowDiff').prop('checked') ? TestCaseContentType.NO_DIFF : TestCaseContentType.DIFF;
  12541. testCase.setContent(diffContent, contentType);
  12542. failedTests++;
  12543. }
  12544.  
  12545. const judgeStats = `${i18next.t('resultBlock.state', { ns: 'codeEditor' })}${result.Stats}`;
  12546. testCase.setJudge(judgeStats);
  12547.  
  12548. await OJB_delay(500); // 等待500毫秒
  12549. };
  12550.  
  12551. // 对队列中的对象进行测试
  12552. for (let i = 0; i < queue.length; i++) {
  12553. const { testCase, data } = queue[i];
  12554. await runTest(testCase, data, i + 1);
  12555. }
  12556.  
  12557. // 测试完成后更新按钮状态
  12558. if (hasError) {
  12559. runButton.setButtonState('error', i18next.t('runTestButton.error', { ns: 'codeEditor' }));
  12560. } else if (failedTests > 0) {
  12561. runButton.setButtonState('error', `${passedTests}/${totalTests} ` + i18next.t('runTestButton.partial', { ns: 'codeEditor' }));
  12562. } else {
  12563. runButton.setButtonState('success', i18next.t('runTestButton.success', { ns: 'codeEditor' }));
  12564. }
  12565. }
  12566.  
  12567. /**
  12568. * 添加题目页代码编辑器
  12569. * @returns
  12570. */
  12571. async function addProblemPageCodeEditor() {
  12572. if (typeof ace === 'undefined') {
  12573. const loadingMessage = new LoadingMessage();
  12574. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('error.codeEditor.load', { ns: 'alert' })}`, 'error');
  12575. return; // 因为Codeforces设定的是未登录时不能访问提交页,也不会加载ace库
  12576. }
  12577.  
  12578. // 获取提交页链接
  12579. const href = window.location.href;
  12580. let submitUrl;
  12581. if (/\/problemset\//.test(href)) {
  12582. // problemset
  12583. submitUrl = OJBetter.common.hostAddress + '/problemset/submit';
  12584. } else if (/\/gym\//.test(href)) {
  12585. // gym 题目
  12586. submitUrl = OJBetter.common.hostAddress + '/gym/' + ((href) => {
  12587. const regex = /\/gym\/(?<num>[0-9a-zA-Z]*?)\/problem\//;
  12588. const match = href.match(regex);
  12589. return match && match.groups.num;
  12590. })(href) + '/submit';
  12591. } else if (OJBetter.typeOfPage.is_acmsguru) {
  12592. // acmsguru 题目
  12593. submitUrl = href.replace(/\/problemsets[A-Za-z0-9\/#]*/, "/problemsets/acmsguru/submit");
  12594. } else {
  12595. submitUrl = href.replace(/\/problem[A-Za-z0-9\/#]*/, "/submit");
  12596. }
  12597.  
  12598. // 获取提交页HTML
  12599. let cloneHTML = await getSubmitHTML(submitUrl);
  12600.  
  12601. // 创建
  12602. let form = await createCodeEditorForm(submitUrl, cloneHTML);
  12603. let selectLang = form.selectLang;
  12604. let submitButton = form.submitButton;
  12605. let runButton = form.runButton;
  12606.  
  12607. // 初始化
  12608. CustomTestInit(); // 自定义测试数据面板
  12609. selectLang.val(OJBetter.monaco.compilerSelection);
  12610.  
  12611. // 设置语言选择change事件监听器
  12612. selectLang.on('change', () => {
  12613. changeMonacoLanguage(form); // 编辑器语言切换监听
  12614. });
  12615. changeMonacoLanguage(form);
  12616.  
  12617. // 样例测试
  12618. runButton.on('click', (event) => runCode(event, runButton, form.sourceDiv, form.submitDiv))
  12619. .setHoverRedo();
  12620.  
  12621. // 提交
  12622. submitButton.on('click', async function (event) {
  12623. event.preventDefault();
  12624. if (OJBetter.monaco.setting.isCodeSubmitDoubleConfirm) {
  12625. const submit = await OJB_createDialog(
  12626. i18next.t('submitCode.title', { ns: 'dialog' }),
  12627. i18next.t('submitCode.content', { ns: 'dialog' }),
  12628. [
  12629. i18next.t('submitCode.buttons.0', { ns: 'dialog' }),
  12630. i18next.t('submitCode.buttons.1', { ns: 'dialog' })
  12631. ]
  12632. ); //提交确认
  12633. if (submit) {
  12634. submitButton.after(`<img class="OJBetter_loding" src="//codeforces.org/s/84141/images/ajax-loading-24x24.gif">`);
  12635. $('#OJBetter_SubmitForm').submit();
  12636. } else {
  12637. submitButton.addClass('disabled');
  12638. setTimeout(function () {
  12639. submitButton.removeClass('disabled');
  12640. }, 300);
  12641. }
  12642. } else {
  12643. $('#OJBetter_SubmitForm').submit();
  12644. }
  12645. });
  12646. }
  12647.  
  12648. /**
  12649. * 获取翻译服务目标语言的对应代码
  12650. * @param {string} serverName 服务名称
  12651. * @returns {string} 目标语言,如果没有对应代码则返回中文
  12652. */
  12653. function getTargetLanguage(serverName) {
  12654. let targetLanguage = OJBetter.supportList.translationSupport[serverName][OJBetter.translation.targetLang];
  12655. if (targetLanguage) return targetLanguage;
  12656. else return OJBetter.supportList.translationSupport[serverName]['zh'];
  12657. }
  12658.  
  12659. /**
  12660. * 将文本中Markdown格式的加粗**转换成HTML格式。
  12661. * @param {string} text 文本
  12662. * @returns {string} 替换后的字符串
  12663. */
  12664. function convertBoldMarkdownToHTML(text) {
  12665. return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
  12666. }
  12667.  
  12668. /**
  12669. * 将文本中Markdown格式的链接文本转换成HTML格式。
  12670. * @param {string} text 文本
  12671. * @returns {string} 替换后的字符串
  12672. */
  12673. function convertLinksMarkdownToHTML(text) {
  12674. return text.replace(/(?<!!)\[(.*?)\]\(([^"]*?)("(.*?)")*\)/g, '<a href="$2" title="$4">$1</a>');
  12675. }
  12676.  
  12677. /**
  12678. * 将HTML格式的加粗文本转换回Markdown格式。
  12679. * @param {string} text 文本
  12680. * @returns {string} 替换后的字符串
  12681. */
  12682. function convertBoldHTMLToMarkdown(text) {
  12683. return text.replace(/<strong>(.*?)<\/strong>/g, '**$1**');
  12684. }
  12685.  
  12686. /**
  12687. * 将HTML格式的链接文本转换回Markdown格式。
  12688. * @param {string} html - 包含HTML链接标签<a>的字符串。
  12689. * @returns {string} 转换后的字符串,其中HTML链接标签被替换为Markdown的链接语法。
  12690. */
  12691. function convertLinksHTMLToMarkdown(html) {
  12692. return html.replace(/<a href="([^"]*)"( title="([^"]*)")*>([^<]+)<\/a>/g, '[$4]($1 "$3")');
  12693. }
  12694.  
  12695. /**
  12696. * DeepL翻译
  12697. * @param {string} raw 原文
  12698. * @returns {Promise<TransRawData>} 翻译结果对象
  12699. */
  12700. async function translate_deepl(raw) {
  12701. const id = (Math.floor(Math.random() * 99999) + 100000) * 1000;
  12702. const data = {
  12703. jsonrpc: '2.0',
  12704. method: 'LMT_handle_texts',
  12705. id,
  12706. params: {
  12707. splitting: 'newlines',
  12708. lang: {
  12709. source_lang_user_selected: 'auto',
  12710. target_lang: getTargetLanguage('deepl'),
  12711. },
  12712. texts: [{
  12713. text: raw,
  12714. requestAlternatives: 3
  12715. }],
  12716. timestamp: getTimeStamp(raw.split('i').length - 1)
  12717. }
  12718. }
  12719. let postData = JSON.stringify(data);
  12720. if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
  12721. postData = postData.replace('"method":"', '"method" : "');
  12722. } else {
  12723. postData = postData.replace('"method":"', '"method": "');
  12724. }
  12725. const options = {
  12726. method: 'POST',
  12727. url: 'https://www2.deepl.com/jsonrpc',
  12728. data: postData,
  12729. headers: {
  12730. 'Content-Type': 'application/json',
  12731. 'Host': 'www2.deepl.com',
  12732. 'Origin': 'https://www.deepl.com',
  12733. 'Referer': 'https://www.deepl.com/',
  12734. },
  12735. anonymous: true,
  12736. nocache: true,
  12737. }
  12738.  
  12739. return await BaseTranslate(options, res => JSON.parse(res)?.result?.texts?.[0]?.text || res, res => {
  12740. const resObj = {
  12741. status: true,
  12742. message: 'ok'
  12743. };
  12744. if (res.includes('"message":"Too many requests"')) {
  12745. resObj.status = false;
  12746. resObj.message = i18next.t('error.deepl429', { ns: 'translator' }); // Too many requests 提示
  12747. return resObj;
  12748. };
  12749. return resObj;
  12750. });
  12751. }
  12752.  
  12753. /**
  12754. * 使用 DeepL Free API 进行翻译
  12755. * @param {string} raw 原文
  12756. * @returns {Promise<TransRawData>} 翻译结果对象
  12757. */
  12758. async function translate_deepl_api_free(raw) {
  12759. const data = JSON.stringify({
  12760. text: [raw],
  12761. target_lang: getTargetLanguage('deepl'),
  12762. split_sentences: '1',
  12763. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12764. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12765. });
  12766.  
  12767. const options = {
  12768. method: "POST",
  12769. url: OJBetter.deepl.config.proxy || "https://api-free.deepl.com/v2/translate",
  12770. headers: {
  12771. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12772. "Content-Type": "application/json",
  12773. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12774. },
  12775. data: data,
  12776. onload: response => response.responseText,
  12777. onerror: error => console.error(error)
  12778. };
  12779.  
  12780. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  12781. }
  12782.  
  12783. /**
  12784. * 使用 DeepL Pro API 进行翻译
  12785. * @param {string} raw 原文
  12786. * @returns {Promise<TransRawData>} 翻译结果对象
  12787. */
  12788. async function translate_deepl_api_pro(raw) {
  12789. const data = JSON.stringify({
  12790. text: [raw],
  12791. target_lang: getTargetLanguage('deepl'),
  12792. split_sentences: '1',
  12793. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12794. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12795. });
  12796.  
  12797. const options = {
  12798. method: "POST",
  12799. url: OJBetter.deepl.config.proxy || "https://api.deepl.com/v2/translate",
  12800. headers: {
  12801. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12802. "Content-Type": "application/json",
  12803. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12804. },
  12805. data: data,
  12806. onload: response => response.responseText,
  12807. onerror: error => console.error(error)
  12808. };
  12809.  
  12810. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  12811. }
  12812.  
  12813. /**
  12814. * 使用 DeepLX 进行翻译
  12815. * @param {String} text 原文
  12816. * @returns {Promise<TransRawData>} 翻译结果对象
  12817. */
  12818. async function translate_deeplx(text) {
  12819. const options = {
  12820. method: "POST",
  12821. url: OJBetter.deepl.config.proxy || 'https://api.deeplx.org/translate',
  12822. data: JSON.stringify({
  12823. "text": text,
  12824. "source_lang": "EN",
  12825. "target_lang": getTargetLanguage('deepl'),
  12826. }),
  12827. headers: {
  12828. 'Content-Type': 'application/json',
  12829. ...(OJBetter.deepl.config.key ? { Authorization: `Bearer ${OJBetter.deepl.config.key}` } : {})
  12830. },
  12831. responseType: "json",
  12832. };
  12833.  
  12834. return await BaseTranslate(options, res => {
  12835. const parsedResponse = JSON.parse(res);
  12836. if (parsedResponse.code === 200 && parsedResponse.data) {
  12837. return parsedResponse.data;
  12838. } else {
  12839. throw new Error('Translation failed or invalid response format.');
  12840. }
  12841. });
  12842. }
  12843.  
  12844. function getTimeStamp(iCount) {
  12845. const ts = Date.now();
  12846. if (iCount !== 0) {
  12847. iCount = iCount + 1;
  12848. return ts - (ts % iCount) + iCount;
  12849. } else {
  12850. return ts;
  12851. }
  12852. }
  12853.  
  12854. /**
  12855. * 讯飞听见翻译
  12856. * @param {String} text 要翻译的文本
  12857. * @returns {Promise<TransRawData>} 翻译结果对象
  12858. */
  12859. async function translate_iflyrec(text) {
  12860. const options = {
  12861. method: "POST",
  12862. url: 'https://www.iflyrec.com/TranslationService/v1/textTranslation',
  12863. data: JSON.stringify({
  12864. "from": "2",
  12865. "to": getTargetLanguage('iflyrec'),
  12866. "contents": [{
  12867. "text": text,
  12868. "frontBlankLine": 0
  12869. }]
  12870. }),
  12871. anonymous: true,
  12872. headers: {
  12873. 'Content-Type': 'application/json',
  12874. 'Origin': 'https://www.iflyrec.com',
  12875. },
  12876. responseType: "json",
  12877. };
  12878. return await BaseTranslate(options, res => JSON.parse(res).biz[0].translateResult.replace(/\\n/g, "\n\n"));
  12879. }
  12880.  
  12881. /**
  12882. * 有道翻译
  12883. * @param {string} raw 原文
  12884. * @returns {Promise<TransRawData>} 翻译结果对象
  12885. */
  12886. async function translate_youdao_mobile(raw) {
  12887. const options = {
  12888. method: "POST",
  12889. url: 'http://m.youdao.com/translate',
  12890. data: "inputtext=" + encodeURIComponent(raw) + "&type=" + getTargetLanguage('youdao'),
  12891. anonymous: true,
  12892. headers: {
  12893. "Content-Type": "application/x-www-form-urlencoded",
  12894. 'Host': 'm.youdao.com',
  12895. 'Origin': 'http://m.youdao.com',
  12896. 'Referer': 'http://m.youdao.com/translate',
  12897. }
  12898. }
  12899. return await BaseTranslate(options,
  12900. res => {
  12901. const array = /id="translateResult">\s*?<li>([\s\S]*?)<\/li>\s*?<\/ul/.exec(res);
  12902. if (array && array.length > 1) {
  12903. return array[1];
  12904. } else {
  12905. return res;
  12906. }
  12907. },
  12908. res => {
  12909. const resObj = {
  12910. status: true,
  12911. message: 'ok'
  12912. };
  12913. if (res.includes('<title>413 Request Entity Too Large</title>')) {
  12914. resObj.status = false;
  12915. resObj.message = i18next.t('error.youdao413', { ns: 'translator' }); // Request Entity Too Large 提示
  12916. return resObj;
  12917. };
  12918. return resObj;
  12919. })
  12920. }
  12921.  
  12922. /**
  12923. * google翻译
  12924. * @param {string} raw 原文
  12925. * @returns {Promise<TransRawData>} 翻译结果对象
  12926. */
  12927. async function translate_gg(raw) {
  12928. const params = `tl=${getTargetLanguage('google')}&q=${encodeURIComponent(raw)}`;
  12929. const options = {
  12930. method: "GET",
  12931. url: `https://translate.google.com/m?${params}`,
  12932. }
  12933. return await BaseTranslate(options,
  12934. res => $(res).filter('.result-container').text() || $(res).find('.result-container').text());
  12935. }
  12936.  
  12937. /**
  12938. * 彩云翻译
  12939. * @param {string} raw 原文
  12940. * @returns {Promise<TransRawData>} 翻译结果对象
  12941. */
  12942. async function translate_caiyun(raw) {
  12943. const source = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
  12944. const dic = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"].reduce((dic, current, index) => { dic[current] = source[index]; return dic }, {});
  12945. const browser_id = CryptoJS.MD5(Math.random().toString()).toString();
  12946. const caiyun_jwt = await (async () => {
  12947. const options = {
  12948. method: "POST",
  12949. url: 'https://api.interpreter.caiyunai.com/v1/user/jwt/generate',
  12950. headers: {
  12951. "content-type": "application/json",
  12952. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  12953. "origin": "https://fanyi.caiyunapp.com",
  12954. },
  12955. data: JSON.stringify({ browser_id }),
  12956. }
  12957. const res = await OJB_GMRequest(options);
  12958. return JSON.parse(res.responseText).jwt;
  12959. })();
  12960.  
  12961. // 解码
  12962. const decodeUnicode = str => {
  12963. const decoder = new TextDecoder();
  12964. const data = Uint8Array.from(atob(str), c => c.charCodeAt(0));
  12965. return decoder.decode(data);
  12966. };
  12967. const decoder = line => decodeUnicode([...line].map(i => dic[i] || i).join(""));
  12968.  
  12969. const options = {
  12970. method: "POST",
  12971. url: 'https://api.interpreter.caiyunai.com/v1/translator',
  12972. data: JSON.stringify({
  12973. "source": raw.split('\n'),
  12974. "browser_id": browser_id,
  12975. "trans_type": getTargetLanguage('caiyun'),
  12976. "request_id": "web_fanyi",
  12977. "media": "text",
  12978. "os_type": "web",
  12979. "dict": true,
  12980. "cached": true,
  12981. "replaced": true,
  12982. "style": "formal",
  12983. "model": "",
  12984. "detect": true,
  12985. }),
  12986. headers: {
  12987. "content-type": "application/json;charset=UTF-8",
  12988. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  12989. "t-authorization": caiyun_jwt
  12990. }
  12991. }
  12992. return await BaseTranslate(options, res => JSON.parse(res).target.map(decoder).join('\n'))
  12993. }
  12994.  
  12995. /**
  12996. * ChatGPT
  12997. * @param {string} raw 原文
  12998. * @returns {Promise<TransRawData>} 翻译结果对象
  12999. */
  13000. async function translate_openai(raw) {
  13001. const modelDefault = 'gpt-3.5-turbo';
  13002. const lang = getTargetLanguage('openai');
  13003. const prompt = `
  13004. As a professional English translator, your task is to accurately translate a segment of an algorithm programming competition question into ${lang}.
  13005. The translation should use professional terms and maintain the text format, including ${OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru
  13006. ? "keeping the LaTeX equations unchanged."
  13007. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13008. }
  13009. After translation, please ensure that the ${lang} version conforms to normal expression habits.
  13010. What I need is a carefully polished ${lang} translation of my question segment, The segment to be translated is as follows: "
  13011. ${raw}
  13012. "`;
  13013. const data = {
  13014. model: OJBetter.chatgpt.config.model || modelDefault,
  13015. messages: [{
  13016. role: "assistant",
  13017. content: prompt
  13018. }],
  13019. temperature: 0.7,
  13020. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13021. }
  13022. const options = {
  13023. method: "POST",
  13024. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13025. data: JSON.stringify(data),
  13026. responseType: 'json',
  13027. headers: {
  13028. 'Content-Type': 'application/json',
  13029. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13030. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13031. }
  13032. }
  13033. return await BaseTranslate(options,
  13034. res => res,
  13035. undefined,
  13036. response => response.response.choices[0].message.content);
  13037. }
  13038.  
  13039. /**
  13040. * ChatGPT 流式传输
  13041. * @param {string} raw 原文
  13042. * @param {TranslateDiv} translateDiv 翻译结果面板
  13043. * @returns {Promise<TransRawData>} 翻译结果对象
  13044. */
  13045. async function translate_openai_stream(raw, translateDiv) {
  13046. const result = {
  13047. done: true,
  13048. checkPassed: null,
  13049. response: null,
  13050. responseText: null,
  13051. text: "",
  13052. error: null,
  13053. message: null
  13054. };
  13055. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13056. try {
  13057. for await (const delta of openai_stream(raw)) {
  13058. result.text += delta;
  13059. // 翻译结果面板更新
  13060. translateDiv.updateTranslateDiv(result.text, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru), false);
  13061. }
  13062. return result;
  13063. } catch (err) {
  13064. console.warn(err);
  13065. result.error = {
  13066. message: err.message || null,
  13067. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13068. enumerable: err,
  13069. source: 'openai_stream'
  13070. };
  13071. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13072. }
  13073.  
  13074. return result;
  13075. }
  13076.  
  13077. /**
  13078. * 流式传输
  13079. * @param {string} raw 原文
  13080. * @returns {AsyncGenerator<string>} 返回 AsyncGenerator
  13081. */
  13082. async function* openai_stream(raw) {
  13083. const modelDefault = 'gpt-3.5-turbo';
  13084. const lang = getTargetLanguage('openai');
  13085. const prompt = `
  13086. I hope you can act as a professional English translator to help me translate a segment of an algorithm programming competition question into ${lang}.
  13087. During the translation process, I would like you to use more professional terms and maintain the text format, ${OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru
  13088. ? "keeping the LaTeX equations unchanged."
  13089. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13090. }
  13091. After completing the translation, please polish the ${lang} version to ensure it conforms to normal expression habits.
  13092. What I need is a carefully polished ${lang} translation of my question segment, which is as follows:
  13093. "
  13094. ${raw}
  13095. "`;
  13096. const data = {
  13097. model: OJBetter.chatgpt.config.model || modelDefault,
  13098. messages: [{
  13099. role: "assistant",
  13100. content: prompt
  13101. }],
  13102. temperature: 0.7,
  13103. stream: true,
  13104. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13105. }
  13106. const options = {
  13107. method: "POST",
  13108. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13109. data: JSON.stringify(data),
  13110. responseType: 'stream',
  13111. headers: {
  13112. 'Content-Type': 'application/json',
  13113. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13114. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13115. }
  13116. }
  13117. const response = await OJB_GMRequest(options, true);
  13118. const reader = response.response.getReader();
  13119. const decoder = new TextDecoder();
  13120. let buffer = ''; // 用于累积数据片段的缓冲区
  13121.  
  13122. while (true) {
  13123. const { done, value } = await reader.read();
  13124. if (done) break;
  13125. buffer += decoder.decode(value, { stream: true }); // 将新的数据片段追加到缓冲区
  13126. let lines = buffer.split("\n\n"); // 处理累积的数据
  13127.  
  13128. // 缓冲区的最后一行可能还未完整接收,保留在缓冲区中,-1
  13129. for (let i = 0; i < lines.length - 1; i++) {
  13130. let line = lines[i];
  13131. line = line.substring(5); // 移除 'data:' 前缀
  13132. if (line.includes('[DONE]')) {
  13133. return; // End
  13134. }
  13135. try {
  13136. let data = JSON.parse(line);
  13137. let delta = data['choices'][0]['delta'];
  13138. let content = delta['content'] ? delta['content'] : "";
  13139. yield content; // 传递数据给调用者
  13140. } catch (error) {
  13141. console.warn(`Error parsing JSON: ${error}\n\nError data: ${line}`);
  13142. }
  13143. }
  13144.  
  13145. // 保留最后一行在缓冲区中
  13146. buffer = lines.slice(-1);
  13147. }
  13148.  
  13149. return buffer;
  13150. }
  13151.  
  13152. /**
  13153. * @typedef {Object} CheckResponseResult
  13154. * @property {boolean} status 检查是否通过
  13155. * @property {string} message 检查失败时的消息
  13156. */
  13157.  
  13158. /**
  13159. * @typedef {Object} ErrorResponse
  13160. * @property {Object} message 错误消息
  13161. * @property {Object} stack 错误堆栈
  13162. * @property {Object} enumerable 可枚举的错误属性
  13163. * @property {string} source 错误来源
  13164. */
  13165.  
  13166. /**
  13167. * @typedef {Object} TransRawData
  13168. * @property {boolean} done 操作是否完成
  13169. * @property {CheckResponseResult|null} checkPassed 检查是否通过的结果
  13170. * @property {Object|null} response 响应对象
  13171. * @property {string|null} text 处理后的文本
  13172. * @property {ErrorResponse} error 错误列表
  13173. * @property {string|null} message 可能的消息
  13174. */
  13175.  
  13176. /**
  13177. * 通用翻译函数
  13178. * @param {Object} options GM_xmlhttpRequest 的参数
  13179. * @param {Function} processer 响应再处理函数,它接收响应文本,并应返回处理后的文本。
  13180. * @param {Function} checkResponse 检查文本是否符合预期的函数,它接收文本,并返回一个Object,包含状态和信息。默认为返回 { status: true, message: 'ok' }
  13181. * @param {Function} getResponseText 重写响应文本获取函数,它接收response,并返回响应文本。 默认为 response.responseText
  13182. * @returns {Promise<TransRawData>} 返回 Promise,其解析值为翻译结果对象
  13183. */
  13184. async function BaseTranslate(options, processer, checkResponse = () => { return { status: true, message: 'ok' } }, getResponseText = (response) => response.responseText) {
  13185. const result = {
  13186. done: false,
  13187. checkPassed: null,
  13188. response: null,
  13189. responseText: null,
  13190. text: "",
  13191. error: null,
  13192. message: null
  13193. };
  13194. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13195. const toDo = async () => {
  13196. try {
  13197. result.response = await OJB_GMRequest(options);
  13198. result.responseText = result.response.responseText;
  13199. result.text = getResponseText(result.response);
  13200. } catch (err) {
  13201. console.warn(err);
  13202. result.error = {
  13203. message: err.message || null,
  13204. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13205. enumerable: err,
  13206. source: 'GMRequest'
  13207. };
  13208. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13209. throw result;
  13210. }
  13211. try {
  13212. result.text = processer(result.text);
  13213. } catch (err) {
  13214. console.warn(err);
  13215. result.error = {
  13216. message: err.message || null,
  13217. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13218. enumerable: err,
  13219. source: 'processer'
  13220. };
  13221. result.message = `${i18next.t('error.processer', { ns: 'translator' })}${helpText}`;
  13222. throw result;
  13223. }
  13224. try {
  13225. result.checkPassed = checkResponse(result.text);
  13226. if (result.checkPassed.status) result.done = true;
  13227. else result.message = result.checkPassed.message;
  13228. return result;
  13229. } catch (err) {
  13230. console.warn(err);
  13231. result.error = {
  13232. message: err.message || null,
  13233. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13234. enumerable: err,
  13235. source: 'checkResponse'
  13236. };
  13237. result.message = `${i18next.t('error.checkResponse', { ns: 'translator' })}${helpText}`;
  13238. throw result;
  13239. }
  13240. };
  13241.  
  13242. return await OJB_promiseRetryWrapper(toDo, {
  13243. maxRetries: 3,
  13244. errorHandler: (err, maxRetries, attemptsLeft) => {
  13245. const detailedError = {
  13246. maxRetries: maxRetries,
  13247. attemptsLeft: attemptsLeft,
  13248. ...err
  13249. };
  13250. return detailedError;
  13251. }
  13252. });
  13253. }
  13254.  
  13255. /**
  13256. * 查询服务余额
  13257. * @param {Object} quotaConfig - 配额配置对象
  13258. * @returns {Promise} 返回包含余额信息的 Promise
  13259. */
  13260. async function queryServerBalance(quotaConfig) {
  13261. // 确保传入了有效的配置对象
  13262. if (!quotaConfig || !quotaConfig.url) {
  13263. return Promise.reject(new Error('Quota configuration is missing.'));
  13264. }
  13265.  
  13266. // 准备请求选项
  13267. const requestOptions = {
  13268. method: quotaConfig.method || 'GET',
  13269. url: quotaConfig.url,
  13270. headers: {
  13271. ...Object.assign({}, ...quotaConfig.header)
  13272. },
  13273. data: JSON.stringify({ ...Object.assign({}, ...quotaConfig.data) })
  13274. };
  13275.  
  13276. // 发送请求并返回 Promise
  13277. return OJB_GMRequest(requestOptions).then(response => {
  13278. try {
  13279. const responseData = JSON.parse(response.responseText);
  13280. // 从响应数据中提取余额
  13281. const surplusPath = quotaConfig.surplus;
  13282. const surplusValue = OJB_evaluatePathOrExpression(responseData, surplusPath);
  13283. return surplusValue;
  13284. } catch (error) {
  13285. return Promise.reject(new Error('Failed to parse balance response.'));
  13286. }
  13287. }).catch(error => {
  13288. console.warn('Error querying balance:', error);
  13289. return Promise.reject(error);
  13290. });
  13291. }
  13292.  
  13293. /**
  13294. * 确认 jQuery 已加载
  13295. * @param {number} retryDelay 重试延迟(毫秒)
  13296. * @returns {Promise<void>}
  13297. */
  13298. async function ensureJQueryIsLoaded(retryDelay = 50) {
  13299. while (typeof jQuery === 'undefined') {
  13300. console.warn(`JQuery is not loaded. Retry after ${retryDelay} ms.`);
  13301. await OJB_delay(retryDelay);
  13302. retryDelay = Math.min(retryDelay * 2, 2000);
  13303. }
  13304. }
  13305.  
  13306. /**
  13307. * 加载必须的函数
  13308. * @returns {Promise} 加载提示信息
  13309. */
  13310. async function loadRequiredFunctions() {
  13311. await initVar();// 初始化全局变量
  13312. return Promise.allSettled([
  13313. initDB(), // 连接数据库
  13314. initI18next(), // i18next初始化
  13315. initButtonFunc(), // 加载按钮相关函数
  13316. initHTML2MarkDown(), // 初始化html2markdown转换器
  13317. checkScriptVersion(), // 更新检查
  13318. ...(OJBetter.typeOfPage.is_acmsguru ? [acmsguruReblock()] : []) // 为acmsguru题面重新划分div
  13319. ]);
  13320. }
  13321.  
  13322. /**
  13323. * DOM加载后即可执行
  13324. */
  13325. function initOnDOMReady() {
  13326. showAnnounce(); // 显示公告
  13327. showWarnMessage(); // 显示警告消息
  13328. initSettingsPanel(); // 加载设置按钮面板
  13329. initMonacoEditor(); // 初始化monaco编辑器资源
  13330. localizeWebsite(); // 网站本地化替换
  13331. addDependencyStyles(); // 添加一些依赖库的样式
  13332. addI18nStyles(); // 添加包含i18n内容的样式
  13333. if (OJBetter.basic.expandFoldingblocks) ExpandFoldingblocks(); // 折叠块展开
  13334. if (OJBetter.basic.renderPerfOpt) RenderPerfOpt(); // 折叠块渲染优化
  13335. if (OJBetter.basic.selectElementPerfOpt) SelectElementPerfOpt(); // 下拉选择框性能优化
  13336. if (OJBetter.typeOfPage.is_problem) {
  13337. const problemPageLinkbar = new ProblemPageLinkbar(); // 创建题目页相关链接栏
  13338. if (OJBetter.basic.showCF2vjudge) CF2vjudge(problemPageLinkbar); // 跳转到Vjudge按钮
  13339. if (OJBetter.basic.showJumpToLuogu) CF2luogu(problemPageLinkbar); // 跳转到洛谷按钮
  13340. if (OJBetter.clist.enabled.problem) showRatingByClist_problem(problemPageLinkbar); // problem页显示Rating
  13341. }
  13342. if (OJBetter.typeOfPage.is_contest) {
  13343. if (OJBetter.clist.enabled.contest) showRatingByClist_contest(); // contest页显示Rating
  13344. }
  13345. if (OJBetter.typeOfPage.is_problemset) {
  13346. if (OJBetter.clist.enabled.problemset) showRatingByClist_problemset(); // problemset页显示Rating
  13347. }
  13348. if (OJBetter.typeOfPage.is_problem && OJBetter.monaco.enableOnProblemPage) {
  13349. addProblemPageCodeEditor(); // 添加题目页代码编辑器
  13350. }
  13351. }
  13352.  
  13353. /**
  13354. * 需要在页面资源完全加载后执行的函数
  13355. */
  13356. function onResourcesReady(loadingMessage) {
  13357. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadFunc', { ns: 'alert' })}`);
  13358. initializeInParallel(loadingMessage);
  13359. initializeSequentially(loadingMessage);
  13360. }
  13361.  
  13362. /**
  13363. * 可以异步并行的函数
  13364. */
  13365. function initializeInParallel(loadingMessage) {
  13366. if (OJBetter.basic.darkMode == "dark") darkModeStyleAdjustment(); // 黑暗模式额外的处理事件
  13367. if (OJBetter.basic.commentPaging) CommentPagination(); // 评论区分页
  13368. if (OJBetter.translation.comment.transMode == "2") multiChoiceTranslation(); // 选段翻译支持
  13369. if (OJBetter.monaco.beautifyPreBlocks) beautifyPreBlocksWithMonaco(); // 美化Pre代码块
  13370. }
  13371.  
  13372. /**
  13373. * 必须按序执行的函数
  13374. */
  13375. async function initializeSequentially(loadingMessage) {
  13376. await addConversionButton(); // 添加MD/复制/翻译按钮
  13377. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  13378. await initTransResultsRecover(); // 翻译结果恢复功能初始化
  13379. }
  13380. if (OJBetter.translation.auto.enabled) {
  13381. await initTransWhenViewable(); // 自动翻译
  13382. }
  13383. if (OJBetter.basic.standingsRecolor && OJBetter.typeOfPage.is_cfStandings) {
  13384. await recolorStandings(); // cf赛制榜单重新着色
  13385. }
  13386. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadSuccess', { ns: 'alert' })}`, 'success', 3000);
  13387. }
  13388.  
  13389. /**
  13390. * 主方法
  13391. */
  13392. async function main() {
  13393. await ensureJQueryIsLoaded(); // 等待jQuery加载
  13394. const loadingMessage = new LoadingMessage();
  13395. await loadRequiredFunctions(); // 加载必须的函数
  13396. initOnDOMReady(); // DOM加载后即可执行的函数
  13397. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('onload', { ns: 'alert' })}`);
  13398.  
  13399. // 检查页面资源是否已经完全加载
  13400. if (OJBetter.state.notWaiteLoaded) {
  13401. onResourcesReady(loadingMessage);
  13402. } else {
  13403. if (document.readyState === 'complete') {
  13404. onResourcesReady(loadingMessage);
  13405. } else {
  13406. window.addEventListener('load', () => onResourcesReady(loadingMessage));
  13407. }
  13408. }
  13409. };
  13410.  
  13411. // ------------------------------
  13412. // 脚本加载入口
  13413. if (document.readyState === 'loading') {
  13414. document.addEventListener("DOMContentLoaded", main);
  13415. } else {
  13416. main(); // 如果DOMContentLoaded已经触发,立即执行
  13417. }
  13418. // ------------------------------
  13419.  
  13420. // ------------------------------
  13421. // 配置自动迁移代码(将在10个小版本后移除-1.83)
  13422. // ------------------------------
  13423.  
  13424. {
  13425. let bottomZh_CN = GM_getValue("bottomZh_CN");
  13426. if (bottomZh_CN !== undefined) {
  13427. if (bottomZh_CN == true) {
  13428. GM_setValue("localizationLanguage", "zh");
  13429. } else {
  13430. GM_setValue("localizationLanguage", "initial");
  13431. }
  13432. GM_deleteValue("bottomZh_CN");
  13433. location.reload();
  13434. }
  13435. }
  13436. {
  13437. let config = GM_getValue("chatgpt-config");
  13438. if (config && config !== undefined) {
  13439. let index = parseInt(config.choice, 10);
  13440. if (index == -1) config.choice = "";
  13441. else config.choice = config.configurations[index].note;
  13442. config.configurations.forEach(function (item) {
  13443. item.name = item.note;
  13444. delete item.note;
  13445. });
  13446. GM_deleteValue("chatgpt-config");
  13447. GM_setValue("chatgpt_config", config);
  13448. location.reload();
  13449. }
  13450. }
  13451. {
  13452. let config = GM_getValue("Complet_config");
  13453. if (config && config.changed === undefined) {
  13454. config.changed = true; // 设置一个迁移标志
  13455. config.configurations.forEach(function (item) {
  13456. if (item.note !== undefined) {
  13457. item.name = item.note;
  13458. delete item.note;
  13459. }
  13460. });
  13461. GM_setValue("Complet_config", config);
  13462. location.reload();
  13463. }
  13464. }