Codeforces Better!

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

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

  1. // ==UserScript==
  2. // @name Codeforces Better!
  3. // @namespace https://greasyfork.org/users/747162
  4. // @version 1.74.1
  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. html[data-theme=dark] pre{
  1557. background-color: var(--ojb-color-bg-secondary) !important;
  1558. }
  1559. /* 实线边框颜色-圆角 */
  1560. html[data-theme=dark] .roundbox, html[data-theme=dark] .roundbox .rtable td,
  1561. html[data-theme=dark] .sidebar-menu ul li,
  1562. html[data-theme=dark] input, html[data-theme=dark] .ttypography .tt, html[data-theme=dark] #items-per-page,
  1563. html[data-theme=dark] .datatable td, html[data-theme=dark] .datatable th,
  1564. html[data-theme=dark] textarea, html[data-theme=dark] .input-output-copier{
  1565. border: var(--ojb-border-solid-primary) !important;
  1566. border-radius: 2px;
  1567. }
  1568. /* 实线边框颜色-无圆角 */
  1569. html[data-theme=dark] .problem-statement .sample-tests .input,
  1570. html[data-theme=dark] .problem-statement .sample-tests .output, html[data-theme=dark] .pagination span.active,
  1571. html[data-theme=dark] .test-for-popup pre{
  1572. border: var(--ojb-border-solid-primary) !important;
  1573. }
  1574. html[data-theme=dark] .roundbox .titled, html[data-theme=dark] .roundbox .rtable th {
  1575. border-bottom: var(--ojb-border-solid-primary) !important;
  1576. }
  1577. html[data-theme=dark] .roundbox .bottom-links, html[data-theme=dark] #footer{
  1578. border-top: var(--ojb-border-solid-primary) !important;
  1579. }
  1580. html[data-theme=dark] .topic .content {
  1581. border-left: 4px solid var(--ojb-color-border-primary) !important;
  1582. }
  1583. html[data-theme=dark] hr {
  1584. border-color: var(--ojb-color-border-primary) !important;
  1585. }
  1586. /* 虚线边框 */
  1587. html[data-theme=dark] .comment-table{
  1588. border: var(--ojb-border-dashed);
  1589. }
  1590. /* input-output-copier特殊处理 */
  1591. html[data-theme=dark] .html2md-panel.input-output-copier,
  1592. html[data-theme=dark] .translateDiv.input-output-copier,
  1593. html[data-theme=dark] #OJBetter_SubmitForm.input-output-copier{
  1594. border: none !important;
  1595. }
  1596. html[data-theme=dark] .html2md-panel.input-output-copier:hover,
  1597. html[data-theme=dark] #OJBetter_SubmitForm.input-output-copier:hover{
  1598. background-color: #ffffff00 !important;
  1599. }
  1600. /* focus-visible */
  1601. html[data-theme=dark] input:focus-visible, html[data-theme=dark] textarea, html[data-theme=dark] select{
  1602. border-width: 1.5px !important;
  1603. outline: none;
  1604. }
  1605. /* 图片-亮度 */
  1606. html[data-theme=dark] img, html[data-theme=dark] #facebox .popup a{
  1607. opacity: .75;
  1608. }
  1609. /* 反转 */
  1610. html[data-theme=dark] .SumoSelect>.CaptionCont>label>i, html[data-theme=dark] .delete-resource-link,
  1611. html[data-theme=dark] #program-source-text, html[data-theme=dark] .spoiler-content pre,
  1612. html[data-theme=dark] .popup .content pre code{
  1613. filter: invert(1) hue-rotate(.5turn);
  1614. }
  1615. /* 阴影 */
  1616. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .translate-problem-statement{
  1617. box-shadow: var(--ojb-shadow-standard);
  1618. }
  1619. /* 图标按钮状态样式 */
  1620. html[data-theme=dark] .ojb_btn_popover.success i:before, html[data-theme=dark] .ojb_btn_popover.success i {
  1621. color: var(--ojb-color-text-icon-success);
  1622. }
  1623. html[data-theme=dark] .ojb_btn_popover i:before {
  1624. text-shadow: var(--ojb-text-shadow-icon);
  1625. }
  1626. /* 评测状态文字颜色 */
  1627. html[data-theme=dark] .verdict-accepted, html[data-theme=dark] .verdict-accepted-challenged,
  1628. html[data-theme=dark] .verdict-successful-challenge{
  1629. color: #0a0 !important;
  1630. }
  1631. html[data-theme=dark] .verdict-failed, html[data-theme=dark] .verdict-challenged{
  1632. color: red !important;
  1633. }
  1634. html[data-theme=dark] .verdict-rejected, html[data-theme=dark] .verdict-unsuccessful-challenge{
  1635. color: #673ab7 !important;
  1636. }
  1637. html[data-theme=dark] .verdict-waiting {
  1638. color: gray !important;
  1639. }
  1640. /* 样例hover样式 */
  1641. html[data-theme=dark] .problem-statement .darkhighlight {
  1642. background-color: #455a64 !important;
  1643. }
  1644. /* 其他样式 */
  1645. html[data-theme=dark] .rated-user{
  1646. display: initial;
  1647. }
  1648. html[data-theme=dark] .datatable .ilt, html[data-theme=dark] .datatable .irt,
  1649. html[data-theme=dark] .datatable .ilb, html[data-theme=dark] .datatable .irb,
  1650. html[data-theme=dark] .datatable .lt, html[data-theme=dark] .datatable .rt,
  1651. html[data-theme=dark] .datatable .lb, html[data-theme=dark] .datatable .rb{
  1652. background: none;
  1653. }
  1654. html[data-theme=dark] .problems .accepted-problem td.id{
  1655. border-left: 6px solid #47837d !important;
  1656. }
  1657. html[data-theme=dark] .problems .rejected-problem td.id{
  1658. border-left: 6px solid #ef9a9a !important;
  1659. }
  1660. html[data-theme=dark] .problems .accepted-problem td.act {
  1661. background-color: #47837d !important;
  1662. border-radius: 0px;
  1663. }
  1664. html[data-theme=dark] .problems .rejected-problem td.act{
  1665. background-color: #ef9a9a !important;
  1666. border-radius: 0px;
  1667. }
  1668. html[data-theme=dark] .collapsible-topic.collapsed .content .collapsible-topic-options:before{
  1669. background-image: linear-gradient(#22272e00, #22272e);
  1670. }
  1671. `);
  1672. })()
  1673.  
  1674. /**
  1675. * 黑暗模式额外的处理事件
  1676. */
  1677. function darkModeStyleAdjustment() {
  1678. $(".test-example-line").off("mouseenter mouseleave"); // 移除上面原本的事件
  1679. // 为奇数行添加悬停效果
  1680. $('.test-example-line-odd').hover(
  1681. function () {
  1682. $(this).addClass('darkhighlight');
  1683. $(this).prevUntil(':not(.test-example-line-odd)').addClass('darkhighlight');
  1684. $(this).nextUntil(':not(.test-example-line-odd)').addClass('darkhighlight');
  1685. },
  1686. function () {
  1687. $(this).removeClass('darkhighlight');
  1688. $(this).prevUntil(':not(.test-example-line-odd)').removeClass('darkhighlight');
  1689. $(this).nextUntil(':not(.test-example-line-odd)').removeClass('darkhighlight');
  1690. }
  1691. );
  1692.  
  1693. // 为偶数行添加悬停效果
  1694. $('.test-example-line-even').hover(
  1695. function () {
  1696. $(this).addClass('darkhighlight');
  1697. $(this).prevUntil(':not(.test-example-line-even)').addClass('darkhighlight');
  1698. $(this).nextUntil(':not(.test-example-line-even)').addClass('darkhighlight');
  1699. },
  1700. function () {
  1701. $(this).removeClass('darkhighlight');
  1702. $(this).prevUntil(':not(.test-example-line-even)').removeClass('darkhighlight');
  1703. $(this).nextUntil(':not(.test-example-line-even)').removeClass('darkhighlight');
  1704. }
  1705. );
  1706. }
  1707.  
  1708. /**
  1709. * 初始化monaco编辑器资源
  1710. */
  1711. async function initMonacoEditor() {
  1712. if (OJBetter.monaco.enableOnProblemPage || OJBetter.monaco.beautifyPreBlocks) {
  1713. try {
  1714. // 等待Monaco Editor加载器脚本加载完成
  1715. await OJB_LoadJS("https://cdn.staticfile.net/monaco-editor/0.44.0/min/vs/loader.min.js");
  1716.  
  1717. // 配置Monaco Editor
  1718. require.config({
  1719. paths: { vs: "https://cdn.staticfile.net/monaco-editor/0.44.0/min/vs" },
  1720. "vs/nls": { availableLanguages: { "*": "zh-cn" } },
  1721. });
  1722.  
  1723. // 加载Monaco Editor主脚本
  1724. require(["vs/editor/editor.main"], () => {
  1725. OJBetter.monaco.loaderOnload = true;
  1726. });
  1727. } catch (error) {
  1728. console.error("Failed to load Monaco Editor: ", error);
  1729. }
  1730. }
  1731. }
  1732.  
  1733. /**
  1734. * 美化代码块
  1735. */
  1736. async function beautifyPreBlocksWithMonaco() {
  1737. // 判断monacoLoader是否加载完毕
  1738. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  1739.  
  1740. // 用于替换 <pre> 标签为 Monaco 编辑器的函数
  1741. function replacePreWithMonaco(preElement) {
  1742. const pre = $(preElement);
  1743. if (pre.hasClass('source-code-for-copy')) return; // 跳过复制块
  1744. const code = OJB_getCodeFromPre(pre.get(0));
  1745. if (!code) return;
  1746. const language = OJB_codeLangDetect(code);
  1747.  
  1748. // 创建一个用于 Monaco 编辑器的容器
  1749. const container = $('<div></div>');
  1750. const lineCount = code.split('\n').length; // 代码的行数
  1751.  
  1752. // 计算容器的高度
  1753. const calculateContainerHeight = (lineCount) => {
  1754. const lineHeight = 20; // 每行代码的高度
  1755. const minHeight = 100; // 最小高度
  1756. const maxHeight = 1000; // 最大高度
  1757. const dynamicHeight = lineCount * lineHeight;
  1758. return Math.min(Math.max(dynamicHeight, minHeight), maxHeight) + 'px';
  1759. };
  1760.  
  1761. // 应用样式
  1762. container.css({
  1763. height: calculateContainerHeight(lineCount),
  1764. width: '100%'
  1765. });
  1766. pre.replaceWith(container);
  1767.  
  1768. // 初始化 Monaco 编辑器
  1769. monaco.editor.create(container[0], {
  1770. value: code,
  1771. language: language,
  1772. readOnly: true,
  1773. tabSize: 4,
  1774. theme: OJBetter.basic.darkMode == "dark" ? "vs-dark" : "vs",
  1775. scrollbar: {
  1776. verticalScrollbarSize: 10,
  1777. horizontalScrollbarSize: 10,
  1778. alwaysConsumeMouseWheel: false
  1779. },
  1780. automaticLayout: true,
  1781. scrollBeyondLastLine: false
  1782. });
  1783. }
  1784. // 全局替换页面上所有的 <pre> 元素
  1785. $('pre').each(function () {
  1786. replacePreWithMonaco(this);
  1787. });
  1788. // 监听页面上的提交状态页面窗口的 <pre> 元素
  1789. if (OJBetter.typeOfPage.is_statePage || OJBetter.typeOfPage.is_submissions) {
  1790. OJB_observeElement({
  1791. selector: '#facebox',
  1792. callback: (node) => {
  1793. // 如果 facebox 中存在 pre 元素,则替换它们
  1794. const preElements = $(node).find('pre');
  1795. preElements.each(function () {
  1796. replacePreWithMonaco(this);
  1797. });
  1798. }
  1799. });
  1800. }
  1801. }
  1802.  
  1803. // 样式
  1804. GM_addStyle(`
  1805. /*动画*/
  1806. @keyframes shake {
  1807. 0% { transform: translateX(-5px); }
  1808. 100% { transform: translateX(5px); }
  1809. }
  1810. @keyframes rotate {
  1811. from {
  1812. transform: rotate(0deg);
  1813. }
  1814.  
  1815. to {
  1816. transform: rotate(360deg);
  1817. }
  1818. }
  1819. @keyframes rippleout {
  1820. 0% {
  1821. box-shadow: 0 0 0 0 rgba(96, 98, 102, 0.2);
  1822. }
  1823.  
  1824. 100% {
  1825. box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
  1826. }
  1827. }
  1828. @keyframes bounce-in {
  1829. 20%,40%,60%,80%,from,to {
  1830. animation-timing-function: cubic-bezier(.215,.61,.355,1);
  1831. }
  1832.  
  1833. 0% {
  1834. opacity: 0;
  1835. transform: scale3d(.995,.995,.995);
  1836. }
  1837.  
  1838. 20% {
  1839. opacity: 1;
  1840. transform: scale3d(1.005,1.005,1.005);
  1841. }
  1842.  
  1843. 40% {
  1844. transform: scale3d(.998,.998,.998);
  1845. }
  1846.  
  1847. 60% {
  1848. transform: scale3d(1.002,1.002,1.002);
  1849. }
  1850.  
  1851. 80% {
  1852. transform: scale3d(.995,.995,.995);
  1853. }
  1854.  
  1855. to {
  1856. opacity: 1;
  1857. transform: scale3d(1,1,1);
  1858. }
  1859. }
  1860. /*iconfont图标*/
  1861. .iconfont {
  1862. font-family: "iconfont" !important;
  1863. font-size: 16px;
  1864. font-style: normal !important;
  1865. -webkit-font-smoothing: antialiased;
  1866. -moz-osx-font-smoothing: grayscale;
  1867. }
  1868. @font-face {
  1869. font-family: 'iconfont'; /* Project id 4284341 */
  1870. src: url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2') format('woff2'),
  1871. url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2.ttf') format('truetype');
  1872. }
  1873. html {
  1874. scroll-behavior: smooth;
  1875. }
  1876. :root {
  1877. --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";
  1878. }
  1879. span.mdViewContent {
  1880. white-space: pre-wrap;
  1881. }
  1882.  
  1883. /* 特殊处理,加上input-output-copier类, 让convertStatementToText方法忽略该元素 */
  1884. .translateDiv.input-output-copier, .html2md-panel.input-output-copier, #OJBetter_SubmitForm.input-output-copier {
  1885. font-size: initial;
  1886. float: initial;
  1887. color: initial;
  1888. cursor: initial;
  1889. border: none;
  1890. padding: 0px;
  1891. margin: 0px;
  1892. line-height: initial;
  1893. text-transform: none;
  1894. }
  1895. .translateDiv.input-output-copier:hover, .html2md-panel.input-output-copier:hover, #OJBetter_SubmitForm.input-output-copier:hover {
  1896. background-color: #ffffff00;
  1897. }
  1898.  
  1899. /* dialog */
  1900. dialog {
  1901. margin: 0px;
  1902. }
  1903. dialog::backdrop {
  1904. background-color: rgba(0, 0, 0, 0.4);
  1905. }
  1906.  
  1907. /*题目页链接栏样式*/
  1908. #problemToolbar {
  1909. display: flex;
  1910. flex-wrap: wrap;
  1911. justify-content: flex-end;
  1912. overflow: auto;
  1913. height: 100%;
  1914. margin: 0.5em;
  1915. }
  1916.  
  1917. /*html2md面板*/
  1918. .html2md-panel {
  1919. display: flex;
  1920. justify-content: flex-end;
  1921. align-items: center;
  1922. }
  1923. .html2md-panel a {
  1924. text-decoration: none;
  1925. }
  1926. .html2md-panel > button {
  1927. margin: 5px;
  1928. }
  1929. .html2md-panel.is_simple {
  1930. position: absolute;
  1931. right: 2%;
  1932. }
  1933.  
  1934. /*通用按钮*/
  1935. .ojb_btn {
  1936. display: flex;
  1937. align-items: center;
  1938. justify-content: center;
  1939. cursor: pointer;
  1940. background-color: #ffffff;
  1941. color: #606266;
  1942. width: auto;
  1943. font-size: 13px;
  1944. border-radius: 0.3rem;
  1945. padding: 2px 5px;
  1946. margin: 0px 5px;
  1947. border: 1px solid #dcdfe6;
  1948. }
  1949. .ojb_btn[disabled] {
  1950. cursor: not-allowed !important;
  1951. color: rgb(168, 171, 178) !important;
  1952. border: 1px solid #e4e7ed;
  1953. background-color: #ffffff;
  1954. }
  1955. .ojb_btn:hover {
  1956. color: #409eff;
  1957. border-color: #409eff;
  1958. background-color: #f1f8ff;
  1959. z-index: 150;
  1960. }
  1961. .ojb_btn.primary {
  1962. color: #ffffff;
  1963. border: 1px solid #409eff;
  1964. background-color: #409eff;
  1965. }
  1966. .ojb_btn.primary:hover {
  1967. color: #ffffff;
  1968. border: 1px solid #79bbff;
  1969. background-color: #79bbff;
  1970. }
  1971. .ojb_btn.success {
  1972. color: #4caf50;
  1973. border: 1px solid #C8E6C9;
  1974. background-color: #f0f9eb;
  1975. }
  1976. .ojb_btn.warning {
  1977. color: #e6a23c;
  1978. border: 1px solid #f3d19e;
  1979. background-color: #fdf6ec;
  1980. }
  1981. .ojb_btn.error {
  1982. color: #f56c6c;
  1983. border: 1px solid #fab6b6;
  1984. background-color: #fef0f0;
  1985. }
  1986. .ojb_btn.enabled {
  1987. color: #42A5F5;
  1988. border: 1px solid #90CAF9;
  1989. background-color: #fafbff;
  1990. }
  1991. .ojb_btn.active {
  1992. animation: rippleout 0.5s ease-in-out;
  1993. }
  1994. a.ojb_btn {
  1995. text-decoration: none;
  1996. }
  1997. a.ojb_btn:link {
  1998. color: #606266;
  1999. }
  2000. a.ojb_btn span {
  2001. margin-left: 2px;
  2002. }
  2003. /*按钮图标和popover*/
  2004. .ojb_btn_popover {
  2005. display: flex;
  2006. justify-content: center;
  2007. position: relative;
  2008. outline: none;
  2009. appearance: none;
  2010. }
  2011. .ojb_btn_popover:hover span {
  2012. opacity: 1;
  2013. visibility: visible;
  2014. }
  2015. .ojb_btn_popover i:before {
  2016. position: absolute;
  2017. text-shadow: 1px 1px 0px #ffffff, 1px -1px 0px #ffffff, -1px -1px 0px #ffffff, -1px 1px 0px #ffffff;
  2018. }
  2019. .ojb_btn_popover span {
  2020. cursor: initial;
  2021. position: absolute;
  2022. left: 50%;
  2023. opacity: 0;
  2024. visibility: hidden;
  2025. padding: 4px 8px;
  2026. background-color: rgba(33, 33, 33, 0.8);
  2027. color: rgba(255, 255, 255, 0.9019607843);
  2028. font-size: 12px;
  2029. border-radius: 6px;
  2030. line-height: 1.6;
  2031. text-align: left;
  2032. white-space: nowrap;
  2033. transition: all 0.15s ease-in-out;
  2034. z-index: 999;
  2035. }
  2036. .ojb_btn_popover span:hover {
  2037. opacity: 0;
  2038. visibility: hidden;
  2039. }
  2040. .ojb_btn_popover.top:hover span {
  2041. transform: translate(-50%, 0);
  2042. }
  2043. .ojb_btn_popover.top span {
  2044. bottom: 100%;
  2045. transform: translate(-50%, -20%);
  2046. margin-bottom: 4px;
  2047. }
  2048. .ojb_btn_popover.top span:hover {
  2049. transform: translate(-50%, -20%);
  2050. }
  2051. .ojb_btn_popover.bottom:hover span {
  2052. transform: translate(-50%, 105%);
  2053. }
  2054. .ojb_btn_popover.bottom span {
  2055. bottom: -2%;
  2056. transform: translate(-50%, 100%);
  2057. margin-top: 4px;
  2058. }
  2059. .ojb_btn_popover.bottom span:hover {
  2060. transform: translate(-50%, 50%);
  2061. }
  2062. .ojb_btn_popover.loading i {
  2063. color: rgba(33, 33, 33, 0.1);
  2064. }
  2065. .ojb_btn_popover.loading i:before {
  2066. content: "\\e640";
  2067. color: rgb(168, 171, 178);
  2068. animation: rotate 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
  2069. }
  2070. .ojb_btn_popover.running i {
  2071. color: rgba(33, 33, 33, 0.1);
  2072. }
  2073. .ojb_btn_popover.running i:before {
  2074. content: "\\e600";
  2075. color: rgb(168, 171, 178);
  2076. animation: rotate 1s linear infinite;
  2077. }
  2078. .ojb_btn_popover.warning i {
  2079. color: rgba(230, 162, 61, 0.8);
  2080. }
  2081. .ojb_btn_popover.warning i:before {
  2082. content: "\\e68b";
  2083. font-size: 15px;
  2084. left: 10px;
  2085. bottom: 0%;
  2086. color: #ff9800;
  2087. }
  2088. .ojb_btn_popover.error i {
  2089. color: rgba(245, 108, 108, 0.8);
  2090. }
  2091. .ojb_btn_popover.error i:before {
  2092. content: "\\e651";
  2093. font-size: 15px;
  2094. left: 10px;
  2095. bottom: 0%;
  2096. color: #F44336;
  2097. }
  2098. .ojb_btn_popover.success i {
  2099. color: rgba(76, 175, 80, 0.9);
  2100. }
  2101. .ojb_btn_popover.success i:before {
  2102. content: "\\e61e";
  2103. font-size: 15px;
  2104. left: 10px;
  2105. bottom: 0%;
  2106. color: #4caf50;
  2107. }
  2108. .ojb_btn_popover.enabled i {
  2109. color: rgba(33, 150, 243, 0.6);
  2110. }
  2111. .ojb_btn_popover.enabled i:before {
  2112. content: "\\e6f4";
  2113. font-size: 15px;
  2114. left: 10px;
  2115. bottom: 0%;
  2116. color: #2196F3;
  2117. }
  2118. .ojb_btn_popover.redo i {
  2119. color: rgba(33, 33, 33, 0.1);
  2120. }
  2121. .ojb_btn_popover.redo i:before {
  2122. content: "\\e831";
  2123. color: #616161;
  2124. }
  2125. .ojb_btn_popover.reverse i {
  2126. transform: rotate(180deg);
  2127. }
  2128.  
  2129. /*translateDiv样式*/
  2130. .translateDiv .topText {
  2131. display: flex;
  2132. margin-left: 5px;
  2133. color: #9e9e9e;
  2134. font-size: 13px;
  2135. align-items: center;
  2136. }
  2137. .translateDiv .borderlessButton{
  2138. display: flex;
  2139. align-items: center;
  2140. margin: 2.5px 7px;
  2141. fill: #9E9E9E;
  2142. }
  2143. .translateDiv .borderlessButton:hover{
  2144. cursor: pointer;
  2145. fill: #059669;
  2146. }
  2147. .translateDiv.bounce-in {
  2148. animation: bounce-in 1s forwards;
  2149. }
  2150. html:not([data-theme='dark']) .translateDiv {
  2151. box-shadow: 0px 0px 0.5px 0.5px #defdf378;
  2152. }
  2153. .translate-problem-statement {
  2154. justify-items: start;
  2155. letter-spacing: 1.8px;
  2156. color: #059669;
  2157. background-color: #f9f9fa;
  2158. border: 1px solid #c5ebdf;
  2159. border-radius: 0rem 0rem 0.3rem 0.3rem;
  2160. padding: 5px;
  2161. margin: -5px 0px 6px 0px;
  2162. width: 100%;
  2163. box-sizing: border-box;
  2164. font-size: 13px;
  2165. }
  2166. .translate-problem-statement h2 {
  2167. font-size: 1.6em;
  2168. font-weight: 700;
  2169. }
  2170. .translate-problem-statement h3 {
  2171. font-size: 1.3em;
  2172. font-weight: 700;
  2173. }
  2174. .translate-problem-statement-panel{
  2175. display: flex;
  2176. justify-content: space-between;
  2177. background-color: #f9f9fa;
  2178. border: 1px solid #c5ebdf;
  2179. border-radius: 0.3rem;
  2180. margin: 4px 0px;
  2181. }
  2182. .translate-problem-statement-panel .ojb_btn {
  2183. background: none;
  2184. border: none;
  2185. color: #9e9e9e;
  2186. }
  2187. .translate-problem-statement-panel.error, .translate-problem-statement.error {
  2188. color: red;
  2189. border-color: red;
  2190. }
  2191. .translate-problem-statement a, .translate-problem-statement a:link {
  2192. color: #10b981;
  2193. font-weight: 600;
  2194. background: 0 0;
  2195. text-decoration: none;
  2196. }
  2197. .translate-problem-statement ol, .translate-problem-statement ul {
  2198. display: grid;
  2199. margin-inline-start: 0.8em;
  2200. margin-block-start: 0em;
  2201. margin: 0.5em 0 0 3em;
  2202. padding-inline-start: 0px;
  2203. }
  2204. .translate-problem-statement li {
  2205. display: list-item;
  2206. height: auto;
  2207. word-wrap: break-word;
  2208. }
  2209. .translate-problem-statement ol li {
  2210. list-style-type: auto;
  2211. }
  2212. .translate-problem-statement ul li {
  2213. list-style-type: disc;
  2214. }
  2215. .translate-problem-statement img {
  2216. max-width: 100.0%;
  2217. max-height: 100.0%;
  2218. }
  2219. .ttypography .translate-problem-statement .MathJax {
  2220. color: #059669!important;
  2221. }
  2222. .translate-problem-statement span.math {
  2223. margin: 0px 2.5px !important;
  2224. }
  2225. .translate-problem-statement a:hover {
  2226. background-color: #800;
  2227. color: #fff;
  2228. text-decoration: none;
  2229. }
  2230. .translate-problem-statement table {
  2231. border: 1px #ccc solid !important;
  2232. margin: 1.5em 0 !important;
  2233. color: #059669 !important;
  2234. }
  2235. .translate-problem-statement table thead th {
  2236. border: 1px #ccc solid !important;
  2237. color: #059669 !important;
  2238. }
  2239. .translate-problem-statement table td {
  2240. border-right: 1px solid #ccc;
  2241. border-top: 1px solid #ccc;
  2242. padding: 0.7143em 0.5em;
  2243. }
  2244. .translate-problem-statement table th {
  2245. padding: 0.7143em 0.5em;
  2246. }
  2247. .translate-problem-statement p:not(:first-child) {
  2248. margin: 1.5em 0 0;
  2249. }
  2250. .translate-problem-statement p {
  2251. line-height: 20px !important;
  2252. word-wrap: break-word;
  2253. font-size: 13px !important
  2254. }
  2255. .problem-statement p:last-child {
  2256. margin-bottom: 0px !important;
  2257. }
  2258.  
  2259. /*设置按钮*/
  2260. header .enter-or-register-box, header .languages {
  2261. position: absolute;
  2262. right: 170px;
  2263. }
  2264. .ojb_btn.OJBetter_setting {
  2265. float: right;
  2266. height: 30px;
  2267. background: #60a5fa;
  2268. color: white;
  2269. margin: 10px;
  2270. border: 1px solid #60a5fa;
  2271. }
  2272. .ojb_btn.OJBetter_setting.open {
  2273. background-color: #e6e6e6;
  2274. color: #727378;
  2275. cursor: not-allowed;
  2276. }
  2277.  
  2278. /*设置面板*/
  2279. .OJBetter_setting_menu {
  2280. box-shadow: 0px 0px 0px 4px #ffffff;
  2281. position: fixed;
  2282. top: 50%;
  2283. left: 50%;
  2284. width: 600px;
  2285. min-height: 600px;
  2286. transform: translate(-50%, -50%);
  2287. border-radius: 6px;
  2288. background-color: #f0f4f9;
  2289. border-collapse: collapse;
  2290. border: 1px solid #ffffff;
  2291. color: #697e91;
  2292. font-family: var(--vp-font-family-base);
  2293. padding: 10px 20px 20px 10px;
  2294. box-sizing: content-box;
  2295. }
  2296. .OJBetter_setting_menu h3 {
  2297. margin-top: 10px;
  2298. font-size: 1.4em;
  2299. font-weight: 700;
  2300. }
  2301. .OJBetter_setting_menu h4 {
  2302. margin: 15px 0px 10px 0px;
  2303. }
  2304. .OJBetter_setting_menu h4,.OJBetter_setting_menu h5 {
  2305. font-weight: 600;
  2306. }
  2307. .OJBetter_setting_menu hr {
  2308. border: none;
  2309. height: 1px;
  2310. background-color: #ccc;
  2311. margin: 10px 0;
  2312. }
  2313. .OJBetter_setting_menu details {
  2314. padding: 10px;
  2315. margin-bottom: 5px;
  2316. background-color: #ffffff;
  2317. border-bottom: 1px solid #c9c6c696;
  2318. border-radius: 8px;
  2319. }
  2320. .OJBetter_setting_menu .badge {
  2321. border-radius: 4px;
  2322. border: 1px solid #009688;
  2323. color: #009688;
  2324. background-color: #fff;
  2325. padding: 0.5px 4px;
  2326. margin-left: 5px;
  2327. margin-right: auto;
  2328. line-height: initial;
  2329. font-weight: initial;
  2330. }
  2331. .OJBetter_setting_menu .missing {
  2332. box-shadow: inset 0px 0px 1px 1px red;
  2333. }
  2334. /* 页面切换 */
  2335. .OJBetter_setting_menu .settings-page {
  2336. display: none;
  2337. }
  2338. .OJBetter_setting_menu .settings-page.active {
  2339. display: block;
  2340. }
  2341. .OJBetter_setting_container {
  2342. display: flex;
  2343. }
  2344. .OJBetter_setting_sidebar {
  2345. flex: 0 0 auto;
  2346. min-width: 110px;
  2347. padding: 6px 10px 6px 6px;
  2348. margin: 20px 0px;
  2349. border-right: 1px solid #d4d8e9;
  2350. }
  2351. .OJBetter_setting_content {
  2352. flex-grow: 1;
  2353. margin: 20px 0px 0px 12px;
  2354. padding-right: 10px;
  2355. max-height: 580px;
  2356. overflow-y: auto;
  2357. box-sizing: border-box;
  2358. }
  2359. .OJBetter_setting_sidebar h3 {
  2360. margin-top: 0;
  2361. }
  2362. .OJBetter_setting_sidebar hr {
  2363. margin-top: 10px;
  2364. margin-bottom: 10px;
  2365. border: none;
  2366. border-top: 1px solid #DADCE0;
  2367. }
  2368. .OJBetter_setting_sidebar ul {
  2369. list-style-type: none;
  2370. margin: 0;
  2371. padding: 0;
  2372. }
  2373. .OJBetter_setting_sidebar li {
  2374. margin: 5px 0px;
  2375. background-color: #ffffff;
  2376. border: 1px solid #d4d8e9;
  2377. border-radius: 4px;
  2378. font-size: 16px;
  2379. }
  2380. .OJBetter_setting_sidebar li a {
  2381. text-decoration: none;
  2382. display: flex;
  2383. width: 100%;
  2384. font-size: 16px;
  2385. color: gray;
  2386. background-color: #ffffff;
  2387. border: none;
  2388. letter-spacing: 2px;
  2389. padding: 7px;
  2390. margin: 0px;
  2391. border-radius: 4px;
  2392. align-items: center;
  2393. -webkit-box-sizing: border-box;
  2394. -moz-box-sizing: border-box;
  2395. box-sizing: border-box;
  2396. }
  2397. .OJBetter_setting_sidebar li a.active {
  2398. background-color: #eceff1c7;
  2399. }
  2400. /* 链接样式 */
  2401. .OJBetter_setting_menu a {
  2402. font-size: 13px;
  2403. color: #009688;
  2404. background-color: #E0F2F1;
  2405. border: 1px solid #009688;
  2406. border-radius: 4px;
  2407. padding: 0px 5px;
  2408. margin: 0px 5px;
  2409. text-decoration: none;
  2410. }
  2411. /* 下拉选择框 */
  2412. .OJBetter_setting_menu select {
  2413. appearance: none;
  2414. padding: 5px 10px;
  2415. margin: -5px 0px;
  2416. border-radius: 6px;
  2417. border-style: solid;
  2418. border: 1px solid #ced4da;
  2419. color: #009688;
  2420. background: #ffffff;
  2421. font-size: 15px;
  2422. }
  2423. .OJBetter_setting_menu select:focus-visible {
  2424. outline: none;
  2425. }
  2426. .OJBetter_setting_menu select option:disabled {
  2427. color: #EEEEEE;
  2428. }
  2429. /* 数值输入框 */
  2430. .OJBetter_setting_menu input[type="number"] {
  2431. width: 40px;
  2432. color: #009688;
  2433. font-size: 15px;
  2434. appearance: none;
  2435. padding: 5px 10px;
  2436. margin: -5px 3px;
  2437. border-radius: 6px;
  2438. border-style: solid;
  2439. border: 1px solid #ced4da;
  2440. }
  2441. .OJBetter_setting_menu input[type="number"]:focus-visible {
  2442. outline: none;
  2443. }
  2444. .OJBetter_setting_menu input[type="number"]::-webkit-inner-spin-button,
  2445. .OJBetter_setting_menu input[type="number"]::-webkit-outer-spin-button {
  2446. -webkit-appearance: none;
  2447. margin: 0;
  2448. }
  2449. /*设置面板-滚动条*/
  2450. .OJBetter_setting_menu::-webkit-scrollbar, .OJBetter_setting_content::-webkit-scrollbar,
  2451. .OJBetter_modal .content::-webkit-scrollbar {
  2452. width: 5px;
  2453. height: 7px;
  2454. background-color: #aaa;
  2455. }
  2456. .OJBetter_setting_menu::-webkit-scrollbar-thumb, .OJBetter_setting_content::-webkit-scrollbar-thumb,
  2457. .OJBetter_modal .content::-webkit-scrollbar-thumb {
  2458. background-clip: padding-box;
  2459. background-color: #d7d9e4;
  2460. }
  2461. .OJBetter_setting_menu::-webkit-scrollbar-track, .OJBetter_setting_content::-webkit-scrollbar-track,
  2462. .OJBetter_modal .content::-webkit-scrollbar-track {
  2463. background-color: #f1f1f1;
  2464. }
  2465. /*设置面板-关闭按钮*/
  2466. .OJBetter_setting_menu .tool-box {
  2467. position: absolute;
  2468. width: 20px;
  2469. height: 20px;
  2470. top: 3px;
  2471. right: 3px;
  2472. }
  2473. .OJBetter_setting_menu .btn-close {
  2474. width: 20px;
  2475. height: 20px;
  2476. border-radius: 50%;
  2477. border: none;
  2478. margin: 0px;
  2479. padding: 0px;
  2480. background-color: #ff000080;
  2481. transition: .15s ease all;
  2482. box-sizing: border-box;
  2483. text-align: center;
  2484. color: transparent;
  2485. }
  2486. .OJBetter_setting_menu .iconfont {
  2487. font-size: 10px;
  2488. font-weight: bolder;
  2489. }
  2490. .OJBetter_setting_menu .btn-close:hover {
  2491. color: #ffffff;
  2492. background-color: #ff0000cc;
  2493. box-shadow: 0 5px 5px 0 #00000026;
  2494. }
  2495. .OJBetter_setting_menu .btn-close:active {
  2496. color: #ffffffde;
  2497. background-color: #ff000080;
  2498. }
  2499. /*设置面板-checkbox*/
  2500. .OJBetter_setting_menu input[type=checkbox]:focus {
  2501. outline: 0px;
  2502. }
  2503. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"] {
  2504. margin: 0px;
  2505. appearance: none;
  2506. -webkit-appearance: none;
  2507. width: 40px;
  2508. height: 20px;
  2509. border: 1.5px solid #D7CCC8;
  2510. padding: 0px !important;
  2511. border-radius: 20px;
  2512. background: #efebe978;
  2513. position: relative;
  2514. box-sizing: border-box;
  2515. }
  2516. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::before {
  2517. content: "";
  2518. width: 17px;
  2519. height: 17px;
  2520. background: #D7CCC8;
  2521. border: 1.5px solid #BCAAA4;
  2522. border-radius: 50%;
  2523. position: absolute;
  2524. top: 0;
  2525. left: 0;
  2526. transform: translate(2%, 2%);
  2527. transition: all 0.3s ease-in-out;
  2528. box-sizing: border-box;
  2529. }
  2530. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::after {
  2531. 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");
  2532. position: absolute;
  2533. top: 0;
  2534. left: 24px;
  2535. }
  2536. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked {
  2537. border: 1.5px solid #C5CAE9;
  2538. background: #E8EAF6;
  2539. }
  2540. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::before {
  2541. background: #C5CAE9;
  2542. border: 1.5px solid #7986CB;
  2543. transform: translate(122%, 2%);
  2544. transition: all 0.3s ease-in-out;
  2545. }
  2546. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::after {
  2547. 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");
  2548. position: absolute;
  2549. top: 1.5px;
  2550. left: 4.5px;
  2551. }
  2552. .OJBetter_setting_menu .OJBetter_setting_list button {
  2553. cursor: pointer;
  2554. color: #7986cb;
  2555. background-color: #e8eaf6;
  2556. border: 1px solid #7986cb;
  2557. border-radius: 6px;
  2558. width: 100px;
  2559. margin: -5px 2px;
  2560. padding: 5px 10px;
  2561. }
  2562. .OJBetter_setting_menu .OJBetter_setting_list button:hover {
  2563. color: #e8eaf6;
  2564. background-color: #7986cb;
  2565. border: 1px solid #7986cb;
  2566. }
  2567. .OJBetter_setting_menu label, #darkMode_span, #loaded_span {
  2568. font-size: 16px;
  2569. }
  2570. .OJBetter_setting_list {
  2571. display: flex;
  2572. flex-wrap: wrap;
  2573. align-items: center;
  2574. padding: 10px;
  2575. margin: 5px 0px;
  2576. background-color: #ffffff;
  2577. border: 1px solid #c9c6c642;
  2578. border-bottom-color: #c9c6c696;
  2579. border-radius: 8px;
  2580. justify-content: space-between;
  2581. }
  2582. .OJBetter_setting_list.alert_danger {
  2583. color: #F44336;
  2584. background-color: #FFEBEE;
  2585. border: 1px solid #F44336;
  2586. margin: 10px 0px;
  2587. }
  2588. .OJBetter_setting_list.alert_warn {
  2589. color: #E65100;
  2590. background-color: #FFF3E0;
  2591. border: 1px solid #FF9800;
  2592. margin: 10px 0px;
  2593. }
  2594. .OJBetter_setting_list.alert_tip {
  2595. color: #009688;
  2596. background-color: #E0F2F1;
  2597. border: 1px solid #009688;
  2598. margin: 10px 0px;
  2599. }
  2600. .OJBetter_setting_list.alert_info {
  2601. color: #ffffff;
  2602. background-color: #009688;
  2603. margin: 10px 0px;
  2604. box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset;
  2605. }
  2606. .OJBetter_setting_list p:not(:last-child) {
  2607. margin-bottom: 10px;
  2608. }
  2609. .OJBetter_setting_list p:not(:first-child) {
  2610. margin-top: 10px;
  2611. }
  2612. /*设置面板-checkboxs*/
  2613. .OJBetter_setting_menu .OJBetter_checkboxs {
  2614. flex-basis: 100%;
  2615. display: flex;
  2616. padding: 8px;
  2617. margin: 10px 0px 0px 0px;
  2618. border-bottom: 1px solid #c9c6c696;
  2619. border-radius: 8px;
  2620. border: 1px solid #c5cae9;
  2621. background-color: #f0f8ff;
  2622. }
  2623. .OJBetter_setting_menu .OJBetter_checkboxs label {
  2624. font-size: 13px;
  2625. margin: 0px 6px 0px 3px;
  2626. }
  2627. .OJBetter_setting_menu .OJBetter_checkboxs input[type=checkbox]:checked+label{
  2628. color: #7986cb;
  2629. }
  2630. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"] {
  2631. border: none;
  2632. width: 16px;
  2633. height: 16px;
  2634. }
  2635. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before{
  2636. background: #ffffff;
  2637. transform: none;
  2638. width: 16px;
  2639. height: 16px;
  2640. }
  2641. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked {
  2642. background: none;
  2643. border: none;
  2644. }
  2645. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::before {
  2646. border: 1.5px solid #95a2de;
  2647. background: #e8eaf6;
  2648. transform: none;
  2649. }
  2650. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::after {
  2651. 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");
  2652. top: 0px;
  2653. left: 3.5px;
  2654. }
  2655. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:disabled+label {
  2656. color: #BDBDBD;
  2657. }
  2658. /*设置面板-radio*/
  2659. .OJBetter_setting_menu label {
  2660. display: block;
  2661. font-weight: initial;
  2662. list-style-type: none;
  2663. padding-inline-start: 0px;
  2664. overflow-x: auto;
  2665. max-width: 100%;
  2666. margin: 3px 0px;
  2667. overflow-x: visible;
  2668. }
  2669. .OJBetter_setting_menu_label_text {
  2670. display: flex;
  2671. border: 1px dashed #00aeeccc;
  2672. height: 35px;
  2673. width: 100%;
  2674. color: #6e6e6e;
  2675. font-weight: 300;
  2676. font-size: 14px;
  2677. letter-spacing: 2px;
  2678. padding: 7px;
  2679. margin-bottom: 4px;
  2680. align-items: center;
  2681. -webkit-box-sizing: border-box;
  2682. -moz-box-sizing: border-box;
  2683. box-sizing: border-box;
  2684. }
  2685. input[type="radio"]:checked+.OJBetter_setting_menu_label_text {
  2686. background: #41e49930;
  2687. border: 1px solid green;
  2688. color: green;
  2689. text-shadow: 0px 0px 0.5px green;
  2690. }
  2691. input[type="radio"]:disabled+.OJBetter_setting_menu_label_text {
  2692. background: #fafafa00;
  2693. border: 1px solid #e0e0e07a;
  2694. color: #e0e0e0;
  2695. }
  2696. .OJBetter_setting_menu label input[type="radio"], .OJBetter_contextmenu label input[type="radio"]{
  2697. appearance: none;
  2698. list-style: none;
  2699. padding: 0px !important;
  2700. margin: 0px;
  2701. clip: rect(0 0 0 0);
  2702. -webkit-clip-path: inset(100%);
  2703. clip-path: inset(100%);
  2704. height: 1px;
  2705. overflow: hidden;
  2706. position: absolute;
  2707. white-space: nowrap;
  2708. width: 1px;
  2709. }
  2710. /*设置面板-文本输入框*/
  2711. .OJBetter_setting_menu input[type="text"] {
  2712. display: block;
  2713. height: 25px !important;
  2714. width: 100%;
  2715. background-color: #ffffff;
  2716. color: #727378;
  2717. font-size: 12px;
  2718. border-radius: 0.3rem;
  2719. padding: 1px 5px !important;
  2720. box-sizing: border-box;
  2721. margin: 5px 0px 5px 0px;
  2722. border: 1px solid #00aeeccc;
  2723. box-shadow: 0 0 1px #0000004d;
  2724. }
  2725. .OJBetter_setting_menu .OJBetter_setting_list input[type="text"] {
  2726. margin-left: 5px;
  2727. }
  2728. .OJBetter_setting_menu input[type="text"]:focus-visible{
  2729. border-style: solid;
  2730. border-color: #3f51b5;
  2731. outline: none;
  2732. }
  2733. .OJBetter_setting_menu_config_box {
  2734. width: 100%;
  2735. display: grid;
  2736. margin-top: 5px;
  2737. -webkit-box-sizing: border-box;
  2738. -moz-box-sizing: border-box;
  2739. box-sizing: border-box;
  2740. }
  2741. .OJBetter_setting_menu input::placeholder {
  2742. color: #727378;
  2743. }
  2744. .OJBetter_setting_menu input.no_default::placeholder{
  2745. color: #BDBDBD;
  2746. }
  2747. .OJBetter_setting_menu input.is_null::placeholder{
  2748. color: red;
  2749. border-width: 1.5px;
  2750. }
  2751. .OJBetter_setting_menu input.is_null{
  2752. border-color: red;
  2753. }
  2754. .OJBetter_setting_menu textarea {
  2755. resize: vertical;
  2756. display: block;
  2757. width: 100%;
  2758. height: 60px;
  2759. background-color: #ffffff;
  2760. color: #727378;
  2761. font-size: 12px;
  2762. padding: 1px 5px !important;
  2763. box-sizing: border-box;
  2764. margin: 5px 0px 5px 0px;
  2765. border: 1px solid #00aeeccc;
  2766. box-shadow: 0 0 1px #0000004d;
  2767. }
  2768. .OJBetter_setting_menu textarea:focus-visible{
  2769. border-style: solid;
  2770. border-color: #3f51b5;
  2771. outline: none;
  2772. }
  2773. .OJBetter_setting_menu textarea::placeholder{
  2774. color: #BDBDBD;
  2775. font-size: 14px;
  2776. }
  2777. .OJBetter_setting_menu #tempConfig_save {
  2778. cursor: pointer;
  2779. display: inline-flex;
  2780. padding: 5px;
  2781. background-color: #1aa06d;
  2782. color: #ffffff;
  2783. font-size: 14px;
  2784. line-height: 1.5rem;
  2785. font-weight: 500;
  2786. justify-content: center;
  2787. width: 100%;
  2788. border-radius: 0.375rem;
  2789. border: none;
  2790. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  2791. margin-top: 20px
  2792. }
  2793. .OJBetter_setting_menu button#debug_button.debug_button {
  2794. width: 18%;
  2795. }
  2796. .OJBetter_setting_menu span.tip {
  2797. color: #999;
  2798. font-size: 12px;
  2799. font-weight: 500;
  2800. padding: 5px 0px;
  2801. }
  2802. /*设置面板-tip*/
  2803. .help_tip {
  2804. margin-right: auto;
  2805. }
  2806. span.input_label {
  2807. font-size: 14px;
  2808. }
  2809. .help_tip .tip_text {
  2810. display: none;
  2811. position: absolute;
  2812. color: #697e91;
  2813. font-weight: 400;
  2814. font-size: 14px;
  2815. letter-spacing: 0px;
  2816. background-color: #ffffff;
  2817. padding: 10px;
  2818. margin: 5px 0px;
  2819. border-radius: 4px;
  2820. border: 1px solid #e4e7ed;
  2821. box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);
  2822. z-index: 100;
  2823. }
  2824. .help_tip .tip_text p {
  2825. margin-bottom: 5px;
  2826. }
  2827. .help_tip .tip_text:before {
  2828. content: "";
  2829. position: absolute;
  2830. top: -20px;
  2831. right: -10px;
  2832. bottom: -10px;
  2833. left: -10px;
  2834. z-index: -1;
  2835. }
  2836. .help-icon {
  2837. cursor: help;
  2838. width: 15px;
  2839. color: #b4b9d4;
  2840. margin-left: 5px;
  2841. margin-top: 3px;
  2842. }
  2843. .OJBetter_setting_menu .OJBetter_setting_menu_label_text .help_tip .help-icon {
  2844. color: #7fbeb2;
  2845. }
  2846. .help_tip .help-icon:hover + .tip_text, .help_tip .tip_text:hover {
  2847. display: block;
  2848. cursor: help;
  2849. width: 250px;
  2850. }
  2851. /* 版本信息 */
  2852. .OJBetter_setting_menu .versionInfo{
  2853. display: grid;
  2854. justify-items: center;
  2855. font-size: 16px;
  2856. padding: 10px;
  2857. }
  2858. .OJBetter_setting_menu .versionInfo>* {
  2859. margin: 10px 0px;
  2860. }
  2861.  
  2862. /* 配置管理面板 */
  2863. .config{
  2864. width: 100%;
  2865. margin: 10px 0px;
  2866. }
  2867. .config li.tempConfig_add_button {
  2868. cursor: pointer;
  2869. height: 40px;
  2870. border: 1px dashed #BDBDBD;
  2871. border-radius: 8px;
  2872. background-color: #fcfbfb36;
  2873. color: #bdbdbd;
  2874. font-size: 14px;
  2875. align-items: center;
  2876. justify-content: center;
  2877. }
  2878. .config li.tempConfig_add_button:hover {
  2879. border: 1px dashed #03A9F4;
  2880. background-color: #d7f0fb8c;
  2881. color: #03A9F4;
  2882. }
  2883. .config .config_bar_list {
  2884. display: flex;
  2885. width: 100%;
  2886. padding-bottom: 2px;
  2887. border: 1px solid #c5cae9;
  2888. background-color: #f0f8ff;
  2889. box-sizing: border-box;
  2890. border-radius: 0px 0px 8px 8px;
  2891. }
  2892. .config .config_bar_list input[type="radio"] {
  2893. appearance: none;
  2894. width: 0;
  2895. height: 0;
  2896. overflow: hidden;
  2897. }
  2898. .config .config_bar_list input[type="radio"] {
  2899. margin: 0px;
  2900. }
  2901. .config .config_bar_list input[type=radio]:focus {
  2902. outline: 0px;
  2903. }
  2904. .config .config_bar_ul_li_text {
  2905. display: flex;
  2906. align-items: center;
  2907. justify-content: center;
  2908. max-width: 100%;
  2909. height: 40px;
  2910. overflow-x: auto;
  2911. font-size: 14px;
  2912. font-weight: 400;
  2913. margin: 0px 4px;
  2914. padding: 3px;
  2915. border: 1px solid #dedede;
  2916. border-radius: 10px;
  2917. box-shadow: 0px 2px 4px 0px rgba(0,0,0,.05);
  2918. box-sizing: border-box;
  2919. }
  2920. .config .config_bar_ul li button {
  2921. background-color: #e6e6e6;
  2922. color: #727378;
  2923. height: 23px;
  2924. font-size: 14px;
  2925. border-radius: 0.3rem;
  2926. padding: 1px 5px;
  2927. margin: 5px;
  2928. border: none;
  2929. box-shadow: 0 0 1px #0000004d;
  2930. }
  2931. .config .config_bar_ul {
  2932. display: flex;
  2933. align-items: center;
  2934. list-style-type: none;
  2935. padding-inline-start: 0px;
  2936. overflow-x: auto;
  2937. max-width: 100%;
  2938. margin: 0px;
  2939. padding: 5px;
  2940. }
  2941. .config .config_bar_ul li {
  2942. width: 80px;
  2943. display: grid;
  2944. margin: 4px 4px;
  2945. min-width: 100px;
  2946. box-sizing: border-box;
  2947. }
  2948. .config .config_bar_ul_li_text:hover {
  2949. background-color: #eae4dc24;
  2950. }
  2951. input[type="radio"]:checked + .config_bar_ul_li_text {
  2952. background: #41b3e430;
  2953. border: 1px solid #5e7ce0;
  2954. color: #5e7ce0;
  2955. }
  2956. .config .config_bar_ul::-webkit-scrollbar {
  2957. width: 5px;
  2958. height: 4px;
  2959. }
  2960. .config .config_bar_ul::-webkit-scrollbar-thumb {
  2961. background-clip: padding-box;
  2962. background-color: #d7d9e4;
  2963. border-radius: 8px;
  2964. }
  2965. .config .config_bar_ul::-webkit-scrollbar-button:start:decrement {
  2966. width: 4px;
  2967. background-color: transparent;
  2968. }
  2969. .config .config_bar_ul::-webkit-scrollbar-button:end:increment {
  2970. width: 4px;
  2971. background-color: transparent;
  2972. }
  2973. .config .config_bar_ul::-webkit-scrollbar-track {
  2974. border-radius: 5px;
  2975. }
  2976. .config .config_bar_ul_li_text::-webkit-scrollbar {
  2977. width: 5px;
  2978. height: 7px;
  2979. background-color: #aaa;
  2980. }
  2981. .config .config_bar_ul_li_text::-webkit-scrollbar-thumb {
  2982. background-clip: padding-box;
  2983. background-color: #d7d9e4;
  2984. }
  2985. .config .config_bar_ul_li_text::-webkit-scrollbar-track {
  2986. background-color: #f1f1f1;
  2987. }
  2988. .config .config_bar_list_add_div {
  2989. display: flex;
  2990. height: 40px;
  2991. margin: 4px 2px;
  2992. }
  2993.  
  2994. /* 修改菜单 */
  2995. #config_bar_menu {
  2996. z-index: 400;
  2997. position: fixed;
  2998. width: 60px;
  2999. background: #ffffff;
  3000. box-shadow: 1px 1px 4px 0px #0000004d;
  3001. border: 0px solid rgba(0,0,0,0.04);
  3002. border-radius: 4px;
  3003. padding: 8px 0;
  3004. }
  3005. .config_bar_menu_item {
  3006. cursor: pointer;
  3007. padding: 2px 6px;
  3008. display: flex;
  3009. justify-content: center;
  3010. align-items: center;
  3011. height: 32px;
  3012. font-size: 14px;
  3013. font-weight: 500;
  3014. box-shadow: inset 0px 0px 0px 0px #8bb2d9;
  3015. }
  3016. #config_bar_menu_edit:hover {
  3017. background-color: #00aeec;
  3018. color: white;
  3019. }
  3020. #config_bar_menu_delete:hover {
  3021. background-color: #FF5722;
  3022. color: white;
  3023. }
  3024.  
  3025. /* 配置编辑页面 */
  3026. #config_edit_menu {
  3027. z-index: 300;
  3028. width: 450px;
  3029. }
  3030.  
  3031. /* 黑暗模式选项按钮 */
  3032. .dark-mode-selection {
  3033. display: flex;
  3034. justify-content: center;
  3035. align-items: center;
  3036. max-width: 350px;
  3037. -webkit-user-select: none;
  3038. -moz-user-select: none;
  3039. -ms-user-select: none;
  3040. user-select: none;
  3041. }
  3042. .dark-mode-selection label {
  3043. margin: 8px 0px 8px 8px;
  3044. }
  3045. .dark-mode-selection > * {
  3046. margin: 6px;
  3047. }
  3048. .dark-mode-selection .OJBetter_setting_menu_label_text {
  3049. border-radius: 8px;
  3050. margin-bottom: 0px;
  3051. }
  3052.  
  3053. /*确认弹窗*/
  3054. .OJBetter_modal {
  3055. z-index: 600;
  3056. display: grid;
  3057. position: fixed;
  3058. top: 50%;
  3059. left: 50%;
  3060. transform: translate(-50%, -50%);
  3061. font-size: 12px;
  3062. font-family: var(--vp-font-family-base);
  3063. width: max-content;
  3064. padding: 10px 20px;
  3065. box-shadow: 0px 0px 0px 4px #ffffff;
  3066. border-radius: 6px;
  3067. background-color: #f0f4f9;
  3068. border-collapse: collapse;
  3069. border: 1px solid #ffffff;
  3070. color: #697e91;
  3071. }
  3072. .OJBetter_modal h2 {
  3073. font-size: 1.6em;
  3074. font-weight: 700;
  3075. }
  3076. .OJBetter_modal .content{
  3077. white-space: nowrap;
  3078. max-height: 500px;
  3079. overflow-y: auto;
  3080. }
  3081. .OJBetter_modal .buttons{
  3082. display: flex;
  3083. padding-top: 15px;
  3084. }
  3085. .OJBetter_modal button {
  3086. display: inline-flex;
  3087. justify-content: center;
  3088. align-items: center;
  3089. line-height: 1;
  3090. white-space: nowrap;
  3091. cursor: pointer;
  3092. text-align: center;
  3093. box-sizing: border-box;
  3094. outline: none;
  3095. transition: .1s;
  3096. user-select: none;
  3097. vertical-align: middle;
  3098. -webkit-appearance: none;
  3099. height: 24px;
  3100. padding: 5px 11px;
  3101. margin-right: 15px;
  3102. font-size: 12px;
  3103. border-radius: 4px;
  3104. color: #ffffff;
  3105. background: #009688;
  3106. border-color: #009688;
  3107. border: none;
  3108. }
  3109. .OJBetter_modal button.secondary{
  3110. background-color:#4DB6AC;
  3111. }
  3112. .OJBetter_modal button:hover{
  3113. background-color:#4DB6AC;
  3114. }
  3115. .OJBetter_modal button.secondary:hover {
  3116. background-color: #80CBC4;
  3117. }
  3118. .OJBetter_modal .help-icon {
  3119. margin: 0px 8px 0px 0px;
  3120. height: 1em;
  3121. width: 1em;
  3122. line-height: 1em;
  3123. display: inline-flex;
  3124. justify-content: center;
  3125. align-items: center;
  3126. position: relative;
  3127. fill: currentColor;
  3128. font-size: inherit;
  3129. }
  3130. .OJBetter_modal p {
  3131. margin: 5px 0px;
  3132. }
  3133.  
  3134. /* 右键菜单 */
  3135. .OJBetter_contextmenu {
  3136. z-index: 500;
  3137. display: grid;
  3138. position: absolute;
  3139. background-color: #f0f4f9;
  3140. border-collapse: collapse;
  3141. color: #697e91;
  3142. font-family: var(--vp-font-family-base);
  3143. overflow: hidden;
  3144. box-sizing: content-box;
  3145. box-shadow: 0px 0px 0px 2px #eddbdb4d;
  3146. }
  3147. .OJBetter_contextmenu label {
  3148. margin: 0px;
  3149. }
  3150. input[type="radio"]:checked+.OJBetter_contextmenu_label_text {
  3151. background: #41e49930;
  3152. border: 1px solid green;
  3153. color: green;
  3154. font-weight: 500;
  3155. }
  3156. .OJBetter_contextmenu_label_text {
  3157. display: flex;
  3158. border: 1px dashed #80cbc4;
  3159. height: 26px;
  3160. width: 100%;
  3161. color: gray;
  3162. font-size: 13px;
  3163. font-weight: initial;
  3164. padding: 4px;
  3165. align-items: center;
  3166. -webkit-box-sizing: border-box;
  3167. -moz-box-sizing: border-box;
  3168. box-sizing: border-box;
  3169. }
  3170. .OJBetter_contextmenu_label_text:hover {
  3171. color: #F44336;
  3172. border: 1px dashed #009688;
  3173. background-color: #ffebcd;
  3174. }
  3175.  
  3176. /* RatingByClist */
  3177. .ratingBadge, html[data-theme=dark] button.ratingBadge{
  3178. display: block;
  3179. font-weight: 700;
  3180. font-size: 11px;
  3181. margin-top: 5px;
  3182. padding: 2px;
  3183. border-radius: 4px;
  3184. color: #B0BEC5;
  3185. border: 1px solid #cccccc66;
  3186. }
  3187.  
  3188. /* 多选翻译 */
  3189. .block_selected{
  3190. box-shadow: 0px 0px 0px 1px #FF9800;
  3191. outline: none;
  3192. }
  3193.  
  3194. /* 悬浮菜单 */
  3195. .OJBetter_MiniTranslateButton {
  3196. z-index: 100;
  3197. display: grid;
  3198. position: absolute;
  3199. border-collapse: collapse;
  3200. fill: #F57C00;
  3201. background-color: #FFF3E0;
  3202. overflow: hidden;
  3203. box-sizing: content-box;
  3204. box-shadow: 0px 0px 0px 2px #FFE0B2;
  3205. border-radius: 100%;
  3206. }
  3207. .OJBetter_MiniTranslateButton:hover {
  3208. cursor: pointer;
  3209. box-shadow: 0px 0px 0px 2px #FFB74D;
  3210. }
  3211.  
  3212. /* acmsguru划分块 */
  3213. .OJBetter_acmsguru {
  3214. margin: 0 0 1em!important;
  3215. }
  3216.  
  3217. /* 代码提交表单 */
  3218. #OJBetter_SubmitForm.input-output-copier:hover {
  3219. background-color: #ffffff00;
  3220. }
  3221. #OJBetter_SubmitForm input[type="number"] {
  3222. width: 40px;
  3223. color: #009688;
  3224. appearance: none;
  3225. border-radius: 6px;
  3226. border-style: solid;
  3227. border: none;
  3228. background-color: #ffffff00;
  3229. }
  3230. #OJBetter_SubmitForm :focus-visible {
  3231. outline: none;
  3232. }
  3233. #OJBetter_SubmitForm .topDiv {
  3234. height: 50px;
  3235. display: flex;
  3236. align-items: center;
  3237. justify-content: space-between;
  3238. padding: 10px 0px;
  3239. box-sizing: border-box;
  3240. }
  3241. #OJBetter_SubmitForm .topDiv .topRightDiv {
  3242. height: 100%;
  3243. display: flex;
  3244. flex-wrap: wrap;
  3245. gap: 0px;
  3246. }
  3247. #OJBetter_SubmitForm input[type="checkbox"], #OJBetter_SubmitForm label {
  3248. margin: 0px;
  3249. font-weight: initial;
  3250. }
  3251. #OJBetter_SubmitForm #fontSizeInput {
  3252. border: none;
  3253. background-color: #ffffff00;
  3254. }
  3255.  
  3256. /* 顶部区域 */
  3257. #OJBetter_SubmitForm .topRightDiv>* {
  3258. height: 100%;
  3259. box-sizing: border-box;
  3260. }
  3261. #OJBetter_SubmitForm .topRightDiv>button{
  3262. padding: 0px 8px;
  3263. }
  3264. #OJBetter_SubmitForm .topRightDiv {
  3265. display: flex;
  3266. flex-wrap: wrap;
  3267. gap: 0px;
  3268. align-items: center;
  3269. }
  3270. #OJBetter_SubmitForm input[type="checkbox"], #OJBetter_SubmitForm label {
  3271. margin: 0px;
  3272. font-weight: initial;
  3273. }
  3274.  
  3275. /* LSP连接Log */
  3276. #LSPLog{
  3277. width: 500px;
  3278. height: 500px;
  3279. position: fixed;
  3280. top: 50%;
  3281. left: 50%;
  3282. padding: 10px;
  3283. transform: translate(-50%, -50%);
  3284. border: 1px solid;
  3285. z-index: 200;
  3286. background-color: #ffffff;
  3287. }
  3288. #LSPLog button{
  3289. position: fixed;
  3290. top: 10px;
  3291. right: 10px;
  3292. z-index: 200;
  3293. }
  3294. #LSPLog #LSPLogList{
  3295. width: 500px;
  3296. height: 500px;
  3297. overflow: auto;
  3298. color: #424242;
  3299. }
  3300. #LSPLog li:nth-child(odd){
  3301. background-color: #f5f5f5;
  3302. }
  3303. #LSPLog details{
  3304. padding: 2px;
  3305. }
  3306.  
  3307. /* 代码编辑器 */
  3308. #OJBetter_editor{
  3309. box-sizing: border-box;
  3310. height: 600px;
  3311. border: 1px solid #d3d3d3;
  3312. width: 100%;
  3313. resize: vertical;
  3314. display: flex;
  3315. flex-direction: column;
  3316. }
  3317. #OJBetter_editor.fullscreen{
  3318. position: fixed;
  3319. top: 0;
  3320. left: 0;
  3321. width: 100%;
  3322. height: 100vh;
  3323. z-index: 2000;
  3324. }
  3325. #OJBetter_editor.bottom{
  3326. position: fixed;
  3327. bottom: 0;
  3328. left: 0;
  3329. width: 100%;
  3330. height: 50vh;
  3331. z-index: 2000;
  3332. }
  3333. .ojb_btn.exit_button_bottom {
  3334. position: fixed;
  3335. bottom: 30px;
  3336. right: 15px;
  3337. z-index: 2000;
  3338. height: 28px;
  3339. }
  3340.  
  3341. /* monaco */
  3342. #OJBetter_monaco {
  3343. flex: 1;
  3344. min-height: 0;
  3345. width: 100%;
  3346. }
  3347. #OJBetter_monaco .highlight {
  3348. border: 1px solid #ffffff00;
  3349. background-color: #ffffff00!important
  3350. }
  3351. .monaco-hover hr {
  3352. margin: 4px -8px 4px !important;
  3353. }
  3354.  
  3355. /* 状态底栏 */
  3356. #OJBetter_statusBar{
  3357. height: 22px;
  3358. font-size: 12px;
  3359. color: #757575;
  3360. border: 1px solid #d3d3d3;
  3361. background-color: #f8f8f8;
  3362. padding: 3px;
  3363. box-sizing: border-box;
  3364. }
  3365.  
  3366. /* 提交 */
  3367. #OJBetter_submitDiv{
  3368. display: flex;
  3369. padding-top: 15px;
  3370. height: 50px;
  3371. box-sizing: border-box;
  3372. }
  3373. #OJBetter_submitDiv >* {
  3374. border-radius: 6px;
  3375. }
  3376. #OJBetter_submitDiv > button {
  3377. height: 100%;
  3378. aspect-ratio: 1 / 1;
  3379. }
  3380. #SubmitButton {
  3381. color: #fff;
  3382. background-color: #209978;
  3383. border-color: #17795E;
  3384. }
  3385. #SubmitButton:hover {
  3386. background-color: #17795e;
  3387. }
  3388. #SubmitButton.disabled {
  3389. background-color: red;
  3390. animation: shake 0.07s infinite alternate;
  3391. }
  3392. #programTypeId{
  3393. height: 100%;
  3394. padding: 5px 10px;
  3395. border-radius: 6px;
  3396. border-style: solid;
  3397. border: 1px solid #ced4da;
  3398. color: #212529;
  3399. }
  3400.  
  3401. /* 调试 */
  3402. .OJBetter_loding{
  3403. padding: 6px 0px 0px 5px;
  3404. height: 22px;
  3405. }
  3406. #CompilerArgsInput{
  3407. flex-grow: 1;
  3408. width: 100%;
  3409. height: 100%;
  3410. margin-bottom: 10px;
  3411. padding: 5px 10px;
  3412. border-radius: 6px;
  3413. box-sizing: border-box;
  3414. border: 1px solid #ccc;
  3415. box-shadow: inset 0px 1px 1px rgba(0,0,0,.075);
  3416. }
  3417. #CompilerArgsInput[disabled] {
  3418. cursor: not-allowed;
  3419. }
  3420. #CompilerSetting{
  3421. font-size: 14px;
  3422. margin-top: 10px;
  3423. display: none;
  3424. }
  3425. #CompilerSetting select, #CompilerSetting textarea{
  3426. padding: 4px 10px;
  3427. border-radius: 6px;
  3428. border-style: solid;
  3429. border: 1px solid #ced4da;
  3430. color: #212529;
  3431. }
  3432. #CompilerBox{
  3433. display: grid;
  3434. margin-top: 10px;
  3435. border: #d0d7de solid 1px;
  3436. border-radius: 6px;
  3437. }
  3438. #CompilerBox > * {
  3439. margin: 5px;
  3440. }
  3441.  
  3442. /* 自定义样例 */
  3443. #customTestBlock {
  3444. margin-top: 10px;
  3445. font-size: 14px;
  3446. color: #616161;
  3447. border: 1px solid #d3d3d3;
  3448. box-sizing: border-box;
  3449. position: relative;
  3450. }
  3451. #customTestBlock #customTests{
  3452. border-top: 1px solid #d3d3d3;
  3453. margin: 0px 0px 40px 0px;
  3454. }
  3455. #customTestBlock summary {
  3456. cursor: pointer;
  3457. padding: 10px;
  3458. }
  3459. #customTestBlock textarea {
  3460. resize: vertical;
  3461. }
  3462. .sampleDiv {
  3463. color: #727378;
  3464. background-color: #FAFAFA;
  3465. padding: 5px;
  3466. margin-bottom: 10px;
  3467. box-shadow: inset 0 0 1px #0000004d;
  3468. position: relative;
  3469. }
  3470. .dynamicTextarea {
  3471. width: 98%;
  3472. height: 120px;
  3473. margin: 10px 5px;
  3474. border: 1px solid #E0E0E0;
  3475. }
  3476. .deleteCustomTest {
  3477. cursor: pointer;
  3478. position: absolute;
  3479. top: 5px;
  3480. right: 5px;
  3481. display: flex;
  3482. fill: #9E9E9E;
  3483. padding: 2px 2px;
  3484. border-radius: 4px;
  3485. border: 1px solid #ffffff00;
  3486. background-color: #ffffff00;
  3487. align-items: center;
  3488. }
  3489. .deleteCustomTest:hover {
  3490. fill: #EF5350;
  3491. border: 1px solid #ef9a9a;
  3492. background-color: #FFEBEE;
  3493. }
  3494. #addCustomTest {
  3495. cursor: pointer;
  3496. position: absolute;
  3497. bottom: 5px;
  3498. right: 5px;
  3499. padding: 3px 10px;
  3500. color: #795548;
  3501. border: 1px solid #ccc;
  3502. border-radius: 4px;
  3503. background-color: #FAFAFA;
  3504. }
  3505. #addCustomTest:hover {
  3506. background-color: #f5f5f5;
  3507. }
  3508.  
  3509. /* 调试结果 */
  3510. #statePanel{
  3511. display: none;
  3512. padding: 5px;
  3513. margin-top: 10px;
  3514. border: 1px solid #ddd;
  3515. border-radius: 4px;
  3516. }
  3517. .test-case {
  3518. padding: 10px;
  3519. border: 1px solid #ddd;
  3520. border-radius: 4px;
  3521. }
  3522. .test-case:not(:first-child){
  3523. margin-top: 5px;
  3524. }
  3525. .test-case > * {
  3526. margin: 5px 0px;
  3527. }
  3528. .test-case > :first-child {
  3529. margin-top: 0px;
  3530. }
  3531. .test-case > :last-child {
  3532. margin-bottom: 0px;
  3533. }
  3534. .test-case-title, .test-case-status {
  3535. font-size: 16px;
  3536. display: inline;
  3537. }
  3538. .test-case-status{
  3539. margin-left: 5px;
  3540. }
  3541. .test-case-status.error{
  3542. color: red;
  3543. }
  3544. .test-case-status.success{
  3545. color: #449d44;
  3546. }
  3547. .test-case-judge {
  3548. font-size: 13px;
  3549. }
  3550.  
  3551. /* 差异对比 */
  3552. .output_diff {
  3553. color: #5d4037;
  3554. margin: 5px 0px;
  3555. display: grid;
  3556. border: 1px solid #bcaaa4;
  3557. font-size: 13px;
  3558. font-family: Consolas, "Lucida Console", "Andale Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
  3559. overflow: auto;
  3560. }
  3561. .output_diff .added {
  3562. background-color: #c8f7c5;
  3563. user-select: none;
  3564. }
  3565. .output_diff .removed {
  3566. background-color: #f7c5c5;
  3567. }
  3568. .output_diff .diffLine {
  3569. display: flex;
  3570. }
  3571. .output_diff .diffLine:nth-child(odd) {
  3572. background-color: #f5f5f5;
  3573. }
  3574. .lineNo {
  3575. display: flex;
  3576. align-items: center;
  3577. justify-content: center;
  3578. width: 17px;
  3579. color: #BDBDBD;
  3580. font-size: 10px;
  3581. border-right: 1px solid;
  3582. user-select: none;
  3583. }
  3584. .lineContent {
  3585. display: grid;
  3586. width: 100%;
  3587. }
  3588. .lineContent>span {
  3589. height: 16px;
  3590. padding-left: 3px;
  3591. }
  3592. .output_no_diff {
  3593. padding: 5px;
  3594. border: 1px solid #ddd;
  3595. }
  3596. .diff_note {
  3597. font-size: 10px;
  3598. }
  3599.  
  3600. /* 网站本地化替换规则标记 */
  3601. .markingTextReplaceRule{
  3602. color: #FFF3E0;
  3603. background-color: #FF9800;
  3604. }
  3605.  
  3606. /* SelectPage样式 */
  3607. .sp_input {
  3608. padding: 4px 6px px !important;
  3609. height: 20px !important;
  3610. min-height: 20px !important;
  3611. line-height: 20px !important;
  3612. }
  3613. div.sp_clear_btn {
  3614. padding: 0px !important;
  3615. }
  3616.  
  3617. /* 移动设备 */
  3618. @media (max-device-width: 450px) {
  3619. .ojb_btn{
  3620. height: 2em;
  3621. font-size: 1.2em;
  3622. }
  3623. .ojb_btn.OJBetter_setting{
  3624. height: 2.5em;
  3625. font-size: 1em;
  3626. }
  3627. .OJBetter_setting_menu{
  3628. width: 90%;
  3629. }
  3630. .OJBetter_setting_menu label, #darkMode_span, #loaded_span, .OJBetter_setting_menu_label_text,
  3631. .OJBetter_setting_sidebar li{
  3632. font-size: 1em;
  3633. }
  3634. .translate-problem-statement{
  3635. font-size: 1.2em;
  3636. }
  3637. .OJBetter_modal{
  3638. font-size: 1.5em;
  3639. }
  3640. .OJBetter_setting_list, .translate-problem-statement{
  3641. padding: 0.5em;
  3642. }
  3643. .OJBetter_setting_menu_label_text{
  3644. height: 2.5em;
  3645. padding: 0.5em;
  3646. }
  3647. #pagBar #jump-input, #pagBar #items-per-page, .OJBetter_modal button{
  3648. height: 2.5em;
  3649. font-size: 1em;
  3650. }
  3651. .translate-problem-statement p, .translate-problem-statement ul li{
  3652. line-height: 1.5em !important;
  3653. }
  3654. .OJBetter_contextmenu_label_text{
  3655. height: 3em;
  3656. font-size: 1em;
  3657. }
  3658. }
  3659.  
  3660. /* 覆盖网站原本的样式 */
  3661. #footer > div:nth-child(7) {
  3662. left: 0px !important;
  3663. }
  3664. `);
  3665.  
  3666. /**
  3667. * 添加一些依赖库和条件加载的css样式
  3668. */
  3669. function addDependencyStyles() {
  3670. GM_addStyle(GM_getResourceText("xtermcss"));
  3671. GM_addStyle(GM_getResourceText("selectpagecss"));
  3672. // 自定义图标大小
  3673. GM_addStyle(`
  3674. .iconfont {
  3675. font-size: ${OJBetter.preference.iconButtonSize}px;
  3676. }
  3677. `);
  3678. }
  3679.  
  3680. /**
  3681. * 添加包含i18n内容的css样式
  3682. */
  3683. function addI18nStyles() {
  3684. GM_addStyle(`
  3685. /* 加载鼠标悬浮覆盖层css */
  3686. .overlay::before {
  3687. content: '';
  3688. position: absolute;
  3689. top: 0;
  3690. left: 0;
  3691. width: 100%;
  3692. height: 100%;
  3693. 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);
  3694. z-index: 100;
  3695. }
  3696. .overlay::after {
  3697. content: '${i18next.t('targetArea', { ns: 'common' })}';
  3698. position: absolute;
  3699. top: 50%;
  3700. left: 50%;
  3701. transform: translate(-50%, -50%);
  3702. color: #00695C;
  3703. font-size: 16px;
  3704. font-weight: bold;
  3705. z-index: 100;
  3706. }
  3707.  
  3708. .config::before {
  3709. content: "${i18next.t('common.configManageTitle', { ns: 'settings' })}";
  3710. display: block;
  3711. height: 20px;
  3712. background-color: #f0f8ff;
  3713. border: 1px solid #c5cae9;
  3714. border-bottom: 0px;
  3715. line-height: 20px;
  3716. padding: 2px 10px;
  3717. border-radius: 8px 8px 0px 0px;
  3718. box-sizing: content-box;
  3719. }
  3720. .config.missing::before {
  3721. content: "${i18next.t('common.missing.radio', { ns: 'settings' })}";
  3722. background-color: #fef0f0;
  3723. color: #f56c6c;
  3724. border: 1px solid #fab6b6;
  3725. }
  3726. `);
  3727. }
  3728.  
  3729. // ------------------------------
  3730. // 一些工具类
  3731. // ------------------------------
  3732.  
  3733. /**
  3734. * 自定义错误类,以区分不同的错误类型
  3735. */
  3736. class OJB_GMError extends Error {
  3737. constructor(type, message, originalError) {
  3738. super(message);
  3739. this.name = 'GMError';
  3740. this.type = type;
  3741. this.stack = originalError.stack;
  3742. Object.assign(this, originalError);
  3743. }
  3744. }
  3745.  
  3746. /**
  3747. * 文本块替换/恢复类
  3748. */
  3749. class TextBlockReplacer {
  3750. constructor() {
  3751. /** @type {string[]} 匹配项 */
  3752. this.matches = [];
  3753. /** @type {Map<string, string>} 待还原项 */
  3754. this.replacements = new Map();
  3755. /** @type {Map<string, string>} 暂时未找到的待还原项 */
  3756. this.tempReplacements = new Map();
  3757. /** @type {string} 替换符号 */
  3758. this.replaceSymbol = OJBetter.translation.replaceSymbol;
  3759. }
  3760.  
  3761. /**
  3762. * 替换文本
  3763. * @param {string} text 原文本
  3764. * @param {RegExp} regex 匹配规则
  3765. * @returns {string} 替换后的文本
  3766. */
  3767. replace(text, regex) {
  3768. this.matches = text.match(regex) || [];
  3769. try {
  3770. for (let i = 0; i < this.matches.length; i++) {
  3771. const match = this.matches[i];
  3772. const id = OJB_getRandomNumber(8);
  3773. let replacement = '';
  3774. switch (this.replaceSymbol) {
  3775. case "1":
  3776. replacement = `【${id}】`;
  3777. break;
  3778. case "2":
  3779. replacement = `{${id}}`;
  3780. break;
  3781. case "3":
  3782. replacement = `[${id}]`;
  3783. break;
  3784. default:
  3785. replacement = `【${id}】`;
  3786. break;
  3787. }
  3788. text = text.replace(match, replacement);
  3789. this.replacements.set(id, match);
  3790. }
  3791. } catch (e) { }
  3792. return text;
  3793. }
  3794.  
  3795.  
  3796. /**
  3797. * 恢复替换的文本
  3798. * @param {string} text 还原前的文本
  3799. * @returns {string} 还原后的文本
  3800. */
  3801. recover(text) {
  3802. let textCopy = text;
  3803.  
  3804. /**
  3805. * 替换文本
  3806. * @param {string} replacement 替换的文本
  3807. * @param {string} regexPattern 匹配规则
  3808. * @returns {void}
  3809. */
  3810. const replaceText = (replacement, regexPattern) => {
  3811. const latexMatch = '(?<latex_block>\\$\\$(\\\\.|[^\\$])*?\\$\\$)|(?<latex_inline>\\$(\\\\.|[^\\$])*?\\$)|';
  3812. const regex = new RegExp(latexMatch + regexPattern, 'g');
  3813. textCopy = textCopy.replace(regex, (match, ...args) => {
  3814. // LaTeX中的不替换
  3815. const groups = args[args.length - 1]; // groups是replace方法的最后一个参数
  3816. if (groups.latex_block || groups.latex_inline) return match;
  3817. // 没有空格则加一个
  3818. const offset = args[args.length - 3]; // offset是replace方法的倒数第三个参数
  3819. let leftSpace = "", rightSpace = "";
  3820. if (!/\s/.test(textCopy[offset - 1])) leftSpace = " ";
  3821. if (!/\s/.test(textCopy[offset + match.length])) rightSpace = " ";
  3822. return leftSpace + replacement + rightSpace;
  3823. });
  3824. };
  3825.  
  3826. /**
  3827. * 尝试还原
  3828. * @param {string} replacement 替换的文本
  3829. * @param {string} id 替换的 id
  3830. * @returns {boolean} 是否替换成功
  3831. */
  3832. const tryRecover = (replacement, id) => {
  3833. // 尝试还原,如果还原成功,则从 replacements 中删除
  3834. const originalText = textCopy;
  3835. replaceText(replacement, `【\\s*${id}\\s*】|\\[\\s*${id}\\s*\\]|{\\s*${id}\\s*}`); // 替换符完整匹配(考虑了多出空格的情况)
  3836. replaceText(replacement, `【\\s*${id}(?![】\\d])|(?<![【\\d])${id}\\s*】|\\[\\s*${id}(?![\\]\\d])|(?<![\\[\\d])${id}\\s*\\]|{\\s*${id}(?![}\\d])|(?<![{\\d])${id}\\s*}`); // 替换符部分匹配
  3837.  
  3838. if (textCopy === originalText) {
  3839. // 如果文本没有变化,说明没有找到,加入到 tempReplacements
  3840. this.tempReplacements.set(id, replacement);
  3841. return false;
  3842. } else {
  3843. // 如果文本变化了,说明找到并成功替换,则删除
  3844. this.replacements.delete(id);
  3845. this.tempReplacements.delete(id);
  3846. return true;
  3847. }
  3848. }
  3849.  
  3850. // 处理 replacements 中的项
  3851. this.replacements.forEach((replacement, id) => {
  3852. tryRecover(replacement, id);
  3853. });
  3854.  
  3855. // 处理 tempReplacements 中的项
  3856. while (this.tempReplacements.size > 0) {
  3857. let found = false;
  3858. this.tempReplacements.forEach((replacement, id) => {
  3859. found = tryRecover(replacement, id) || found;
  3860. });
  3861. if (!found) break; // 如果这一轮没有找到任何项,终止循环
  3862. }
  3863.  
  3864. // 如果 tempReplacements 还有未找到的项
  3865. if (this.tempReplacements.size > 0) {
  3866. console.warn("There are still some replacements not found:", this.tempReplacements);
  3867. }
  3868.  
  3869. return textCopy;
  3870. }
  3871. }
  3872.  
  3873. // ------------------------------
  3874. // 一些工具函数
  3875. // ------------------------------
  3876.  
  3877. /**
  3878. * 格式化链接格式
  3879. * @param {string} url 链接字符串
  3880. * @returns {string} 清理后的链接字符串
  3881. */
  3882. function OJB_cleanLink(url) {
  3883. if (url === null || url === undefined) return "";
  3884.  
  3885. // 替换'http://'为'https://'
  3886. let cleanUrl = url.replace(/^http:\/\//i, 'https://');
  3887.  
  3888. // 移除末尾的斜杠
  3889. cleanUrl = cleanUrl.replace(/\/$/, '');
  3890.  
  3891. return cleanUrl;
  3892. }
  3893.  
  3894. /**
  3895. * 深度比较两个对象或数组是否完全相等。
  3896. * @param {any} a - 第一个比较对象。
  3897. * @param {any} b - 第二个比较对象。
  3898. * @returns {boolean} - 如果两个对象或数组深度相等,则返回true,否则返回false。
  3899. */
  3900. function OJB_deepEquals(a, b) {
  3901. if (a === b) return true;
  3902. if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false;
  3903. const keysA = Object.keys(a);
  3904. const keysB = Object.keys(b);
  3905. if (keysA.length !== keysB.length) return false;
  3906. for (let key of keysA) {
  3907. if (!b.hasOwnProperty(key)) return false;
  3908. if (!OJB_deepEquals(a[key], b[key])) return false;
  3909. }
  3910. return true;
  3911. }
  3912.  
  3913. /**
  3914. * 用于封装需要重试的异步函数
  3915. * @param {Function} task 需要封装的异步函数
  3916. * @param {Object} options 配置项
  3917. * @param {Number} options.maxRetries 重试次数,默认为 5
  3918. * @param {Number} options.retryInterval 重试时间间隔,默认为 0 毫秒
  3919. * @param {Function} options.errorHandler 错误处理函数,默认为抛出错误
  3920. * @param {...any} args task 函数的参数
  3921. * @returns {Promise} 返回 Promise
  3922. */
  3923. async function OJB_promiseRetryWrapper(task, {
  3924. maxRetries = 5,
  3925. retryInterval = 0,
  3926. errorHandler = (err) => { throw err }
  3927. } = {}, ...args) {
  3928. let attemptsLeft = maxRetries;
  3929. while (attemptsLeft--) {
  3930. try {
  3931. return await task(...args);
  3932. } catch (err) {
  3933. if (attemptsLeft <= 0) {
  3934. return errorHandler(err, maxRetries, attemptsLeft);
  3935. }
  3936. if (retryInterval > 0) {
  3937. await OJB_delay(retryInterval);
  3938. }
  3939. }
  3940. }
  3941. }
  3942.  
  3943. /**
  3944. * GM_xmlhttpRequest 的 Promise 封装
  3945. * @param {Object} options GM_xmlhttpRequest 的参数
  3946. * @param {Boolean} isStream 是否为流式请求
  3947. * @returns {Promise<OJB_GMError>} 返回 Promise
  3948. */
  3949. function OJB_GMRequest(options, isStream = false) {
  3950. return new Promise((resolve, reject) => {
  3951. GM_xmlhttpRequest({
  3952. ...options,
  3953. ...(isStream ? {
  3954. onloadstart: resolve
  3955. } : {
  3956. onload: resolve
  3957. }),
  3958. onerror: (error) => reject(new OJB_GMError('error', 'An error occurred during the request.', error)),
  3959. ontimeout: (error) => reject(new OJB_GMError('timeout', 'The request timed out.', error)),
  3960. onabort: (error) => reject(new OJB_GMError('abort', 'The request was aborted.', error)),
  3961. });
  3962. });
  3963. }
  3964.  
  3965. /**
  3966. * 获取cookie
  3967. * @param {string} name cookie名称
  3968. * @returns {string} cookie值
  3969. */
  3970. function OJB_getCookie(name) {
  3971. const cookies = document.cookie.split(";");
  3972. for (let i = 0; i < cookies.length; i++) {
  3973. const cookie = cookies[i].trim();
  3974. const [cookieName, cookieValue] = cookie.split("=");
  3975.  
  3976. if (cookieName === name) {
  3977. return decodeURIComponent(cookieValue);
  3978. }
  3979. }
  3980. return "";
  3981. }
  3982.  
  3983. /**
  3984. * 检查是否仍在同一浏览器会话中
  3985. * @param {string} sessionKey - 会话键名,用于标识会话
  3986. * @returns {boolean} - 如果在当前会话中之前已经设置过这个键,则返回true,否则返回false
  3987. */
  3988. function OJB_isSameBrowserSession(sessionKey) {
  3989. const fullCookieName = `OJB_Session_${sessionKey}`;
  3990. const sessionValue = OJB_getCookie(fullCookieName);
  3991. if (sessionValue === "") {
  3992. document.cookie = `${fullCookieName}=true; path=/`;
  3993. return false;
  3994. }
  3995. return true;
  3996. }
  3997.  
  3998. /**
  3999. * 随机数生成
  4000. * @param {number} numDigits 位数
  4001. * @returns {number}
  4002. */
  4003. function OJB_getRandomNumber(numDigits) {
  4004. let min = Math.pow(10, numDigits - 1);
  4005. let max = Math.pow(10, numDigits) - 1;
  4006. return Math.floor(Math.random() * (max - min + 1)) + min;
  4007. }
  4008.  
  4009. /**
  4010. * 防抖函数
  4011. * @param {Function} callback 回调函数
  4012. * @returns {Function}
  4013. */
  4014. function OJB_debounce(callback) {
  4015. let timer;
  4016. let immediateExecuted = false;
  4017. const delay = 500;
  4018. return function () {
  4019. clearTimeout(timer);
  4020. if (!immediateExecuted) { callback.call(this); immediateExecuted = true; }
  4021. timer = setTimeout(() => { immediateExecuted = false; }, delay);
  4022. };
  4023. }
  4024.  
  4025. /**
  4026. * 为元素添加鼠标拖拽支持
  4027. * @param {JQuery<HTMLElement>} element 要添加拖拽支持的元素
  4028. * @returns {void}
  4029. */
  4030. function OJB_addDraggable(element) {
  4031. let isDragging = false;
  4032. let x, y, l, t, nl, nt;
  4033. let isSpecialMouseDown = false; // 选取某些元素时不拖动
  4034.  
  4035. element.on('mousedown', function (e) {
  4036. isSpecialMouseDown = $(e.target).is('label, p, input, textarea, span, select, details, summary');
  4037. if (isSpecialMouseDown) return;
  4038.  
  4039. isDragging = true;
  4040. x = e.clientX;
  4041. y = e.clientY;
  4042. l = element.offset().left - $(window).scrollLeft();
  4043. t = element.offset().top - $(window).scrollTop();
  4044.  
  4045. element.css({ left: l + 'px', top: t + 'px', transform: 'none' });
  4046.  
  4047. $(document).on("mousemove", drag);
  4048. $(document).on("mouseup", stopDrag);
  4049. element.css('cursor', 'all-scroll');
  4050. });
  4051.  
  4052. const drag = (e) => {
  4053. if (!isDragging) return;
  4054. // 不执行拖动操作
  4055. if ($(e.target).is('label, p, input, textarea, span') || isSpecialMouseDown && !$(e.target).is('input, textarea')) return;
  4056. e.preventDefault();
  4057.  
  4058. const nx = e.clientX;
  4059. const ny = e.clientY;
  4060. nl = nx - (x - l);
  4061. nt = ny - (y - t);
  4062. element.css({ transform: `translate(${nx - x}px, ${ny - y}px)` });
  4063. };
  4064.  
  4065. const stopDrag = () => {
  4066. isDragging = false;
  4067. isSpecialMouseDown = false;
  4068. element.css('cursor', 'default');
  4069.  
  4070. // 在停止拖拽后,设置元素的left和top,并还原transform
  4071. element.css({ left: nl + 'px', top: nt + 'px', transform: 'none' });
  4072. $(document).off("mousemove", drag);
  4073. $(document).off("mouseup", stopDrag);
  4074. };
  4075. }
  4076.  
  4077. /**
  4078. * 切换元素的折叠/展开过渡动画
  4079. * @param {HTMLElement} element
  4080. */
  4081. function OJB_toggleCollapseExpand(element) {
  4082. // 设置transitionend事件监听器的函数
  4083. const setTransitionListener = (listener) => {
  4084. const listenerName = `transitionEndListener${Date.now()}`;
  4085. window[listenerName] = listener;
  4086. element.addEventListener('transitionend', listener);
  4087. element.setAttribute('data-transition-end-listener', listenerName);
  4088. };
  4089.  
  4090. // 移除事件监听器的函数
  4091. const removeTransitionListener = () => {
  4092. const transitionEndListenerName = element.getAttribute('data-transition-end-listener');
  4093. if (transitionEndListenerName) {
  4094. element.removeEventListener('transitionend', window[transitionEndListenerName]);
  4095. element.removeAttribute('data-transition-end-listener');
  4096. }
  4097. };
  4098.  
  4099. const collapsed = element.getAttribute('data-collapsed') === 'true';
  4100. const sectionHeight = element.scrollHeight;
  4101.  
  4102. // 移除事件监听器
  4103. removeTransitionListener();
  4104.  
  4105. // 设置初始样式
  4106. element.style.overflow = 'hidden';
  4107. element.style.transition = 'height 0.3s ease-out 0s';
  4108. element.style.height = collapsed ? `0px` : `${sectionHeight}px`;
  4109. element.style.opacity = collapsed ? '' : '1';
  4110.  
  4111. // 需要立即开始动画
  4112. requestAnimationFrame(() => {
  4113. // 设置结束样式
  4114. element.style.height = collapsed ? `${sectionHeight}px` : `0px`;
  4115. });
  4116.  
  4117. const transitionEndListener = (event) => {
  4118. if (event.propertyName === 'height') {
  4119. if (collapsed) {
  4120. // 展开后的设置
  4121. element.style.height = '';
  4122. element.style.overflow = '';
  4123. } else {
  4124. // 折叠后的设置
  4125. element.style.opacity = '0';
  4126. }
  4127. removeTransitionListener();
  4128. }
  4129. };
  4130.  
  4131. setTransitionListener(transitionEndListener);
  4132.  
  4133. // 更新data-collapsed属性
  4134. element.setAttribute('data-collapsed', collapsed ? 'false' : 'true');
  4135. }
  4136.  
  4137. /**
  4138. * 获取外部JSON并转换为Object
  4139. * @param {string} url JSON Url
  4140. * @param {boolean} [nacache=true] 是否不使用缓存
  4141. * @returns {Promise<Object>} JSON Object
  4142. */
  4143. async function OJB_getExternalJSON(url, nacache = true) {
  4144. const response = await OJB_GMRequest({
  4145. method: "GET",
  4146. url: url,
  4147. nocache: nacache
  4148. });
  4149. try {
  4150. return JSON.parse(response.responseText);
  4151. } catch (e) {
  4152. throw new Error(`JSON parse error\n${e}`);
  4153. }
  4154. }
  4155.  
  4156. /**
  4157. * 创建确认对话框dialog
  4158. * @param {string} title 标题
  4159. * @param {string} content 内容
  4160. * @param {string[]} buttons 按钮 (取消 确定) 可以为null
  4161. * @param {boolean} renderMarkdown 是否使用markdown渲染文本
  4162. * @returns {Promise<boolean>} 用户点击了确定按钮返回true, 否则返回false
  4163. */
  4164. function OJB_createDialog(title, content, buttons, renderMarkdown = false) {
  4165. return new Promise(resolve => {
  4166. let contentHtml = content;
  4167.  
  4168. if (renderMarkdown) {
  4169. const md = window.markdownit();
  4170. contentHtml = md.render(content);
  4171. }
  4172.  
  4173. const dialog = OJB_safeCreateJQElement(`
  4174. <dialog class="OJBetter_modal">
  4175. <h2>${title}</h2>
  4176. <div class="content">${contentHtml}</div>
  4177. </dialog>
  4178. `);
  4179. const buttonbox = OJB_safeCreateJQElement(`<div class="buttons"></div>`);
  4180. const cancelButton = OJB_safeCreateJQElement(`<button class="cancelButton">${buttons[0]}</button>`)
  4181. .addClass("secondary");
  4182. const continueButton = OJB_safeCreateJQElement(`<button class="continueButton">${buttons[1]}</button>`);
  4183. if (buttons[0] !== null) buttonbox.append(cancelButton);
  4184. if (buttons[1] !== null) buttonbox.append(continueButton);
  4185. dialog.append(buttonbox);
  4186. $('body').before(dialog);
  4187.  
  4188. OJB_showModal(dialog);
  4189. OJB_addDraggable(dialog);
  4190.  
  4191. continueButton.click(function () {
  4192. OJB_closeAndRemoveModal(dialog);
  4193. resolve(true);
  4194. });
  4195.  
  4196. cancelButton.click(function () {
  4197. OJB_closeAndRemoveModal(dialog);
  4198. resolve(false);
  4199. });
  4200. });
  4201. }
  4202.  
  4203. /**
  4204. * 显示模态对话框并阻止页面滚动,同时考虑滚动条宽度变化和原始marginRight
  4205. * @param {JQuery<HTMLElement>} element
  4206. */
  4207. function OJB_showModal(element) {
  4208. const dialog = element.get(0);
  4209. dialog.showModal();
  4210. OJBetter.state.openDialogCount++;
  4211.  
  4212. if (OJBetter.state.openDialogCount === 1) {
  4213. const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
  4214. // 获取原始的html marginRight,考虑到可能的非数字值,比如auto
  4215. const originalMarginRight = window.getComputedStyle(document.documentElement).marginRight;
  4216. const marginRightValue = parseFloat(originalMarginRight) || 0; // 将非数字值转换为0
  4217.  
  4218. if (scrollbarWidth > 0) {
  4219. // 保存原始的marginRight,并设置新的值以补偿滚动条宽度
  4220. document.documentElement.style.setProperty('--original-margin-right', originalMarginRight);
  4221. document.documentElement.style.marginRight = `${marginRightValue + scrollbarWidth}px`;
  4222. }
  4223.  
  4224. // 保存原始的overflow样式
  4225. document.documentElement.setAttribute('data-original-overflow', document.documentElement.style.overflow);
  4226. document.documentElement.style.overflow = 'hidden';
  4227. }
  4228.  
  4229. const allowScrollIfNeeded = () => {
  4230. OJBetter.state.openDialogCount--;
  4231. if (OJBetter.state.openDialogCount === 0) {
  4232. // 恢复原始的html marginRight和overflow样式
  4233. const originalMarginRight = document.documentElement.style.getPropertyValue('--original-margin-right');
  4234. document.documentElement.style.marginRight = originalMarginRight;
  4235. document.documentElement.style.removeProperty('--original-margin-right');
  4236.  
  4237. const originalOverflow = document.documentElement.getAttribute('data-original-overflow');
  4238. document.documentElement.style.overflow = originalOverflow;
  4239. document.documentElement.removeAttribute('data-original-overflow');
  4240. }
  4241. };
  4242.  
  4243. dialog.addEventListener('close', allowScrollIfNeeded);
  4244. }
  4245.  
  4246. /**
  4247. * 关闭并移除模态对话框
  4248. * @param {JQuery<HTMLElement>} element
  4249. */
  4250. function OJB_closeAndRemoveModal(element) {
  4251. const dialog = element.get(0);
  4252. dialog.close();
  4253. dialog.remove();
  4254. }
  4255.  
  4256. /**
  4257. * 关闭并移除模态对话框
  4258. * @param {JQuery<HTMLElement>} element
  4259. */
  4260. function OJB_closeModal(element) {
  4261. const dialog = element.get(0);
  4262. dialog.close();
  4263. }
  4264.  
  4265. /**
  4266. * 清除i18next的缓存数据并刷新
  4267. */
  4268. function clearI18nextCache() {
  4269. Object.keys(localStorage)
  4270. .filter(key => key.startsWith('i18next_res_'))
  4271. .forEach(key => localStorage.removeItem(key));
  4272. window.location.reload();
  4273. }
  4274.  
  4275. /**
  4276. * 清除网站本地化数据
  4277. */
  4278. async function clearWebsiteL10nData() {
  4279. OJBetter.common.database.localizeSubsData.clear().then(() => {
  4280. console.log('localizeSubsData table has been cleared');
  4281. window.location.reload();
  4282. }).catch((error) => {
  4283. console.error('Failed to clear localizeSubsData table:', error);
  4284. });
  4285. }
  4286.  
  4287. /**
  4288. * 从Pre代码块中获取原始代码
  4289. * @param {HTMLElement} element pre代码块元素
  4290. * @returns {string|null} 代码文本
  4291. */
  4292. function OJB_getCodeFromPre(element) {
  4293. /**
  4294. * 从Ace格式化的代码块中获取原始代码
  4295. * @param {HTMLElement} element pre代码块元素
  4296. * @returns {string} 代码文本
  4297. */
  4298. const getCodeFromAcePre = function (element) {
  4299. const editor = ace.edit(element);
  4300. return editor.getValue();
  4301. }
  4302.  
  4303. /**
  4304. * 从Pretty格式化的代码块中获取原始代码-1
  4305. * 代码直接存放在 pre 元素中
  4306. * @param {HTMLElement} element pre代码块元素
  4307. * @returns {string} 代码文本
  4308. */
  4309. const getCodeFromPrettyPre = function (element) {
  4310. return Array.from(element.querySelectorAll('li')).map(function (li) {
  4311. return li.textContent;
  4312. }).join('\n');
  4313. }
  4314.  
  4315. /**
  4316. * 从Pretty格式化的代码块中获取原始代码-2
  4317. * 代码存放在子元素 code 中
  4318. * @param {HTMLElement} element pre代码块元素
  4319. * @returns {string} 代码文本
  4320. */
  4321. const getCodeFromPreChild = function (element) {
  4322. const code = element.querySelector("code.prettyprint");
  4323. if (code.classList.contains("linenums")) {
  4324. return getCodeFromPrettyPre(element);
  4325. } else {
  4326. return element.querySelector("code.prettyprint").textContent;
  4327. }
  4328. }
  4329.  
  4330. if (element.id === "submission-code") {
  4331. return getCodeFromAcePre(element);
  4332. } else if (element.classList.contains("prettyprint")) {
  4333. return getCodeFromPrettyPre(element);
  4334. } else if (element.querySelector("code.prettyprint")) {
  4335. return getCodeFromPreChild(element);
  4336. } else {
  4337. return null;
  4338. }
  4339. }
  4340.  
  4341. /**
  4342. * 判断代码的语言
  4343. * @param {string} code 代码文本
  4344. * @returns {string} 可能的语言
  4345. */
  4346. function OJB_codeLangDetect(code) {
  4347. result = hljs.highlightAuto(code);
  4348. return result.language;
  4349. }
  4350.  
  4351. /**
  4352. * 获取指定命名空间下的所有i18n翻译键值对。
  4353. *
  4354. * @param {string} namespace - 要获取键值对的i18next命名空间。
  4355. * @returns {Map<string, string>} 一个包含命名空间下所有键值对的Map对象。
  4356. */
  4357. function OJB_getAllI18nKeysForNamespace(namespace) {
  4358. const language = i18next.language; // 获取当前语言
  4359. const resources = i18next.store.data[language]; // 获取当前语言的所有资源
  4360. const nsResources = resources[namespace]; // 获取特定命名空间的资源
  4361. const resultMap = new Map();
  4362.  
  4363. if (nsResources) {
  4364. // 遍历命名空间下的所有键值对,并添加到Map中
  4365. Object.keys(nsResources).forEach(key => {
  4366. resultMap.set(key, nsResources[key]);
  4367. });
  4368. } else {
  4369. console.log(`No resources found for namespace "${namespace}"`);
  4370. }
  4371.  
  4372. return resultMap;
  4373. }
  4374.  
  4375. /**
  4376. * 更新检查
  4377. */
  4378. async function checkScriptVersion() {
  4379. try {
  4380. const versionResponse = await OJB_GMRequest({
  4381. method: "GET",
  4382. url: "https://aowuucdn.oss-accelerate.aliyuncs.com/script/versions.json",
  4383. timeout: 10 * 1e3,
  4384. nocache: true
  4385. });
  4386. const versionData = JSON.parse(versionResponse.responseText);
  4387. const { [OJBetter.state.formatName]: { dev: version_dev, release: version_release } } = versionData;
  4388. const baseUrls = {
  4389. greasyfork: 'https://update.greasyfork.org/scripts/465777/Codeforces%20Better%21.user.js',
  4390. github: `https://github.com/beijixiaohu/OJBetter/raw/main/script/${OJBetter.about.updateChannel}/${OJBetter.state.formatName}.user.js`,
  4391. aliyunoss: `https://aowuucdn.oss-accelerate.aliyuncs.com/script/${OJBetter.about.updateChannel}/${OJBetter.state.formatName}.user.js`
  4392. };
  4393. /** @type {string} 更新跳转url */
  4394. const updateUrl = baseUrls[OJBetter.about.updateSource];
  4395. /** @type {string} 是否暂时跳过cookie */
  4396. const skipUpdate = OJB_getCookie("skipUpdate");
  4397. /** @type {string} 当前更新频道的最新版本 */
  4398. const version = OJBetter.about.updateChannel == "release" ? version_release : version_dev;
  4399. if (OJB_compareVersions(version, OJBetter.state.version) === 1 && skipUpdate !== "true") {
  4400. const updateConfirmed = await OJB_createDialog(
  4401. i18next.t('update.title', { ns: 'dialog', scriptName: OJBetter.state.name }),
  4402. i18next.t('update.content', { ns: 'dialog', oldVersion: OJBetter.state.version, newVersion: version }),
  4403. [
  4404. i18next.t('update.buttons.0', { ns: 'dialog' }),
  4405. i18next.t('update.buttons.1', { ns: 'dialog' })
  4406. ],
  4407. true
  4408. );
  4409.  
  4410. if (updateConfirmed) {
  4411. window.location.href = updateUrl;
  4412. } else {
  4413. document.cookie = "skipUpdate=true; path=/";
  4414. }
  4415. }
  4416. } catch (error) {
  4417. console.error("Update check failed: ", error);
  4418. }
  4419. }
  4420.  
  4421. /**
  4422. * 公告
  4423. */
  4424. async function showAnnounce() {
  4425. /** @type {string} 最新公告版本*/
  4426. const lastAnnounceVer = i18next.t('lastVersion', { ns: 'announce' });
  4427. if (OJB_compareVersions(OJBetter.state.version, OJBetter.state.lastAnnounceVer) === 1) {
  4428. const title = `🎉${i18next.t('announce.title', { ns: 'dialog' })} ${OJBetter.state.version}`;
  4429. /** @type {Boolean} 是否是新的公告 */
  4430. const isNewAnnounceVer = OJB_compareVersions(lastAnnounceVer, OJBetter.state.lastReadAnnounceVer) === 1;
  4431. /** @type {Boolean} 是否展示新的公告(高于当前版本的测试公告不展示) */
  4432. const showNewAnnounceVer = OJB_compareVersions(lastAnnounceVer, OJBetter.state.version) !== 1;
  4433. /**
  4434. * 获取公告的内容
  4435. * @returns {string} 公告内容
  4436. */
  4437. const getAnnounceContent = function () {
  4438. // 获取公告
  4439. const announceMap = OJB_getAllI18nKeysForNamespace('announce');
  4440. // 移除 'lastVersion' 键
  4441. announceMap.delete('lastVersion');
  4442. // 将 Map 转换为数组并根据版本号排序
  4443. const sortedVersions = [...announceMap.keys()].sort(OJB_compareVersions).reverse();
  4444. let content = "";
  4445. sortedVersions.forEach(version => {
  4446. content += `### ${version}\n\n`; // 使用版本号作为标题
  4447. content += announceMap.get(version); // 添加对应版本的公告内容
  4448. content += "\n\n";
  4449. });
  4450.  
  4451. return content;
  4452. };
  4453.  
  4454. const content = (() => {
  4455. if (isNewAnnounceVer && showNewAnnounceVer) {
  4456. return `${i18next.t('announce.prefix', { ns: 'dialog' })}\n\n${getAnnounceContent()}`;
  4457. } else {
  4458. return i18next.t('announce.divContent', { ns: 'dialog' });
  4459. }
  4460. })();
  4461. const ok = await OJB_createDialog(
  4462. title,
  4463. content,
  4464. [
  4465. null,
  4466. i18next.t('announce.buttons.0', { ns: 'dialog' })
  4467. ],
  4468. true
  4469. ); //跳过折叠块确认
  4470. if (ok) {
  4471. if (isNewAnnounceVer && showNewAnnounceVer) {
  4472. GM_setValue('lastReadAnnounceVer', lastAnnounceVer);
  4473. }
  4474. GM_setValue('lastAnnounceVer', OJBetter.state.version);
  4475. }
  4476. }
  4477. };
  4478.  
  4479. /**
  4480. * 页面顶部提示信息alert类
  4481. */
  4482. class LoadingMessage {
  4483. constructor() {
  4484. this._statusElement = null;
  4485. this._isDisplayed = false;
  4486. this.init();
  4487. }
  4488.  
  4489. /**
  4490. * 初始化加载提示信息
  4491. */
  4492. init() {
  4493. this._statusElement = this.createStatusElement();
  4494. this.insertStatusElement();
  4495. }
  4496.  
  4497. /**
  4498. * 创建提示信息元素
  4499. */
  4500. createStatusElement() {
  4501. const statusElement = $("<div></div>").addClass("alert OJBetter_alert")
  4502. .css({
  4503. "margin": "1em",
  4504. "text-align": "center",
  4505. "position": "relative"
  4506. }).hide();
  4507. return statusElement;
  4508. }
  4509.  
  4510. /**
  4511. * 插入提示信息
  4512. * @returns {void}
  4513. */
  4514. insertStatusElement() {
  4515. (OJBetter.typeOfPage.is_mSite ? $("header") : $(".menu-box:first").next()).after(this._statusElement);
  4516. }
  4517.  
  4518. /**
  4519. * 显示提示信息
  4520. */
  4521. showStatus() {
  4522. this._statusElement.show();
  4523. this._isDisplayed = true;
  4524. }
  4525.  
  4526. /**
  4527. * 隐藏提示信息
  4528. */
  4529. hideStatus() {
  4530. this._statusElement.fadeOut(500);
  4531. this._isDisplayed = false;
  4532. }
  4533.  
  4534. /**
  4535. * 移除提示信息
  4536. */
  4537. removeStatus() {
  4538. this._statusElement.remove();
  4539. this._isDisplayed = false;
  4540. }
  4541.  
  4542. /**
  4543. * 更新提示信息
  4544. * @param {string} text 提示信息文本
  4545. * @param {string} type 提示信息类型,可选值:info, success, warning, error
  4546. * @param {number} timeout 提示信息显示的持续时间(毫秒), 默认为无限长
  4547. */
  4548. updateStatus(text, type = 'info', timeout = Infinity, isMarkdown = false) {
  4549. if (isMarkdown) {
  4550. let md = window.markdownit({
  4551. html: !is_escapeHTML,
  4552. });
  4553. text = md.render(text);
  4554. }
  4555. this._statusElement.html(text).removeClass("alert-info alert-success alert-warning alert-error").addClass(`alert-${type}`);
  4556. if (!this._isDisplayed) {
  4557. this.showStatus();
  4558. }
  4559. if (timeout !== Infinity) {
  4560. setTimeout(() => {
  4561. this.hideStatus();
  4562. }, timeout);
  4563. }
  4564. }
  4565. }
  4566.  
  4567. /**
  4568. * 获取网站本地化的数据
  4569. * @param {*} localizationLanguage 本地化语言
  4570. * @returns {Promise<Object>} 本地化数据
  4571. */
  4572. async function getLocalizeWebsiteJson(localizationLanguage) {
  4573. let data = await OJBetter.common.database.localizeSubsData.get(localizationLanguage);
  4574. let url = localizationLanguage === "zh" ?
  4575. `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/subs/${OJBetter.state.formatName}.json` :
  4576. `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${localizationLanguage}/resources/subs/${OJBetter.state.formatName}.json`;
  4577. if (data) data = data.data;
  4578. if (!data) {
  4579. // 如果本地没有数据,从远端获取并保存
  4580. data = await OJB_getExternalJSON(url);
  4581. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: data });
  4582. } else {
  4583. // 如果本地有数据,检查是否已经在当前会话中尝试过更新
  4584. const sessionKey = `ojb_updateL10nWebsiteJson_${localizationLanguage}`;
  4585. if (!OJB_isSameBrowserSession(sessionKey)) {
  4586. // 如果尚未更新,则在后台更新
  4587. (async () => {
  4588. try {
  4589. const newData = await OJB_getExternalJSON(url);
  4590. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: newData });
  4591. console.log("Website local data has been refreshed!");
  4592. } catch (error) {
  4593. console.error('Failed to update localization data:', error);
  4594. }
  4595. })();
  4596. }
  4597. }
  4598. return data;
  4599. }
  4600.  
  4601. /**
  4602. * 网站本地化替换
  4603. * @returns
  4604. */
  4605. async function localizeWebsite() {
  4606. if (OJBetter.localization.websiteLang === "initial") return;
  4607.  
  4608. // 设置网页语言
  4609. var htmlTag = document.getElementsByTagName("html")[0];
  4610. htmlTag.setAttribute("lang", OJBetter.localization.websiteLang);
  4611.  
  4612. // 获取网站本地化的数据
  4613. var subs = await getLocalizeWebsiteJson(OJBetter.localization.websiteLang);
  4614.  
  4615. /**
  4616. * 文本节点遍历替换
  4617. * @param {JQuery} $nodes jQuery对象
  4618. * @param {Object} textReplaceRules 文本替换规则对象
  4619. * @param {string} key 应用的规则集的名字
  4620. */
  4621. const traverseTextNodes = ($nodes, textReplaceRules, key) => {
  4622. if (!$nodes) return;
  4623.  
  4624. $nodes.each((_, node) => {
  4625. if (node.nodeType === Node.TEXT_NODE) {
  4626. Object.entries(textReplaceRules).forEach(([match, replace]) => {
  4627. try {
  4628. const regex = new RegExp(match, 'g');
  4629. const beforeText = node.textContent;
  4630. node.textContent = node.textContent.replace(regex, replace);
  4631. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4632. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4633. }
  4634. } catch (error) {
  4635. console.error(`Error processing text replacement for match: ${match}`, error);
  4636. }
  4637. });
  4638. } else {
  4639. $(node).contents().each((_, childNode) => {
  4640. traverseTextNodes($(childNode), textReplaceRules, key);
  4641. });
  4642. }
  4643. });
  4644. };
  4645.  
  4646. /**
  4647. * value替换
  4648. * @param {JQuery} $nodes jQuery对象
  4649. * @param {Object} valueReplaceRules 值替换规则对象
  4650. * @param {string} key 应用的规则集的名字
  4651. */
  4652. function traverseValueNodes($nodes, valueReplaceRules, key) {
  4653. if (!$nodes) return;
  4654.  
  4655. $nodes.each(function () {
  4656. let $node = $(this);
  4657. if ($node.is('[value]')) {
  4658. Object.keys(valueReplaceRules).forEach(match => {
  4659. const replace = valueReplaceRules[match];
  4660. const regex = new RegExp(match, 'g');
  4661. let currentValue = $node.val();
  4662. let newValue = currentValue.replace(regex, replace);
  4663. $node.val(newValue);
  4664. if (OJBetter.dev.isRuleMarkingEnabled) {
  4665. if (newValue !== currentValue) $($node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4666. }
  4667. });
  4668. } else {
  4669. $node.children().each(function () {
  4670. traverseValueNodes($(this), valueReplaceRules, key);
  4671. });
  4672. }
  4673. });
  4674. }
  4675.  
  4676. /**
  4677. * 严格的文本节点遍历替换
  4678. * 要求被替换文本严格与规则文本一致
  4679. * @param {JQuery} $nodes jQuery对象
  4680. * @param {Object} textReplaceRules 文本替换规则对象
  4681. * @param {string} key 应用的规则集的名字
  4682. */
  4683. const strictTraverseTextNodes = ($nodes, textReplaceRules, key) => {
  4684. if (!$nodes) return;
  4685.  
  4686. $nodes.each((_, node) => {
  4687. if (node.nodeType === Node.TEXT_NODE) {
  4688. const trimmedNodeText = node.textContent.trim();
  4689. for (const [match, replacement] of Object.entries(textReplaceRules)) {
  4690. if (trimmedNodeText === match) {
  4691. const beforeText = node.textContent;
  4692. node.textContent = replacement;
  4693. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4694. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4695. }
  4696. }
  4697. }
  4698. } else {
  4699. $(node).contents().each((_, childNode) => {
  4700. strictTraverseTextNodes($(childNode), textReplaceRules, key);
  4701. });
  4702. }
  4703. });
  4704. };
  4705.  
  4706. /**
  4707. * 应用文本替换
  4708. */
  4709. let commonReplacements = subs.commonReplacements;
  4710. Object.entries(commonReplacements).forEach(([key, value]) => {
  4711. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4712. classSelectors.forEach(classSelector => {
  4713. if (value.isStrict) {
  4714. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4715. } else {
  4716. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4717. }
  4718. });
  4719. });
  4720.  
  4721. /**
  4722. * 应用value替换
  4723. */
  4724. let InputValueReplacements = subs.InputValueReplacements;
  4725. Object.entries(InputValueReplacements).forEach(([key, value]) => {
  4726. const classSelectors = Array.isArray(value.class) ? value.class : [value.class];
  4727. classSelectors.forEach(classSelector => {
  4728. traverseValueNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4729. });
  4730. });
  4731.  
  4732. /**
  4733. * 动态添加的文本的替换
  4734. */
  4735. let dynamicReplacements = subs.dynamicReplacements;
  4736. Object.entries(dynamicReplacements).forEach(([key, value]) => {
  4737. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4738. classSelectors.forEach(classSelector => {
  4739. OJB_observeElement({
  4740. selector: classSelector,
  4741. callback: (node) => {
  4742. // let popupContent = node.textContent.replace(/^×/, ''); // 去除开头多余的 '×' 字符
  4743. if (value.isStrict) {
  4744. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4745. } else {
  4746. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4747. }
  4748. }
  4749. });
  4750. });
  4751. });
  4752.  
  4753. // 杂项
  4754. (function () {
  4755. // 选项汉化input[type="radio"]
  4756. var translations = {
  4757. "as individual participant": "个人",
  4758. "as a team member": "作为一个团队成员",
  4759. };
  4760. $('input[type="radio"]').each(function () {
  4761. var tag = $(this).parent().contents().filter(function () {
  4762. return this.nodeType === Node.TEXT_NODE;
  4763. });
  4764. for (var i = 0; i < tag.length; i++) {
  4765. var text = tag[i].textContent.trim();
  4766. if (translations.hasOwnProperty(text)) {
  4767. $(this).addClass(text);
  4768. tag[i].replaceWith(translations[text]);
  4769. break;
  4770. }
  4771. }
  4772. });
  4773. })();
  4774. (function () {
  4775. var translations = {
  4776. "(standard input\/output)": "标准输入/输出",
  4777. };
  4778. $("div.notice").each(function () {
  4779. var tag = $(this).children().eq(0).text();
  4780. for (var property in translations) {
  4781. if (tag.match(property)) {
  4782. $(this).children().eq(0).text(translations[property]);
  4783. break;
  4784. }
  4785. }
  4786. });
  4787. })();
  4788.  
  4789. // 轻量站特殊
  4790. if (OJBetter.typeOfPage.is_mSite) {
  4791. traverseTextNodes($('nav'), commonReplacements['.second-level-menu']['rules']);
  4792. }
  4793. if (OJBetter.typeOfPage.is_mSite) {
  4794. (function () {
  4795. var translations = {
  4796. "Announcements": "公告",
  4797. "Submissions": "提交记录",
  4798. "Contests": "比赛",
  4799. };
  4800. $(".caption").each(function () {
  4801. var optionValue = $(this).text();
  4802. if (translations[optionValue]) {
  4803. $(this).text(translations[optionValue]);
  4804. }
  4805. });
  4806. })();
  4807. }
  4808. };
  4809.  
  4810. /**
  4811. * i18next初始化
  4812. */
  4813. async function initI18next() {
  4814. return new Promise((resolve, reject) => {
  4815. i18next
  4816. .use(i18nextChainedBackend)
  4817. .init({
  4818. lng: OJBetter.localization.scriptLang,
  4819. ns: ['common', 'settings', 'config', 'dialog', 'alert', 'translator',
  4820. 'button', 'codeEditor', 'comments', 'announce', 'logMessage'], // 命名空间列表
  4821. defaultNS: 'settings',
  4822. fallbackLng: ['zh', OJBetter.translation.targetLang],
  4823. load: 'currentOnly',
  4824. debug: false,
  4825. backend: {
  4826. backends: [
  4827. i18nextLocalStorageBackend,
  4828. i18nextHttpBackend
  4829. ],
  4830. backendOptions: [{
  4831. prefix: 'i18next_res_',
  4832. expirationTime: 7 * 24 * 60 * 60 * 1000,
  4833. defaultVersion: `v${OJBetter.state.version}`,
  4834. store: typeof window !== 'undefined' ? window.localStorage : null
  4835. }, {
  4836. /* options for secondary backend */
  4837. loadPath: (lng, ns) => {
  4838. if (lng[0] === 'zh' || lng[0] === 'zh-Hans') {
  4839. return `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4840. }
  4841. return `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${lng}/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4842. }
  4843. }]
  4844. }
  4845. }, (err, t) => {
  4846. if (err) {
  4847. reject(err);
  4848. } else {
  4849. jqueryI18next.init(i18next, $, {
  4850. useOptionsAttr: true
  4851. });
  4852. resolve(t);
  4853. }
  4854. });
  4855. });
  4856. };
  4857.  
  4858. /**
  4859. * 抽象命令类
  4860. */
  4861. class Command {
  4862. execute() { }
  4863. undo() { }
  4864. }
  4865.  
  4866. /**
  4867. * 命令调用者
  4868. */
  4869. class CommandInvoker {
  4870. constructor() {
  4871. this.history = [];
  4872. }
  4873.  
  4874. /**
  4875. * 执行命令
  4876. * @param {Command} command 命令对象
  4877. */
  4878. execute(command) {
  4879. this.history.push(command);
  4880. command.execute();
  4881. }
  4882.  
  4883. /**
  4884. * 撤销命令
  4885. */
  4886. undo() {
  4887. const command = this.history.pop();
  4888. if (command) {
  4889. command.undo();
  4890. }
  4891. }
  4892. }
  4893.  
  4894. /**
  4895. * 接收者
  4896. */
  4897. class DOMContainer {
  4898. /**
  4899. * @param {JQueryObject} element 容器对象
  4900. */
  4901. constructor(element) {
  4902. this.containerElement = element;
  4903. }
  4904.  
  4905. /**
  4906. * 添加元素
  4907. * @param {JQueryObject} element 元素对象
  4908. * @returns {JQueryObject} 添加的元素对象
  4909. */
  4910. add(element) {
  4911. this.containerElement.append(element);
  4912. return this.containerElement.children().last();
  4913. }
  4914.  
  4915. /**
  4916. * 删除元素
  4917. * @param {JQueryObject} element 元素对象
  4918. */
  4919. remove(element) {
  4920. $(element).remove();
  4921. }
  4922. }
  4923.  
  4924. /**
  4925. * 具体命令类:添加元素
  4926. */
  4927. class AddElementCommand extends Command {
  4928. /**
  4929. * @param {DOMContainer} receiver 接收者
  4930. * @param {JQueryObject} element 元素对象
  4931. */
  4932. constructor(receiver, element) {
  4933. super();
  4934. this.receiver = receiver;
  4935. this.element = element;
  4936. this.addedElement = null;
  4937. }
  4938.  
  4939. execute() {
  4940. this.addedElement = this.receiver.add(this.element);
  4941. }
  4942.  
  4943. undo() {
  4944. if (this.addedElement) {
  4945. this.receiver.remove(this.addedElement);
  4946. }
  4947. }
  4948. }
  4949.  
  4950. /**
  4951. * 具体命令类:删除元素
  4952. */
  4953. class RemoveElementCommand extends Command {
  4954. /**
  4955. * @param {DOMContainer} receiver 接收者
  4956. * @param {JQueryObject} element 元素对象
  4957. */
  4958. constructor(receiver, element) {
  4959. super();
  4960. this.receiver = receiver;
  4961. this.element = element;
  4962. this.parent = $(element).parent();
  4963. this.nextSibling = $(element).next();
  4964. }
  4965.  
  4966. execute() {
  4967. this.receiver.remove(this.element);
  4968. }
  4969.  
  4970. undo() {
  4971. if (this.nextSibling.length > 0) {
  4972. $(this.element).insertBefore(this.nextSibling);
  4973. } else {
  4974. this.parent.append(this.element);
  4975. }
  4976. }
  4977. }
  4978.  
  4979. /**
  4980. * 验证器
  4981. */
  4982. class Validator {
  4983. /**
  4984. * 表单必填项空值校验
  4985. */
  4986. static required(structure) {
  4987. let config = {};
  4988. let allFieldsValid = true;
  4989. for (const key in structure) {
  4990. let value = key.type == 'checkbox' ?
  4991. $(key).prop("checked") : $(key).val();
  4992.  
  4993. config[structure[key].value] = value;
  4994.  
  4995. if (value || structure[key].require === false) {
  4996. $(key).removeClass('is_null');
  4997. } else {
  4998. $(key).addClass('is_null');
  4999. allFieldsValid = false;
  5000. }
  5001. }
  5002. return {
  5003. valid: allFieldsValid,
  5004. config: config
  5005. };
  5006. }
  5007.  
  5008. /**
  5009. * 表单合法性校验
  5010. */
  5011. static checkKeyValuePairs(structure, config) {
  5012. let errorKeys = [];
  5013. let allFieldsValid = true;
  5014.  
  5015. for (const key in structure) {
  5016. const { check, value } = structure[key];
  5017. const fieldValue = config[value];
  5018.  
  5019. // 如果字段没有值或校验类型不匹配,则跳过当前迭代
  5020. if (!fieldValue) continue;
  5021.  
  5022. let isValid = true;
  5023. switch (check) {
  5024. case 'keyValuePairs':
  5025. isValid = Validator.keyValuePairs(fieldValue);
  5026. break;
  5027. case 'dotSeparatedPath':
  5028. isValid = Validator.validateDotSeparatedPath(fieldValue);
  5029. break;
  5030. default:
  5031. // 没有匹配的校验类型
  5032. continue;
  5033. }
  5034.  
  5035. Validator.toggleErrorDisplay(key, isValid);
  5036. if (!isValid) {
  5037. allFieldsValid = false;
  5038. errorKeys.push(key);
  5039. }
  5040. }
  5041.  
  5042. return {
  5043. valid: allFieldsValid,
  5044. errorKeys: errorKeys
  5045. };
  5046. }
  5047.  
  5048. /**
  5049. * 切换错误信息的显示和隐藏
  5050. * @param {string} key - 字段的键
  5051. * @param {boolean} isValid - 字段值是否有效
  5052. */
  5053. static toggleErrorDisplay(key, isValid) {
  5054. const errorMessage = i18next.t('common.unValid', { ns: 'settings' });
  5055. const $errorSpan = $(key).prev('span.text-error');
  5056. if (!isValid) {
  5057. if (!$errorSpan.length) {
  5058. $(key).before(`<span class="text-error" style="color: red;">${errorMessage}</span>`);
  5059. }
  5060. } else {
  5061. $errorSpan.remove();
  5062. }
  5063. }
  5064.  
  5065. /**
  5066. * 键值对合法性校验
  5067. * @param {string} value
  5068. * @returns {boolean}
  5069. */
  5070. static keyValuePairs(value) {
  5071. const keyValuePairs = value.split('\n');
  5072. // 允许值中包含空格和冒号
  5073. const regex = /^[a-zA-Z0-9_-]+\s*:\s*.+$/;
  5074. return keyValuePairs.every(pair => regex.test(pair));
  5075. }
  5076.  
  5077.  
  5078. /**
  5079. * 点分隔符路径格式校验,允许加减运算
  5080. * @param {string} path
  5081. * @returns {boolean}
  5082. */
  5083. static validateDotSeparatedPath(path) {
  5084. // 正则表达式允许标识符之间有点号,标识符可以包含加减运算
  5085. const regex = /^([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*\.)*([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*)$/;
  5086. return regex.test(path);
  5087. }
  5088. }
  5089.  
  5090. /**
  5091. * 配置管理
  5092. */
  5093. class ConfigManager {
  5094. /**
  5095. * @param {HTMLElement} element - 挂载容器
  5096. * @param {string} prefix - 前缀
  5097. * @param {object} tempConfig - 配置内容
  5098. * @param {object} structure - 配置结构
  5099. * @param {object} configHTML - 配置编辑页面HTML
  5100. * @param {boolean} allowChoice - 是否允许选择列表项
  5101. */
  5102. constructor(element, prefix, tempConfig, structure, configHTML, allowChoice = true) {
  5103. /** @param 设置面板DIV */
  5104. this.settingMenuDiv = $('#OJBetter_setting_menu');
  5105. this.element = $(element);
  5106. this.prefix = prefix;
  5107. this.tempConfig = tempConfig;
  5108. this.structure = structure;
  5109. this.configHTML = configHTML;
  5110. this.allowChoice = allowChoice;
  5111.  
  5112. this.controlTip = null;
  5113. this.config_bar_list = null;
  5114. this.config_bar_ul = null;
  5115. this.config_add_button = null;
  5116. this.menu = null;
  5117. this.editItem = null;
  5118. this.deleteItem = null;
  5119.  
  5120. // 绑定方法
  5121. this.onAdd = this.onAdd.bind(this);
  5122. this.onEdit = this.onEdit.bind(this);
  5123. this.onDelete = this.onDelete.bind(this);
  5124. this.createListItemElement = this.createListItemElement.bind(this);
  5125.  
  5126. this.lastItemId = 0; // 列表中当前最后一个元素的id号
  5127. this.init();
  5128. }
  5129.  
  5130. init() {
  5131. this.createControlBar();
  5132. this.createContextMenu();
  5133. this.renderList();
  5134. }
  5135.  
  5136. /**
  5137. * 创建控制栏
  5138. */
  5139. createControlBar() {
  5140. this.controlTip = OJB_safeCreateJQElement(`<div id='${this.prefix}configControlTip' style='color:red;'></div>`);
  5141. this.config_bar_list = OJB_safeCreateJQElement(`<div class='config_bar_list' id='${this.prefix}config_bar_list'></div>`);
  5142. this.config_bar_ul = OJB_safeCreateJQElement(`<ul class='config_bar_ul' id='${this.prefix}config_bar_ul'></ul>`);
  5143. this.element.append(this.controlTip);
  5144. this.element.append(this.config_bar_list);
  5145. this.config_bar_list.append(this.config_bar_ul);
  5146. }
  5147.  
  5148. /**
  5149. * 创建右键菜单
  5150. */
  5151. createContextMenu() {
  5152. const menu = OJB_safeCreateJQElement(`<div id='config_bar_menu' style='display: none;'></div>`);
  5153. const editItem = OJB_safeCreateJQElement(`
  5154. <div class='config_bar_menu_item' id='config_bar_menu_edit'>
  5155. ${i18next.t('contextMenu.edit', { ns: 'translator' })}
  5156. </div>`);
  5157. const deleteItem = OJB_safeCreateJQElement(`
  5158. <div class='config_bar_menu_item' id='config_bar_menu_delete'>
  5159. ${i18next.t('contextMenu.delete', { ns: 'translator' })}
  5160. </div>`);
  5161. menu.append(editItem);
  5162. menu.append(deleteItem);
  5163. this.editItem = editItem;
  5164. this.deleteItem = deleteItem;
  5165. this.menu = menu;
  5166. this.settingMenuDiv.append(menu);
  5167. }
  5168.  
  5169. /**
  5170. * 关闭右键菜单
  5171. */
  5172. closeContextMenu() {
  5173. this.menu.css({ display: "none" });
  5174. }
  5175.  
  5176. /**
  5177. * 创建列表项
  5178. * @param {string} text - 列表项文本
  5179. * @returns {HTMLElement} - 列表项
  5180. */
  5181. createListItemElement(text) {
  5182. const id = OJB_getRandomNumber(4);
  5183. const li = $("<li></li>");
  5184. const radio = OJB_safeCreateJQElement(`<input type='radio' name='${this.prefix}config_item'></input>`)
  5185. .attr("value", text)
  5186. .attr("id", id)
  5187. .attr("prev_id", this.lastItemId)
  5188. .appendTo(li);
  5189. if (!this.allowChoice) {
  5190. radio.prop("disabled", true);
  5191. }
  5192. const label = OJB_safeCreateJQElement(`<label for='${id}' class='config_bar_ul_li_text'>${text}</label>`).appendTo(li);
  5193.  
  5194.  
  5195. this.lastItemId = id;
  5196.  
  5197. // 添加右键菜单
  5198. li.on("contextmenu", (event) => {
  5199. event.preventDefault();
  5200. this.menu.css({
  5201. display: "block",
  5202. left: event.pageX, top: event.pageY
  5203. });
  5204.  
  5205. const deleteItem = this.deleteItem;
  5206. const editItem = this.editItem;
  5207.  
  5208. // 移除旧事件
  5209. deleteItem.off("click");
  5210. editItem.off("click");
  5211.  
  5212. // 获取 li 在 ul 中的索引
  5213. const index = li.index();
  5214.  
  5215. deleteItem.on("click", () => this.onDelete(index, li));
  5216. editItem.on("click", () => this.onEdit(index, li));
  5217.  
  5218. $(document).one("click", (event) => {
  5219. if (!this.menu.get(0).contains(event.target)) {
  5220. this.closeContextMenu();
  5221. deleteItem.off("click", () => this.onDelete);
  5222. editItem.off("click", () => this.onEdit);
  5223. }
  5224. });
  5225. });
  5226.  
  5227. return li;
  5228. }
  5229.  
  5230. /**
  5231. * 渲染配置列表
  5232. */
  5233. renderList() {
  5234. const list = this.config_bar_ul;
  5235. list.empty(); // 清空
  5236. this.tempConfig.configurations.forEach((item) => {
  5237. list.append(this.createListItemElement(item['name']));
  5238. });
  5239.  
  5240. // 添加按钮
  5241. let addButton = OJB_safeCreateJQElement(`<li id='${this.prefix}add_button' class="tempConfig_add_button">
  5242. <span>+ ${i18next.t('add', { ns: 'common' })}</span>
  5243. </li>`);
  5244. this.config_add_button = addButton;
  5245. list.append(addButton);
  5246. addButton.on("click", this.onAdd);
  5247. }
  5248.  
  5249. /**
  5250. * 添加配置项
  5251. */
  5252. onAdd() {
  5253. const configMenu = this.createConfigHTML();
  5254. const structure = this.structure;
  5255.  
  5256. configMenu.on("click", "#tempConfig_save", () => {
  5257.  
  5258. // 检查必填字段
  5259. const { valid, config } = Validator.required(structure);
  5260. if (!valid) return;
  5261.  
  5262. // 检查键值对
  5263. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5264. if (!checkOk) return;
  5265.  
  5266. this.tempConfig.configurations.push(config);
  5267.  
  5268. this.createListItemElement(config.name).insertBefore(this.config_add_button);
  5269.  
  5270. configMenu.remove();
  5271. });
  5272.  
  5273. configMenu.on("click", ".btn-close", () => {
  5274. configMenu.remove();
  5275. });
  5276. }
  5277.  
  5278. /**
  5279. * 修改配置项
  5280. * @param {number} index - 配置项索引
  5281. * @param {HTMLElement} li - 配置项
  5282. * @returns {void}
  5283. */
  5284. onEdit(index, li) {
  5285. const configMenu = this.createConfigHTML();
  5286. const structure = this.structure;
  5287.  
  5288. this.closeContextMenu();
  5289.  
  5290. // 填充表单
  5291. for (const [key, { value, type }] of Object.entries(this.structure)) {
  5292. const configValue = this.tempConfig.configurations[index][value];
  5293. const $element = $(key);
  5294. if (type === 'checkbox') {
  5295. $element.prop("checked", configValue);
  5296. } else {
  5297. $element.val(configValue);
  5298. }
  5299. }
  5300.  
  5301. configMenu.on("click", "#tempConfig_save", () => {
  5302. // 检查必填字段
  5303. const { valid, config } = Validator.required(structure);
  5304. if (!valid) return;
  5305.  
  5306. // 检查键值对
  5307. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5308. if (!checkOk) return;
  5309.  
  5310. // 更新配置
  5311. this.tempConfig.configurations[index] = config;
  5312. li.find('label').text(config.name);
  5313.  
  5314. OJB_closeAndRemoveModal(configMenu);
  5315. });
  5316.  
  5317. configMenu.on("click", ".btn-close", () => {
  5318. OJB_closeAndRemoveModal(configMenu);
  5319. });
  5320. }
  5321.  
  5322. /**
  5323. * 删除配置项
  5324. * @param {number} index - 配置项索引
  5325. * @param {HTMLElement} li - 配置项
  5326. * @returns {void}
  5327. */
  5328. onDelete(index, li) {
  5329. this.closeContextMenu();
  5330. this.tempConfig.configurations.splice(index, 1);
  5331. li.remove();
  5332. }
  5333.  
  5334. /**
  5335. * 创建配置编辑页面
  5336. * @returns {JQuery<HTMLElement>} 返回配置编辑页面
  5337. */
  5338. createConfigHTML() {
  5339. const configMenu = OJB_safeCreateJQElement(this.configHTML);
  5340. this.settingMenuDiv.after(configMenu);
  5341. OJB_showModal(configMenu);
  5342. OJB_addDraggable(configMenu);
  5343. elementLocalize(configMenu);
  5344. return configMenu;
  5345. }
  5346.  
  5347. /**
  5348. * 获取配置内容
  5349. * @returns {object} - 配置内容
  5350. */
  5351. getTempConfig() {
  5352. return this.tempConfig;
  5353. }
  5354.  
  5355. /**
  5356. * 注册列表项选中改变监听
  5357. */
  5358. registerChoiceChange() {
  5359. this.config_bar_ul.on("change", "input[type='radio']", (event) => {
  5360. const value = event.target.value;
  5361. this.tempConfig.choice = value;
  5362. });
  5363. }
  5364. }
  5365.  
  5366. const OJBetter_setting_sidebar_HTML = `
  5367. <div class="OJBetter_setting_sidebar">
  5368. <ul>
  5369. <li><a href="#basic-settings" id="sidebar-basic-settings" class="active" data-i18n="settings:sidebar.basic"></a></li>
  5370. <li><a href="#l10n_settings" id="sidebar-l10n_settings" data-i18n="settings:sidebar.localization"></a></li>
  5371. <li><a href="#translation-settings" id="sidebar-translation-settings" data-i18n="settings:sidebar.translation"></a></li>
  5372. <li><a href="#clist_rating-settings" id="sidebar-clist_rating-settings" data-i18n="settings:sidebar.clist"></a></li>
  5373. <li><a href="#code_editor-settings" id="sidebar-code_editor-settings" data-i18n="settings:sidebar.monaco"></a></li>
  5374. <li><a href="#preference-settings" id="sidebar-preference-settings" data-i18n="settings:sidebar.preference"></a></li>
  5375. <li><a href="#dev-settings" id="sidebar-dev-settings" data-i18n="settings:sidebar.dev"></a></li>
  5376. <li><a href="#about-settings" id="sidebar-about-settings" data-i18n="settings:sidebar.about"></a></li>
  5377. </ul>
  5378. </div>
  5379. `;
  5380.  
  5381. const basic_settings_HTML = `
  5382. <div id="basic-settings" class="settings-page active">
  5383. <h3 data-i18n="settings:basic.title"></h3>
  5384. <hr>
  5385. <div class='OJBetter_setting_list' style="padding: 0px 10px;">
  5386. <span id="darkMode_span" data-i18n="settings:basic.darkMode.name"></span>
  5387. <div class="dark-mode-selection">
  5388. <label>
  5389. <input class="radio-input" type="radio" name="darkMode" value="dark" />
  5390. <span class="OJBetter_setting_menu_label_text"
  5391. data-i18n="settings:basic.darkMode.options.dark"></span>
  5392. <span class="radio-icon"> </span>
  5393. </label>
  5394. <label>
  5395. <input checked="" class="radio-input" type="radio" name="darkMode" value="light" />
  5396. <span class="OJBetter_setting_menu_label_text"
  5397. data-i18n="settings:basic.darkMode.options.light"></span>
  5398. <span class="radio-icon"> </span>
  5399. </label>
  5400. <label>
  5401. <input class="radio-input" type="radio" name="darkMode" value="follow" />
  5402. <span class="OJBetter_setting_menu_label_text"
  5403. data-i18n="settings:basic.darkMode.options.system"></span>
  5404. <span class="radio-icon"> </span>
  5405. </label>
  5406. </div>
  5407. </div>
  5408. <div class='OJBetter_setting_list'>
  5409. <label for="expandFoldingblocks" data-i18n="settings:basic.expandBlocks"></label>
  5410. <input type="checkbox" id="expandFoldingblocks" name="expandFoldingblocks">
  5411. </div>
  5412. <div class='OJBetter_setting_list'>
  5413. <label for="renderPerfOpt" data-i18n="settings:basic.renderOptimization.label"></label>
  5414. <div class="help_tip">
  5415. ${helpCircleHTML}
  5416. <div class="tip_text" data-i18n="[html]settings:basic.renderOptimization.helpText"></div>
  5417. </div>
  5418. <input type="checkbox" id="renderPerfOpt" name="renderPerfOpt">
  5419. </div>
  5420. <div class='OJBetter_setting_list'>
  5421. <label for="selectElementPerfOpt" data-i18n="settings:basic.selectElementOptimization.label"></label>
  5422. <div class="help_tip">
  5423. ${helpCircleHTML}
  5424. <div class="tip_text" data-i18n="[html]settings:basic.selectElementOptimization.helpText"></div>
  5425. </div>
  5426. <input type="checkbox" id="selectElementPerfOpt" name="selectElementPerfOpt">
  5427. </div>
  5428. <div class='OJBetter_setting_list'>
  5429. <label for="commentPaging" data-i18n="settings:basic.paging.label"></label>
  5430. <div class="help_tip">
  5431. ${helpCircleHTML}
  5432. <div class="tip_text" data-i18n="[html]settings:basic.paging.helpText"></div>
  5433. </div>
  5434. <input type="checkbox" id="commentPaging" name="commentPaging">
  5435. </div>
  5436. <div class='OJBetter_setting_list'>
  5437. <label for="showJumpToLuogu" data-i18n="settings:basic.luoguJump.label"></label>
  5438. <div class="help_tip">
  5439. ${helpCircleHTML}
  5440. <div class="tip_text" data-i18n="[html]settings:basic.luoguJump.helpText"></div>
  5441. </div>
  5442. <input type="checkbox" id="showJumpToLuogu" name="showJumpToLuogu">
  5443. </div>
  5444. <div class='OJBetter_setting_list'>
  5445. <label for="showCF2vjudge" data-i18n="settings:basic.vjudgeJump.label"></label>
  5446. <div class="help_tip">
  5447. ${helpCircleHTML}
  5448. <div class="tip_text" data-i18n="[html]settings:basic.vjudgeJump.helpText"></div>
  5449. </div>
  5450. <input type="checkbox" id="showCF2vjudge" name="showCF2vjudge">
  5451. </div>
  5452. <div class='OJBetter_setting_list'>
  5453. <label for="standingsRecolor" data-i18n="settings:basic.recolor.label"></label>
  5454. <div class="help_tip">
  5455. ${helpCircleHTML}
  5456. <div class="tip_text" data-i18n="[html]settings:basic.recolor.helpText"></div>
  5457. </div>
  5458. <input type="checkbox" id="standingsRecolor" name="standingsRecolor">
  5459. </div>
  5460. </div>
  5461. `;
  5462.  
  5463. const l10n_settings_HTML = `
  5464. <div id="l10n_settings" class="settings-page">
  5465. <h3 data-i18n="settings:localization.title"></h3>
  5466. <hr>
  5467. <div class='OJBetter_setting_list'>
  5468. <label for="scriptL10nLanguage" style="display: flex;" data-i18n="settings:localization.scriptLanguageLabel"></label>
  5469. <select id="scriptL10nLanguage" name="scriptL10nLanguage">
  5470. <option value="zh">简体中文</option>
  5471. <option value="zh-Hant">繁體中文</option>
  5472. <option value="en">English</option>
  5473. <option value="de">Deutsch</option>
  5474. <option value="fr">Français</option>
  5475. <option value="ko">한국어</option>
  5476. <option value="pt">Português</option>
  5477. <option value="ja">日本語</option>
  5478. <option value="es">Español</option>
  5479. <option value="it">Italiano</option>
  5480. <option value="hi">हिन्दी</option>
  5481. </select>
  5482. </div>
  5483. <div class='OJBetter_setting_list'>
  5484. <label for="localizationLanguage" style="display: flex;" data-i18n="settings:localization.websiteLanguageLabel"></label>
  5485. <select id="localizationLanguage" name="localizationLanguage">
  5486. <option value="initial">——</option>
  5487. <option value="zh">简体中文</option>
  5488. <option value="zh-Hant">繁體中文</option>
  5489. <option value="de">Deutsch</option>
  5490. <option value="fr">Français</option>
  5491. <option value="ko">한국어</option>
  5492. <option value="pt">Português</option>
  5493. <option value="ja">日本語</option>
  5494. <option value="es">Español</option>
  5495. <option value="it">Italiano</option>
  5496. <option value="hi">हिन्दी</option>
  5497. </select>
  5498. </div>
  5499. <div class='OJBetter_setting_list alert_tip'>
  5500. <div data-i18n="[html]settings:localization.notice.1"></div>
  5501. </div>
  5502. <div class='OJBetter_setting_list alert_tip'>
  5503. <div data-i18n="[html]settings:localization.notice.2"></div>
  5504. </div>
  5505. </div>
  5506. `;
  5507.  
  5508. const translation_settings_HTML = `
  5509. <div id="translation-settings" class="settings-page">
  5510. <h3 data-i18n="settings:translation.title"></h3>
  5511. <hr>
  5512. <h4 data-i18n="settings:translation.options.title"></h4>
  5513. <div class='OJBetter_setting_list'>
  5514. <label for="transTargetLang" style="display: flex;" data-i18n="settings:translation.preference.target.title"></label>
  5515. <div class="help_tip">
  5516. ${helpCircleHTML}
  5517. <div class="tip_text" data-i18n="[html]settings:translation.preference.target.helpText"></div>
  5518. </div>
  5519. <select id="transTargetLang" name="transTargetLang">
  5520. <option value="zh">简体中文</option>
  5521. <option value="zh-Hant">繁體中文</option>
  5522. <option value="de">Deutsch</option>
  5523. <option value="fr">Français</option>
  5524. <option value="ko">한국어</option>
  5525. <option value="pt">Português</option>
  5526. <option value="ja">日本語</option>
  5527. <option value="es">Español</option>
  5528. <option value="it">Italiano</option>
  5529. <option value="hi">हिन्दी</option>
  5530. </select>
  5531. </div>
  5532. <div id="translationServices">
  5533. <label>
  5534. <input type='radio' name='translation' value='deepl'>
  5535. <span class='OJBetter_setting_menu_label_text'
  5536. data-i18n="settings:translation.options.services.deepl"></span>
  5537. </label>
  5538. <label>
  5539. <input type='radio' name='translation' value='iflyrec'>
  5540. <span class='OJBetter_setting_menu_label_text'
  5541. data-i18n="settings:translation.options.services.iflyrec"></span>
  5542. </label>
  5543. <label>
  5544. <input type='radio' name='translation' value='youdao'>
  5545. <span class='OJBetter_setting_menu_label_text'
  5546. data-i18n="settings:translation.options.services.youdao"></span>
  5547. </label>
  5548. <label>
  5549. <input type='radio' name='translation' value='google'>
  5550. <span class='OJBetter_setting_menu_label_text'
  5551. data-i18n="settings:translation.options.services.google"></span>
  5552. </label>
  5553. <label>
  5554. <input type='radio' name='translation' value='caiyun'>
  5555. <span class='OJBetter_setting_menu_label_text'
  5556. data-i18n="settings:translation.options.services.caiyun"></span>
  5557. </label>
  5558. <label>
  5559. <input type='radio' name='translation' value='openai'>
  5560. <span class='OJBetter_setting_menu_label_text'
  5561. data-i18n="settings:translation.options.services.openai.name">
  5562. <div class="help_tip">
  5563. ${helpCircleHTML}
  5564. <div class="tip_text"
  5565. data-i18n="[html]settings:translation.options.services.openai.helpText"></div>
  5566. </div>
  5567. </span>
  5568. </label>
  5569. </div>
  5570. <hr>
  5571. <h4>DeepL</h4>
  5572. <div class='OJBetter_setting_list'>
  5573. <label for="deepl_type" style="display: flex;" data-i18n="settings:translation.deepl.mode.title"></label>
  5574. <div class="help_tip">
  5575. ${helpCircleHTML}
  5576. <div class="tip_text" data-i18n="[html]settings:translation.deepl.mode.helpText"></div>
  5577. </div>
  5578. <select id="deepl_type" name="deepl_type">
  5579. <option value="free" data-i18n="settings:translation.deepl.mode.select.free"></option>
  5580. <option value="api" data-i18n="settings:translation.deepl.mode.select.api"></option>
  5581. </select>
  5582. </div>
  5583. <div id="deepl_config" class="config"></div>
  5584. <div class='OJBetter_setting_list'>
  5585. <label for="enableEmphasisProtection" data-i18n="settings:translation.deepl.enableEmphasisProtection.title"></label>
  5586. <div class="help_tip" style="margin-right: initial;">
  5587. ${helpCircleHTML}
  5588. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableEmphasisProtection.helpText"></div>
  5589. </div>
  5590. <div class="badge">Official API Only</div>
  5591. <input type="checkbox" id="enableEmphasisProtection" name="enableEmphasisProtection">
  5592. </div>
  5593. <div class='OJBetter_setting_list'>
  5594. <label for="enableLinkProtection" data-i18n="settings:translation.deepl.enableLinkProtection.title"></label>
  5595. <div class="help_tip" style="margin-right: initial;">
  5596. ${helpCircleHTML}
  5597. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableLinkProtection.helpText"></div>
  5598. </div>
  5599. <div class="badge">Official API Only</div>
  5600. <input type="checkbox" id="enableLinkProtection" name="enableLinkProtection">
  5601. </div>
  5602. <hr>
  5603. <h4>ChatGPT</h4>
  5604. <div id="chatgpt_config" class="config"></div>
  5605. <div class='OJBetter_setting_list'>
  5606. <label for="openai_isStream" data-i18n="settings:translation.chatgpt.isStream.name"></label>
  5607. <div class="help_tip">
  5608. ${helpCircleHTML}
  5609. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.isStream.helpText"></div>
  5610. </div>
  5611. <input type="checkbox" id="openai_isStream" name="openai_isStream">
  5612. </div>
  5613. <hr>
  5614. <h4 data-i18n="settings:translation.preference.title"></h4>
  5615. <div class='OJBetter_setting_list'>
  5616. <label for="comment_translation_choice" style="display: flex;"
  5617. data-i18n="settings:translation.preference.comment_translation_choice.title">
  5618. </label>
  5619. <select id="comment_translation_choice" name="comment_translation_choice">
  5620. <option value="0" data-i18n="settings:translation.preference.comment_translation_choice.services.follow"></option>
  5621. <option value="deepl" data-i18n="settings:translation.preference.comment_translation_choice.services.deepl"></option>
  5622. <option value="iflyrec" data-i18n="settings:translation.preference.comment_translation_choice.services.iflyrec"></option>
  5623. <option value="youdao" data-i18n="settings:translation.preference.comment_translation_choice.services.youdao"></option>
  5624. <option value="google" data-i18n="settings:translation.preference.comment_translation_choice.services.google"></option>
  5625. <option value="caiyun" data-i18n="settings:translation.preference.comment_translation_choice.services.caiyun"></option>
  5626. <option value="openai" data-i18n="settings:translation.preference.comment_translation_choice.services.openai"></option>
  5627. </select>
  5628. </div>
  5629. <hr>
  5630. <h4 data-i18n="settings:translation.autoTranslation.title"></h4>
  5631. <div class='OJBetter_setting_list'>
  5632. <label for="autoTranslation" data-i18n="settings:translation.autoTranslation.enable"></label>
  5633. <div class="help_tip">
  5634. ${helpCircleHTML}
  5635. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.helpText"></div>
  5636. </div>
  5637. <input type="checkbox" id="autoTranslation" name="autoTranslation">
  5638. </div>
  5639. <div class='OJBetter_setting_list'>
  5640. <label for='shortTextLength'>
  5641. <div style="display: flex;align-items: center;"
  5642. data-i18n="settings:translation.autoTranslation.shortTextLength.name"></div>
  5643. </label>
  5644. <div class="help_tip">
  5645. ${helpCircleHTML}
  5646. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.shortTextLength.helpText">
  5647. </div>
  5648. </div>
  5649. <input type='number' id='shortTextLength' class='no_default' require=true data-i18n="[placeholder]settings:translation.autoTranslation.shortTextLength.placeholder">
  5650. <span data-i18n="settings:translation.autoTranslation.shortTextLength.end"></span>
  5651. </div>
  5652. <div class='OJBetter_setting_list'>
  5653. <label for="allowMixTrans" data-i18n="settings:translation.autoTranslation.allowMixTrans.name"></label>
  5654. <div class="help_tip">
  5655. ${helpCircleHTML}
  5656. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.allowMixTrans.helpText">
  5657. </div>
  5658. </div>
  5659. <input type="checkbox" id="allowMixTrans" name="allowMixTrans">
  5660. <div class='OJBetter_checkboxs'>
  5661. <input type="checkbox" id="deepl" name="mixedTranslation" value="deepl">
  5662. <label for="deepl" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.deepl"></label>
  5663. <input type="checkbox" id="iflyrec" name="mixedTranslation" value="iflyrec">
  5664. <label for="iflyrec" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.iflyrec"></label>
  5665. <input type="checkbox" id="youdao" name="mixedTranslation" value="youdao">
  5666. <label for="youdao" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.youdao"></label>
  5667. <input type="checkbox" id="google" name="mixedTranslation" value="google">
  5668. <label for="google" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.google">Google</label>
  5669. <input type="checkbox" id="caiyun" name="mixedTranslation" value="caiyun">
  5670. <label for="caiyun" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.caiyun"></label>
  5671. </div>
  5672. </div>
  5673. <hr>
  5674. <h4 data-i18n="settings:translation.advanced.name"></h4>
  5675. <div class='OJBetter_setting_list'>
  5676. <label for="comment_translation_mode" style="display: flex;" data-i18n="settings:translation.advanced.mode.name"></label>
  5677. <div class="help_tip">
  5678. ${helpCircleHTML}
  5679. <div class="tip_text" data-i18n="[html]settings:translation.advanced.mode.helpText"></div>
  5680. </div>
  5681. <select id="comment_translation_mode" name="comment_translation_mode">
  5682. <option value="0" data-i18n="settings:translation.advanced.mode.options.0"></option>
  5683. <option value="1" data-i18n="settings:translation.advanced.mode.options.1"></option>
  5684. <option value="2" data-i18n="settings:translation.advanced.mode.options.2"></option>
  5685. </select>
  5686. </div>
  5687. <div class='OJBetter_setting_list'>
  5688. <label for="memoryTranslateHistory" data-i18n="settings:translation.advanced.memory.name"></label>
  5689. <div class="help_tip">
  5690. ${helpCircleHTML}
  5691. <div class="tip_text" data-i18n="[html]settings:translation.advanced.memory.helpText"></div>
  5692. </div>
  5693. <input type="checkbox" id="memoryTranslateHistory" name="memoryTranslateHistory">
  5694. </div>
  5695. <div class='OJBetter_setting_list'>
  5696. <label for="translation_retransAction" style="display: flex;" data-i18n="settings:translation.advanced.retrans.name"></label>
  5697. <div class="help_tip">
  5698. ${helpCircleHTML}
  5699. <div class="tip_text" data-i18n="[html]settings:translation.advanced.retrans.helpText"></div>
  5700. </div>
  5701. <select id="translation_retransAction" name="translation_retransAction">
  5702. <option value=0 data-i18n="settings:translation.advanced.retrans.options.0"></option>
  5703. <option value=1 data-i18n="settings:translation.advanced.retrans.options.1"></option>
  5704. </select>
  5705. </div>
  5706. <div class='OJBetter_setting_list'>
  5707. <label for='transWaitTime'>
  5708. <div style="display: flex;align-items: center;" data-i18n="settings:translation.advanced.transWaitTime.name"></div>
  5709. </label>
  5710. <div class="help_tip">
  5711. ${helpCircleHTML}
  5712. <div class="tip_text" data-i18n="[html]settings:translation.advanced.transWaitTime.helpText"></div>
  5713. </div>
  5714. <input type='number' id='transWaitTime' class='no_default' require=true data-i18n="[placeholder]settings:translation.advanced.transWaitTime.placeholder">
  5715. <span data-i18n="settings:translation.advanced.transWaitTime.end"></span>
  5716. </div>
  5717. <div class='OJBetter_setting_list'>
  5718. <label for="translation_replaceSymbol" style="display: flex;" data-i18n="settings:translation.advanced.replaceSymbol.name"></label>
  5719. <div class="help_tip">
  5720. ${helpCircleHTML}
  5721. <div class="tip_text" data-i18n="[html]settings:translation.advanced.replaceSymbol.helpText"></div>
  5722. </div>
  5723. <select id="translation_replaceSymbol" name="translation_replaceSymbol">
  5724. <option value=2 data-i18n="settings:translation.advanced.replaceSymbol.options.2"></option>
  5725. <option value=1 data-i18n="settings:translation.advanced.replaceSymbol.options.1"></option>
  5726. <option value=3 data-i18n="settings:translation.advanced.replaceSymbol.options.3"></option>
  5727. </select>
  5728. </div>
  5729. <div class='OJBetter_setting_list'>
  5730. <label for="filterTextWithoutEmphasis" data-i18n="settings:translation.advanced.filterTextWithoutEmphasis.name"></label>
  5731. <div class="help_tip">
  5732. ${helpCircleHTML}
  5733. <div class="tip_text" data-i18n="[html]settings:translation.advanced.filterTextWithoutEmphasis.helpText"></div>
  5734. </div>
  5735. <input type="checkbox" id="filterTextWithoutEmphasis" name="filterTextWithoutEmphasis">
  5736. </div>
  5737. </div>
  5738. `;
  5739.  
  5740. const clist_rating_settings_HTML = `
  5741. <div id="clist_rating-settings" class="settings-page">
  5742. <h3 data-i18n="settings:clist.title"></h3>
  5743. <hr>
  5744. <h4 data-i18n="settings:clist.basics.name"></h4>
  5745. <div class='OJBetter_setting_list alert_tip'>
  5746. <div>
  5747. <p data-i18n="[html]settings:clist.basics.notice"></p>
  5748. </div>
  5749. </div>
  5750. <div class='OJBetter_setting_list'>
  5751. <label for='clist_Authorization'>
  5752. <div style="display: flex;align-items: center;">
  5753. <span class="input_label" data-i18n="settings:clist.basics.key.title"></span>
  5754. </div>
  5755. </label>
  5756. <div class="help_tip">
  5757. ${helpCircleHTML}
  5758. <div class="tip_text" data-i18n="[html]settings:clist.basics.key.helpText"></div>
  5759. </div>
  5760. <input type='text' id='clist_Authorization' class='no_default' required="true"
  5761. data-i18n="[placeholder]settings:clist.basics.key.keyPlaceholder">
  5762. </div>
  5763. <hr>
  5764. <h4 data-i18n="settings:clist.displayRating.title"></h4>
  5765. <div class='OJBetter_setting_list'>
  5766. <label for="showClistRating_contest"><span data-i18n="settings:clist.displayRating.contest.name"></span></label>
  5767. <input type="checkbox" id="showClistRating_contest" name="showClistRating_contest">
  5768. </div>
  5769. <div class='OJBetter_setting_list'>
  5770. <label for="showClistRating_problem"><span data-i18n="settings:clist.displayRating.problem.name"></span></label>
  5771. <input type="checkbox" id="showClistRating_problem" name="showClistRating_problem">
  5772. </div>
  5773. <div class='OJBetter_setting_list'>
  5774. <label for="showClistRating_problemset"><span data-i18n="settings:clist.displayRating.problemset.name"></span></label>
  5775. <input type="checkbox" id="showClistRating_problemset" name="showClistRating_problemset">
  5776. </div>
  5777. <hr>
  5778. <div class='OJBetter_setting_list'>
  5779. <label for="RatingHidden"><span data-i18n="settings:clist.spoilerProtection.title"></span></label>
  5780. <div class="help_tip">
  5781. ${helpCircleHTML}
  5782. <div class="tip_text" data-i18n="[html]settings:clist.spoilerProtection.helpText"></div>
  5783. </div>
  5784. <input type="checkbox" id="RatingHidden" name="RatingHidden">
  5785. </div>
  5786. </div>
  5787. `;
  5788.  
  5789. const code_editor_settings_HTML = `
  5790. <div id="code_editor-settings" class="settings-page">
  5791. <h3 data-i18n="settings:codeEditor.title"></h3>
  5792. <hr>
  5793. <h4 data-i18n="settings:codeEditor.basics"></h4>
  5794. <div class='OJBetter_setting_list'>
  5795. <label for="problemPageCodeEditor"><span
  5796. data-i18n="settings:codeEditor.problemPageCodeEditor.label"></span></label>
  5797. <div class="help_tip">
  5798. ${helpCircleHTML}
  5799. <div class="tip_text" data-i18n="settings:codeEditor.problemPageCodeEditor.helpText"></div>
  5800. </div>
  5801. <input type="checkbox" id="problemPageCodeEditor" name="problemPageCodeEditor">
  5802. </div>
  5803. <div class='OJBetter_setting_list'>
  5804. <label for="beautifyPreBlocks"><span
  5805. data-i18n="settings:codeEditor.beautifyPreBlocks.label"></span></label>
  5806. <div class="help_tip">
  5807. ${helpCircleHTML}
  5808. <div class="tip_text" data-i18n="settings:codeEditor.beautifyPreBlocks.helpText"></div>
  5809. </div>
  5810. <input type="checkbox" id="beautifyPreBlocks" name="beautifyPreBlocks">
  5811. </div>
  5812. <hr>
  5813. <h4 data-i18n="settings:codeEditor.preferences.title"></h4>
  5814. <div class='OJBetter_setting_list'>
  5815. <label for="isCodeSubmitConfirm"><span
  5816. data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.label"></span></label>
  5817. <div class="help_tip">
  5818. ${helpCircleHTML}
  5819. <div class="tip_text" data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.helpText"></div>
  5820. </div>
  5821. <input type="checkbox" id="isCodeSubmitConfirm" name="isCodeSubmitConfirm">
  5822. </div>
  5823. <div class='OJBetter_setting_list'>
  5824. <label for="alwaysConsumeMouseWheel"><span
  5825. data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.label"></span></label>
  5826. <div class="help_tip">
  5827. ${helpCircleHTML}
  5828. <div class="tip_text" data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.helpText"></div>
  5829. </div>
  5830. <input type="checkbox" id="alwaysConsumeMouseWheel" name="alwaysConsumeMouseWheel">
  5831. </div>
  5832. <div class='OJBetter_setting_list'>
  5833. <label for="submitButtonPosition"><span
  5834. data-i18n="settings:codeEditor.preferences.submitButtonPosition.label"></span></label>
  5835. <div class="help_tip">
  5836. ${helpCircleHTML}
  5837. <div class="tip_text" data-i18n="settings:codeEditor.preferences.submitButtonPosition.helpText"></div>
  5838. </div>
  5839. <select id="submitButtonPosition" name="submitButtonPosition">
  5840. <option value="bottom" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.bottom"></option>
  5841. <option value="top" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.top"></option>
  5842. </select>
  5843. </div>
  5844. <hr>
  5845. <h4 data-i18n="settings:codeEditor.onlineCodeExecution.title"></h4>
  5846. <label>
  5847. <input type='radio' name='compiler' value='official'>
  5848. <span class='OJBetter_setting_menu_label_text'
  5849. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.codeforces"></span>
  5850. </label>
  5851. <label>
  5852. <input type='radio' name='compiler' value='wandbox'>
  5853. <span class='OJBetter_setting_menu_label_text'
  5854. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.wandbox"></span>
  5855. </label>
  5856. <label>
  5857. <input type='radio' name='compiler' value='rextester'>
  5858. <span class='OJBetter_setting_menu_label_text'
  5859. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.rextester"></span>
  5860. </label>
  5861. <hr>
  5862. <h4 data-i18n="settings:codeEditor.lsp.title"></h4>
  5863. <div class='OJBetter_setting_list'>
  5864. <label for="useLSP"><span data-i18n="settings:codeEditor.lsp.useLSP.label"></span></label>
  5865. <div class="help_tip">
  5866. ${helpCircleHTML}
  5867. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.useLSP.helpText"></div>
  5868. </div>
  5869. <input type="checkbox" id="useLSP" name="useLSP">
  5870. </div>
  5871. <div class='OJBetter_setting_list'>
  5872. <label for='OJBetter_Bridge_WorkUri'>
  5873. <div style="display: flex;align-items: center;">
  5874. <span class="input_label" data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.label"></span>
  5875. </div>
  5876. </label>
  5877. <div class="help_tip">
  5878. ${helpCircleHTML}
  5879. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.helpText"></div>
  5880. </div>
  5881. <input type='text' id='OJBetter_Bridge_WorkUri' class='no_default'
  5882. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.placeholder">
  5883. </div>
  5884. <div class='OJBetter_setting_list'>
  5885. <label for='OJBetter_Bridge_SocketUrl'>
  5886. <div style="display: flex;align-items: center;">
  5887. <span class="input_label"
  5888. data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.label"></span>
  5889. </div>
  5890. </label>
  5891. <div class="help_tip">
  5892. ${helpCircleHTML}
  5893. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.helpText"></div>
  5894. </div>
  5895. <input type='text' id='OJBetter_Bridge_SocketUrl' class='no_default'
  5896. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.placeholder">
  5897. </div>
  5898. <hr>
  5899. <h4 data-i18n="settings:codeEditor.staticCompletionEnhancement.title"></h4>
  5900. <div class='OJBetter_setting_list'>
  5901. <label for="cppCodeTemplateComplete"><span
  5902. data-i18n="settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.label"></span></label>
  5903. <div class="help_tip">
  5904. ${helpCircleHTML}
  5905. <div class="tip_text" data-i18n="[html]settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.helpText"></div>
  5906. </div>
  5907. <input type="checkbox" id="cppCodeTemplateComplete" name="cppCodeTemplateComplete">
  5908. </div>
  5909. <hr>
  5910. <h5 data-i18n="settings:codeEditor.staticCompletionEnhancement.customization"></h5>
  5911. <div class='OJBetter_setting_list alert_warn'>
  5912. <div>
  5913. <p data-i18n="settings:codeEditor.staticCompletionEnhancement.performanceWarning"></p>
  5914. </div>
  5915. </div>
  5916. <div id="Complet_config" class="config"></div>
  5917. </div>
  5918. `;
  5919.  
  5920. const preference_settings_HTML = `
  5921. <div id="preference-settings" class="settings-page">
  5922. <h3 data-i18n="settings:preference.title"></h3>
  5923. <hr>
  5924. <div class='OJBetter_setting_list'>
  5925. <label for="showLoading" data-i18n="settings:preference.loadingInfo.label"></label>
  5926. <div class="help_tip">
  5927. ${helpCircleHTML}
  5928. <div class="tip_text" data-i18n="[html]settings:preference.loadingInfo.helpText" data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }'></div>
  5929. </div>
  5930. <input type="checkbox" id="showLoading" name="showLoading">
  5931. </div>
  5932. <div class='OJBetter_setting_list'>
  5933. <label for="hoverTargetAreaDisplay" data-i18n="settings:preference.targetArea.label"></label>
  5934. <div class="help_tip">
  5935. ${helpCircleHTML}
  5936. <div class="tip_text" data-i18n="[html]settings:preference.targetArea.helpText"></div>
  5937. </div>
  5938. <input type="checkbox" id="hoverTargetAreaDisplay" name="hoverTargetAreaDisplay">
  5939. </div>
  5940. <div class='OJBetter_setting_list'>
  5941. <label for='iconButtonSize'>
  5942. <div style="display: flex;align-items: center;" data-i18n="settings:preference.iconButtonSize.title"></div>
  5943. </label>
  5944. <div class="help_tip">
  5945. ${helpCircleHTML}
  5946. <div class="tip_text" data-i18n="[html]settings:preference.iconButtonSize.helpText"></div>
  5947. </div>
  5948. <input type='number' id='iconButtonSize' class='no_default' require=true data-i18n="[placeholder]settings:preference.iconButtonSize.placeholder">
  5949. <span>px</span>
  5950. </div>
  5951. </div>
  5952. `;
  5953.  
  5954. const dev_settings_HTML = `
  5955. <div id="dev-settings" class="settings-page">
  5956. <h3 data-i18n="settings:dev.title"></h3>
  5957. <hr>
  5958. <div class='OJBetter_setting_list alert_danger'>
  5959. <div>
  5960. <p data-i18n="[html]settings:dev.notice"></p>
  5961. </div>
  5962. </div>
  5963. <hr>
  5964. <h5 data-i18n="settings:dev.load.title"></h5>
  5965. <div class='OJBetter_setting_list'>
  5966. <label for="notWaiteLoaded"><span data-i18n="settings:dev.load.notWaiteLoaded.label"></span></label>
  5967. <div class="help_tip">
  5968. ${helpCircleHTML}
  5969. <div class="tip_text" data-i18n="[html]settings:dev.load.notWaiteLoaded.helpText"></div>
  5970. </div>
  5971. <input type="checkbox" id="notWaiteLoaded" name="notWaiteLoaded">
  5972. </div>
  5973. <hr>
  5974. <h5 data-i18n="settings:dev.l10n.title"></h5>
  5975. <div class='OJBetter_setting_list'>
  5976. <label><span data-i18n="settings:dev.l10n.refreshScrpitCache.label"></span></label>
  5977. <div class="help_tip">
  5978. ${helpCircleHTML}
  5979. <div class="tip_text" data-i18n="[html]settings:dev.l10n.refreshScrpitCache.helpText"></div>
  5980. </div>
  5981. <button type="button" id="l10n_refreshScrpitCacheButton" name="l10n_refreshScrpitCacheButton" data-i18n="settings:dev.l10n.refreshScrpitCache.button"></button>
  5982. </div>
  5983. <hr>
  5984. <h5 data-i18n="settings:dev.l10n_web.title"></h5>
  5985. <div class='OJBetter_setting_list'>
  5986. <label><span data-i18n="settings:dev.l10n_web.refreshScrpitCache.label"></span></label>
  5987. <div class="help_tip">
  5988. ${helpCircleHTML}
  5989. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.refreshScrpitCache.helpText"></div>
  5990. </div>
  5991. <button type="button" id="l10n_web_refreshScrpitCacheButton" name="l10n_web_refreshScrpitCacheButton" data-i18n="settings:dev.l10n_web.refreshScrpitCache.button"></button>
  5992. </div>
  5993. <div class='OJBetter_setting_list'>
  5994. <label for="isRuleMarkingEnabled"><span data-i18n="settings:dev.l10n_web.isRuleMarkingEnabled.label"></span></label>
  5995. <div class="help_tip">
  5996. ${helpCircleHTML}
  5997. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.isRuleMarkingEnabled.helpText"></div>
  5998. </div>
  5999. <input type="checkbox" id="isRuleMarkingEnabled" name="isRuleMarkingEnabled">
  6000. </div>
  6001. <hr>
  6002. <h5 data-i18n="settings:dev.indexedDB.title"></h5>
  6003. <div class='OJBetter_setting_list'>
  6004. <label><span data-i18n="settings:dev.indexedDB.clear.label"></span></label>
  6005. <div class="help_tip">
  6006. ${helpCircleHTML}
  6007. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.clear.helpText"></div>
  6008. </div>
  6009. <button type="button" id="indexedDB_clearButton" name="indexedDB_clearButton" data-i18n="settings:dev.indexedDB.clear.button"></button>
  6010. </div>
  6011. <div class='OJBetter_setting_list'>
  6012. <label><span data-i18n="settings:dev.indexedDB.inputOrExport.label"></span></label>
  6013. <div class="help_tip">
  6014. ${helpCircleHTML}
  6015. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.inputOrExport.helpText"></div>
  6016. </div>
  6017. <button type="button" id="indexedDB_exportButton" name="indexedDB_exportButton" data-i18n="settings:dev.indexedDB.inputOrExport.export"></button>
  6018. <button type="button" id="indexedDB_importButton" name="indexedDB_importButton" data-i18n="settings:dev.indexedDB.inputOrExport.import"></button>
  6019. </div>
  6020. <hr>
  6021. <h5 data-i18n="settings:dev.configuration.title"></h5>
  6022. <div class='OJBetter_setting_list'>
  6023. <label><span data-i18n="settings:dev.configuration.clear.label"></span></label>
  6024. <div class="help_tip">
  6025. ${helpCircleHTML}
  6026. <div class="tip_text" data-i18n="[html]settings:dev.configuration.clear.helpText"></div>
  6027. </div>
  6028. <button type="button" id="configuration_clearButton" name="configuration_clearButton" data-i18n="settings:dev.configuration.clear.button"></button>
  6029. </div>
  6030. <div class='OJBetter_setting_list'>
  6031. <label><span data-i18n="settings:dev.configuration.inputOrExport.label"></span></label>
  6032. <div class="help_tip">
  6033. ${helpCircleHTML}
  6034. <div class="tip_text" data-i18n="[html]settings:dev.configuration.inputOrExport.helpText"></div>
  6035. </div>
  6036. <button type="button" id="configuration_exportButton" name="configuration_exportButton" data-i18n="settings:dev.configuration.inputOrExport.export"></button>
  6037. <button type="button" id="configuration_importButton" name="configuration_importButton" data-i18n="settings:dev.configuration.inputOrExport.import"></button>
  6038. </div>
  6039. </div>
  6040. `;
  6041.  
  6042. const about_settings_HTML = `
  6043. <div id="about-settings" class="settings-page">
  6044. <h3 data-i18n="settings:about.title"></h3>
  6045. <hr>
  6046. <div class='versionInfo'>
  6047. <p>${OJBetter.state.name}</p>
  6048. <p><span data-i18n="settings:about.version"></span><span id="nowVersion">${OJBetter.state.version}</span></p>
  6049. <p> @北极小狐 <a target="_blank" href="https://github.com/beijixiaohu/OJBetter">Github</a>
  6050. <a target="_blank" href="https://greasyfork.org/zh-CN/scripts/465777">GreasyFork</a></p>
  6051. </div>
  6052. <hr>
  6053. <h5 data-i18n="settings:about.update.title"></h5>
  6054. <div id="thanksforDevChannelNotice" class='OJBetter_setting_list alert_info'>
  6055. <div data-i18n="[html]settings:about.update.thanksforDevChannelNotice"} data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }' ></div>
  6056. </div>
  6057. <div class='OJBetter_setting_list'>
  6058. <label for="updateChannel"><span data-i18n="settings:about.update.channel.label"></span></label>
  6059. <div class="help_tip">
  6060. ${helpCircleHTML}
  6061. <div class="tip_text" data-i18n="[html]settings:about.update.channel.helpText"></div>
  6062. </div>
  6063. <select id="updateChannel" name="updateChannel">
  6064. <option value="release" data-i18n="settings:about.update.channel.options.release"></option>
  6065. <option value="dev" data-i18n="settings:about.update.channel.options.dev"></option>
  6066. </select>
  6067. </div>
  6068. <div class='OJBetter_setting_list'>
  6069. <label for="updateSource"><span data-i18n="settings:about.update.source.label"></span></label>
  6070. <div class="help_tip">
  6071. ${helpCircleHTML}
  6072. <div class="tip_text" data-i18n="[html]settings:about.update.source.helpText"></div>
  6073. </div>
  6074. <select id="updateSource" name="updateSource">
  6075. <option value="greasyfork" data-i18n="settings:about.update.source.options.greasyfork"></option>
  6076. <option value="github" data-i18n="settings:about.update.source.options.github"></option>
  6077. <option value="aliyunoss" data-i18n="settings:about.update.source.options.aliyunoss"></option>
  6078. </select>
  6079. </div>
  6080. </div>
  6081. `;
  6082.  
  6083. const OJBetter_setting_content_HTML = `
  6084. <div class="OJBetter_setting_content">
  6085. ${basic_settings_HTML}
  6086. ${l10n_settings_HTML}
  6087. ${translation_settings_HTML}
  6088. ${clist_rating_settings_HTML}
  6089. ${code_editor_settings_HTML}
  6090. ${preference_settings_HTML}
  6091. ${dev_settings_HTML}
  6092. ${about_settings_HTML}
  6093. </div>
  6094. `;
  6095.  
  6096. // 设置界面HTML
  6097. const OJBetterSettingMenu_HTML = `
  6098. <dialog class='OJBetter_setting_menu' id='OJBetter_setting_menu'>
  6099. <div class="tool-box">
  6100. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6101. <i class="iconfont">&#xe614;</i>
  6102. </button>
  6103. </div>
  6104. <div class="OJBetter_setting_container">
  6105. ${OJBetter_setting_sidebar_HTML}
  6106. ${OJBetter_setting_content_HTML}
  6107. </div>
  6108. </dialog>
  6109. `;
  6110.  
  6111. const apiCustomConfigHTML = (prefix) => {
  6112. return `
  6113. <div class="OJBetter_setting_list">
  6114. <label for='${prefix}_header'>
  6115. <div style="display: flex;align-items: center;">
  6116. <span class="input_label" data-i18n="config:common.advanced.header.label"></span>
  6117. <div class="help_tip">
  6118. ${helpCircleHTML}
  6119. <div class="tip_text" data-i18n="[html]config:common.advanced.header.tipText"></div>
  6120. </div>
  6121. </div>
  6122. </label>
  6123. <textarea id="${prefix}_header" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.header.placeholder"></textarea>
  6124. </div>
  6125. <div class="OJBetter_setting_list">
  6126. <label for='${prefix}_data'>
  6127. <div style="display: flex;align-items: center;">
  6128. <span class="input_label" data-i18n="config:common.advanced.data.label"></span>
  6129. <div class="help_tip">
  6130. ${helpCircleHTML}
  6131. <div class="tip_text" data-i18n="[html]config:common.advanced.data.tipText"></div>
  6132. </div>
  6133. </div>
  6134. </label>
  6135. <textarea id="${prefix}_data" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.data.placeholder"></textarea>
  6136. </div>
  6137. `;
  6138. };
  6139.  
  6140. const apiQuotaConfigHTML = (prefix) => {
  6141. return `
  6142. <div class="OJBetter_setting_list">
  6143. <label for='${prefix}_quota_url'>
  6144. <div style="display: flex;align-items: center;">
  6145. <span class="input_label" data-i18n="config:common.quota.url.label"></span>
  6146. <div class="help_tip">
  6147. ${helpCircleHTML}
  6148. <div class="tip_text" data-i18n="[html]config:common.quota.url.tipText"></div>
  6149. </div>
  6150. </div>
  6151. </label>
  6152. <input type='text' id='${prefix}_quota_url' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.url.placeholder">
  6153. </div>
  6154. <div class="OJBetter_setting_list">
  6155. <label for="${prefix}_quota_method" style="display: flex;" data-i18n="config:common.quota.method.label"></label>
  6156. <div class="help_tip">
  6157. ${helpCircleHTML}
  6158. <div class="tip_text" data-i18n="[html]config:common.quota.method.tipText"></div>
  6159. </div>
  6160. <select id="${prefix}_quota_method" name="${prefix}_quota_method">
  6161. <option value="get">GET</option>
  6162. <option value="post">POST</option>
  6163. </select>
  6164. </div>
  6165. <div class="OJBetter_setting_list">
  6166. <label for='${prefix}_quota_header'>
  6167. <div style="display: flex;align-items: center;">
  6168. <span class="input_label" data-i18n="config:common.quota.header.label"></span>
  6169. <div class="help_tip">
  6170. ${helpCircleHTML}
  6171. <div class="tip_text" data-i18n="[html]config:common.quota.header.tipText"></div>
  6172. </div>
  6173. </div>
  6174. </label>
  6175. <textarea id="${prefix}_quota_header" placeholder='' require = false data-i18n="[placeholder]config:common.quota.header.placeholder"></textarea>
  6176. </div>
  6177. <div class="OJBetter_setting_list">
  6178. <label for='${prefix}_quota_data'>
  6179. <div style="display: flex;align-items: center;">
  6180. <span class="input_label" data-i18n="config:common.quota.data.label"></span>
  6181. <div class="help_tip">
  6182. ${helpCircleHTML}
  6183. <div class="tip_text" data-i18n="[html]config:common.quota.data.tipText"></div>
  6184. </div>
  6185. </div>
  6186. </label>
  6187. <textarea id="${prefix}_quota_data" placeholder='' require = false data-i18n="[placeholder]config:common.quota.data.placeholder"></textarea>
  6188. </div>
  6189. <div class="OJBetter_setting_list">
  6190. <div style="display: flex;align-items: center;">
  6191. <span class="input_label" data-i18n="config:common.quota.surplus.label"></span>
  6192. <div class="help_tip">
  6193. ${helpCircleHTML}
  6194. <div class="tip_text" data-i18n="[html]config:common.quota.surplus.tipText"></div>
  6195. </div>
  6196. </div>
  6197. </label>
  6198. <input type='text' id='${prefix}_quota_surplus' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.surplus.placeholder">
  6199. </div>
  6200. `;
  6201. }
  6202.  
  6203. const deeplConfigEditHTML = `
  6204. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6205. <div class='OJBetter_setting_content'>
  6206. <div class="tool-box">
  6207. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6208. <i class="iconfont">&#xe614;</i>
  6209. </button>
  6210. </div>
  6211. <h4 data-i18n="config:deepl.title"></h4>
  6212. <h5 data-i18n="config:deepl.basic.title"></h5>
  6213. <hr>
  6214. <div class="OJBetter_setting_list">
  6215. <label for='name'>
  6216. <span class="input_label" data-i18n="config:deepl.basic.name.label"></span>
  6217. </label>
  6218. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.name.placeholder">
  6219. </div>
  6220. <div class='OJBetter_setting_list'>
  6221. <label for="deepl_apiGenre" style="display: flex;" data-i18n="config:deepl.genre.label"></label>
  6222. <div class="help_tip">
  6223. ${helpCircleHTML}
  6224. <div class="tip_text" data-i18n="[html]config:deepl.genre.tipText"></div>
  6225. </div>
  6226. <select id="deepl_apiGenre" name="deepl_apiGenre">
  6227. <option value="api-free">api-free</option>
  6228. <option value="api-pro">api-pro</option>
  6229. <option value="deeplx">deeplx</option>
  6230. </select>
  6231. </div>
  6232. <div class="OJBetter_setting_list">
  6233. <label for='deepl_key'>
  6234. <div style="display: flex;align-items: center;">
  6235. <span class="input_label" data-i18n="config:deepl.basic.key.label"></span>
  6236. <div class="help_tip">
  6237. ${helpCircleHTML}
  6238. <div class="tip_text" data-i18n="[html]config:deepl.basic.key.tipText"></div>
  6239. </div>
  6240. </div>
  6241. </label>
  6242. <input type='text' id='deepl_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.key.placeholder">
  6243. </div>
  6244. <div class="OJBetter_setting_list">
  6245. <label for='deepl_proxy'>
  6246. <div style="display: flex;align-items: center;">
  6247. <span class="input_label" data-i18n="config:deepl.basic.proxy.label">Proxy API:</span>
  6248. <div class="help_tip">
  6249. ${helpCircleHTML}
  6250. <div class="tip_text" data-i18n="[html]config:deepl.basic.proxy.tipText"></div>
  6251. </div>
  6252. </div>
  6253. </label>
  6254. <input type='text' id='deepl_proxy' placeholder='' require = false>
  6255. </div>
  6256. <hr>
  6257. <details>
  6258. <summary data-i18n="config:common.advanced.title"></summary>
  6259. ${apiCustomConfigHTML('deepl')}
  6260. </details>
  6261. <details>
  6262. <summary data-i18n="config:common.quota.title"></summary>
  6263. ${apiQuotaConfigHTML('deepl')}
  6264. </details>
  6265. <button id='tempConfig_save' data-i18n="common:save"></button>
  6266. </div>
  6267. </dialog>
  6268. `;
  6269.  
  6270. const chatgptConfigEditHTML = `
  6271. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6272. <div class='OJBetter_setting_content'>
  6273. <div class="tool-box">
  6274. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6275. <i class="iconfont">&#xe614;</i>
  6276. </button>
  6277. </div>
  6278. <h4 data-i18n="config:chatgpt.title"></h4>
  6279. <h5 data-i18n="config:chatgpt.basic.title"></h5>
  6280. <hr>
  6281. <div class="OJBetter_setting_list">
  6282. <label for='name'>
  6283. <span class="input_label" data-i18n="config:chatgpt.basic.name.label"></span>
  6284. </label>
  6285. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.name.placeholder">
  6286. </div>
  6287. <div class="OJBetter_setting_list">
  6288. <label for='chatgpt_model'>
  6289. <div style="display: flex;align-items: center;">
  6290. <span class="input_label" data-i18n="[html]config:chatgpt.basic.model.label"></span>
  6291. <div class="help_tip">
  6292. ${helpCircleHTML}
  6293. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.model.tipText"></div>
  6294. </div>
  6295. </div>
  6296. </label>
  6297. <input type='text' id='chatgpt_model' placeholder='gpt-3.5-turbo' require = false>
  6298. </div>
  6299. <div class="OJBetter_setting_list">
  6300. <label for='chatgpt_key'>
  6301. <div style="display: flex;align-items: center;">
  6302. <span class="input_label" data-i18n="config:chatgpt.basic.key.label"></span>
  6303. <div class="help_tip">
  6304. ${helpCircleHTML}
  6305. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.key.tipText"></div>
  6306. </div>
  6307. </div>
  6308. </label>
  6309. <input type='text' id='chatgpt_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.key.placeholder">
  6310. </div>
  6311. <div class="OJBetter_setting_list">
  6312. <label for='chatgpt_proxy'>
  6313. <div style="display: flex;align-items: center;">
  6314. <span class="input_label" data-i18n="config:chatgpt.basic.proxy.label">Proxy API:</span>
  6315. <div class="help_tip">
  6316. ${helpCircleHTML}
  6317. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.proxy.tipText"></div>
  6318. </div>
  6319. </div>
  6320. </label>
  6321. <input type='text' id='chatgpt_proxy' placeholder='https://api.openai.com/v1/chat/completions' require = false>
  6322. </div>
  6323. <hr>
  6324. <details>
  6325. <summary data-i18n="config:common.advanced.title"></summary>
  6326. ${apiCustomConfigHTML('chatgpt')}
  6327. </details>
  6328. <details>
  6329. <summary data-i18n="config:common.quota.title"></summary>
  6330. ${apiQuotaConfigHTML('chatgpt')}
  6331. </details>
  6332. <button id='tempConfig_save' data-i18n="common:save"></button>
  6333. </div>
  6334. </dialog>
  6335. `;
  6336.  
  6337. const CompletConfigEditHTML = `
  6338. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6339. <div class='OJBetter_setting_content'>
  6340. <div class="tool-box">
  6341. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6342. <i class="iconfont">&#xe614;</i>
  6343. </button>
  6344. </div>
  6345. <h4 data-i18n="config:complet.title"></h4>
  6346. <hr>
  6347. <div class="OJBetter_setting_list">
  6348. <label for='name'>
  6349. <span class="input_label" data-i18n="config:complet.name.label"></span>
  6350. </label>
  6351. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.name.placeholder">
  6352. </div>
  6353. <div class='OJBetter_setting_list'>
  6354. <label for="complet_isChoose"><span id="loaded_span" data-i18n="config:complet.choose.label"></span></label>
  6355. <input type="checkbox" id="complet_isChoose" name="complet_isChoose" require = false>
  6356. </div>
  6357. <div class='OJBetter_setting_list'>
  6358. <label for="complet_genre" style="display: flex;" data-i18n="config:complet.genre.label"></label>
  6359. <div class="help_tip">
  6360. ${helpCircleHTML}
  6361. <div class="tip_text" data-i18n="[html]config:complet.genre.tipText"></div>
  6362. </div>
  6363. <select id="complet_genre" name="complet_genre">
  6364. <option value="monaco">monaco</option>
  6365. <option value="ace">ace</option>
  6366. </select>
  6367. </div>
  6368. <div class='OJBetter_setting_list'>
  6369. <label for="complet_language" style="display: flex;" data-i18n="config:complet.language.label"></label>
  6370. <select id="complet_language" name="complet_language">
  6371. <option value="cpp">cpp</option>
  6372. <option value="python">python</option>
  6373. <option value="java">java</option>
  6374. <option value="c">c</option>
  6375. </select>
  6376. </div>
  6377. <div class="OJBetter_setting_list">
  6378. <label for='complet_jsonUrl'>
  6379. <div style="display: flex;align-items: center;">
  6380. <span class="input_label">JSON URL:</span>
  6381. <div class="help_tip">
  6382. ${helpCircleHTML}
  6383. <div class="tip_text" data-i18n="[html]config:complet.jsonurl.tipText"></div>
  6384. </div>
  6385. </div>
  6386. </label>
  6387. <div class='OJBetter_setting_list alert_warn' data-i18n="[html]config:complet.jsonurl.alert"></div>
  6388. <input type='text' id='complet_jsonUrl' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.jsonurl.placeholder">
  6389. </div>
  6390. <button id='tempConfig_save' data-i18n="common:save"></button>
  6391. </div>
  6392. </dialog>
  6393. `;
  6394.  
  6395. /**
  6396. * 加载设置按钮面板
  6397. */
  6398. async function initSettingsPanel() {
  6399. /**
  6400. * 添加右上角设置按钮
  6401. * @param {string} location 位置选择器
  6402. * @param {string} method 插入方法
  6403. */
  6404. function insertOJBetterSettingButton(location, method) {
  6405. $(location)[method](`<button class='ojb_btn OJBetter_setting'>
  6406. ${OJBetter.state.name} ${i18next.t('settings', { ns: 'common' })}</button>`);
  6407. }
  6408.  
  6409. /**
  6410. * ============================================
  6411. * 该网站插入设置按钮的位置和方式
  6412. */
  6413. insertOJBetterSettingButton(".lang-chooser", "before");
  6414. insertOJBetterSettingButton(".enter-or-register-box", "after");
  6415. if (OJBetter.typeOfPage.is_completeProblemset) insertOJBetterSettingButton(".lang", "before");
  6416. /**
  6417. * ============================================
  6418. */
  6419.  
  6420. const $settingBtns = $(".OJBetter_setting");
  6421. $settingBtns.click(() => {
  6422. $settingBtns.prop("disabled", true).addClass("open");
  6423.  
  6424. // 设置面板div
  6425. const settingMenu = OJB_safeCreateJQElement(OJBetterSettingMenu_HTML);
  6426. $("body").append(settingMenu);
  6427.  
  6428. elementLocalize(settingMenu); // 加载i18n
  6429. OJB_showModal(settingMenu);
  6430. OJB_addDraggable($('#OJBetter_setting_menu')); // 窗口支持拖拽
  6431.  
  6432. // help帮助悬浮窗位置更新
  6433. $(document).on('mouseenter', '.help-icon', function (event) {
  6434. var menuOffset = $('.OJBetter_setting_menu:last').offset();
  6435. var mouseX = event.pageX - menuOffset.left;
  6436. var mouseY = event.pageY - menuOffset.top;
  6437.  
  6438. $('.tip_text').css({
  6439. 'top': mouseY + 'px',
  6440. 'left': mouseX + 'px'
  6441. });
  6442. });
  6443.  
  6444. // 选项卡切换
  6445. $('.OJBetter_setting_sidebar a').click(function (event) {
  6446. event.preventDefault();
  6447. $('.OJBetter_setting_sidebar a').removeClass('active');
  6448. $('.settings-page').removeClass('active');
  6449. $(this).addClass('active');
  6450. const targetPageId = $(this).attr('href').substring(1);
  6451. $('#' + targetPageId).addClass('active');
  6452. });
  6453.  
  6454. /**
  6455. * 更新单选按钮组的可用状态
  6456. * @param {string} selector 单选按钮组的选择器
  6457. * @param {string} targetLanguage 目标语言
  6458. * @param {Object} translationSupport 翻译支持的语言对应表
  6459. */
  6460. const updateRadioButtonsAvailability = (selector, targetLanguage) => {
  6461. Object.entries(OJBetter.supportList.translationSupport).forEach(([service, languages]) => {
  6462. const radioButton = $(selector).find(`input[value="${service}"]`);
  6463. const isEnabled = languages[targetLanguage];
  6464. $(radioButton).prop('disabled', !isEnabled);
  6465. if (!isEnabled) {
  6466. $(radioButton).prop('checked', false);
  6467. }
  6468. });
  6469. };
  6470.  
  6471. /**
  6472. * 检查下拉框选中项是否有效,若无效则清空
  6473. * @param {string} selector 下拉框的选择器
  6474. */
  6475. const validateSelectOption = (selector) => {
  6476. const selectedValue = $(selector).val();
  6477. if (!selectedValue) {
  6478. $(selector).val('');
  6479. }
  6480. };
  6481.  
  6482. /**
  6483. * 更新翻译目标语言下拉框的可用状态
  6484. * @param {string} selector 下拉框的选择器
  6485. * @param {string} targetLanguage 目标语言
  6486. * @param {Object} translationSupport 翻译支持的语言对应表
  6487. */
  6488. const updateSelectOptionsAvailability = (selector, targetLanguage) => {
  6489. $(selector).children('option').each(function () {
  6490. const optionValue = $(this).val();
  6491. const isEnabled = OJBetter.supportList.translationSupport[optionValue] ? OJBetter.supportList.translationSupport[optionValue][targetLanguage] : true;
  6492. $(this).prop('disabled', !isEnabled);
  6493. });
  6494. validateSelectOption(selector);
  6495. };
  6496.  
  6497. /**
  6498. * 更新翻译服务复选框的可用状态
  6499. * @param {string} selector 复选框的选择器
  6500. * @param {string} targetLanguage 目标语言
  6501. * @param {Object} translationSupport 翻译支持的语言对应表
  6502. */
  6503. const updateCheckboxesAvailability = (selector, targetLanguage) => {
  6504. $(selector).children('input').each(function () {
  6505. const checkboxValue = $(this).val();
  6506. const isEnabled = OJBetter.supportList.translationSupport[checkboxValue][targetLanguage];
  6507. $(this).prop('disabled', !isEnabled);
  6508. if (!isEnabled) {
  6509. $(this).prop('checked', false);
  6510. }
  6511. });
  6512. };
  6513.  
  6514. /**
  6515. * 更新更新源下拉框的可用状态
  6516. * @param {string} selector 下拉框的选择器
  6517. * @param {string} targetLanguage 目标语言
  6518. * @param {Object} translationSupport 翻译支持的语言对应表
  6519. */
  6520. const updateUpdateSourceSelectOptionsAvailability = (selector, updateChannel) => {
  6521. $(selector).children('option').each(function () {
  6522. const optionValue = $(this).val();
  6523. const isEnabled = OJBetter.supportList.updateSourceSupportList[optionValue][updateChannel];
  6524. $(this).prop('disabled', !isEnabled);
  6525. });
  6526. validateSelectOption(selector);
  6527. };
  6528.  
  6529. /**
  6530. * 创建配置结构
  6531. * @param {string} type - 该字段的在表单中的类型
  6532. * @param {string} value - 在配置中的键值
  6533. * @param {boolean} require - 是否是表单的必填项
  6534. * @param {string} [check=""] check - 调用的合法性检查
  6535. */
  6536. function createStructure(type, value, require, check = "") {
  6537. return { type, value, require, check };
  6538. }
  6539.  
  6540. // deepl配置
  6541. const deeplStructure = {
  6542. '#name': createStructure('text', 'name', true),
  6543. '#deepl_apiGenre': createStructure('text', 'apiGenre', true),
  6544. '#deepl_key': createStructure('text', 'key', false),
  6545. '#deepl_proxy': createStructure('text', 'proxy', false),
  6546. '#deepl_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6547. '#deepl_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6548. '#deepl_quota_url': createStructure('text', 'quota_url', false),
  6549. '#deepl_quota_method': createStructure('text', 'quota_method', false),
  6550. '#deepl_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6551. '#deepl_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6552. '#deepl_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6553. };
  6554. let tempConfig_deepl = GM_getValue('deepl_config'); // 获取配置信息
  6555. const configManager_deepl = new ConfigManager('#deepl_config', 'deepl_config_', tempConfig_deepl, deeplStructure, deeplConfigEditHTML);
  6556. configManager_deepl.registerChoiceChange();
  6557.  
  6558. // chatgpt配置
  6559. const chatgptStructure = {
  6560. '#name': createStructure('text', 'name', true),
  6561. '#chatgpt_model': createStructure('text', 'model', false),
  6562. '#chatgpt_key': createStructure('text', 'key', true),
  6563. '#chatgpt_proxy': createStructure('text', 'proxy', false),
  6564. '#chatgpt_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6565. '#chatgpt_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6566. '#chatgpt_quota_url': createStructure('text', 'quota_url', false),
  6567. '#chatgpt_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6568. '#chatgpt_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6569. '#chatgpt_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6570. '#chatgpt_quota_method': createStructure('text', 'quota_method', false),
  6571. };
  6572. let tempConfig_chatgpt = GM_getValue('chatgpt_config'); // 获取配置信息
  6573. const configManager_chatgpt = new ConfigManager('#chatgpt_config', 'chatgpt_config_', tempConfig_chatgpt, chatgptStructure, chatgptConfigEditHTML);
  6574. configManager_chatgpt.registerChoiceChange();
  6575.  
  6576. // Complet配置
  6577. const CompletStructure = {
  6578. '#name': createStructure('text', 'name', true),
  6579. '#complet_isChoose': createStructure('checkbox', 'isChoose', true),
  6580. '#complet_genre': createStructure('text', 'genre', true),
  6581. '#complet_language': createStructure('text', 'language', true),
  6582. '#complet_jsonUrl': createStructure('text', 'jsonUrl', true)
  6583. };
  6584. let tempConfig_Complet = GM_getValue('Complet_config'); // 获取配置信息
  6585. const configManager_complet = new ConfigManager('#Complet_config', 'complet_config_', tempConfig_Complet, CompletStructure, CompletConfigEditHTML, false);
  6586.  
  6587. // 状态更新
  6588. $("input[name='darkMode'][value='" + OJBetter.basic.darkMode + "']").prop("checked", true);
  6589. $("#showLoading").prop("checked", GM_getValue("showLoading") === true);
  6590. $("#expandFoldingblocks").prop("checked", GM_getValue("expandFoldingblocks") === true);
  6591. $("#renderPerfOpt").prop("checked", GM_getValue("renderPerfOpt") === true);
  6592. $("#selectElementPerfOpt").prop("checked", GM_getValue("selectElementPerfOpt") === true);
  6593. $("#commentPaging").prop("checked", GM_getValue("commentPaging") === true);
  6594. $("#standingsRecolor").prop("checked", GM_getValue("standingsRecolor") === true);
  6595. $("#showJumpToLuogu").prop("checked", GM_getValue("showJumpToLuogu") === true);
  6596. $("#showCF2vjudge").prop("checked", GM_getValue("showCF2vjudge") === true);
  6597. $("#hoverTargetAreaDisplay").prop("checked", GM_getValue("hoverTargetAreaDisplay") === true);
  6598. $("#showClistRating_contest").prop("checked", GM_getValue("showClistRating_contest") === true);
  6599. $("#showClistRating_problemset").prop("checked", GM_getValue("showClistRating_problemset") === true);
  6600. $("#showClistRating_problem").prop("checked", GM_getValue("showClistRating_problem") === true);
  6601. $("#RatingHidden").prop("checked", GM_getValue("RatingHidden") === true);
  6602. $('#scriptL10nLanguage').val(GM_getValue("scriptL10nLanguage"));
  6603. $('#localizationLanguage').val(GM_getValue("localizationLanguage"));
  6604. $("input[name='translation'][value='" + OJBetter.translation.choice + "']").prop("checked", true);
  6605. $("input[name='translation']").css("color", "gray");
  6606. $('#deepl_type').val(GM_getValue("deepl_type"));
  6607. $("#deepl_config_config_bar_ul").find(`input[name='deepl_config_config_item'][value='${tempConfig_deepl.choice}']`).prop("checked", true);
  6608. $('#enableEmphasisProtection').prop("checked", GM_getValue("enableEmphasisProtection") === true);
  6609. $('#enableLinkProtection').prop("checked", GM_getValue("enableLinkProtection") === true);
  6610. $("#chatgpt_config_config_bar_ul").find(`input[name='chatgpt_config_config_item'][value='${tempConfig_chatgpt.choice}']`).prop("checked", true);
  6611. $("#openai_isStream").prop("checked", GM_getValue("openai_isStream") === true);
  6612. $('#comment_translation_choice').val(GM_getValue("commentTranslationChoice"));
  6613. $('#iconButtonSize').val(GM_getValue("iconButtonSize"));
  6614. $("#autoTranslation").prop("checked", GM_getValue("autoTranslation") === true);
  6615. $('#shortTextLength').val(GM_getValue("shortTextLength"));
  6616. $("#allowMixTrans").prop("checked", GM_getValue("allowMixTrans") === true);
  6617. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6618. if (OJBetter.translation.auto.mixTrans.servers.indexOf($(this).val()) > -1) {
  6619. $(this).prop('checked', true);
  6620. }
  6621. });
  6622. // 翻译目标语言下拉框
  6623. $('#transTargetLang').change(function () {
  6624. var selectedLang = $(this).val();
  6625. updateRadioButtonsAvailability('#translationServices', selectedLang);
  6626. updateSelectOptionsAvailability('#comment_translation_choice', selectedLang);
  6627. updateCheckboxesAvailability('.OJBetter_checkboxs', selectedLang);
  6628. });
  6629. $('#transTargetLang').val(GM_getValue("transTargetLang"));
  6630. $('#transTargetLang').change();
  6631. //
  6632. $('#comment_translation_mode').val(GM_getValue("commentTranslationMode"));
  6633. $("#memoryTranslateHistory").prop("checked", GM_getValue("memoryTranslateHistory") === true);
  6634. $('#transWaitTime').val(GM_getValue("transWaitTime"));
  6635. $('#translation_replaceSymbol').val(GM_getValue("replaceSymbol"));
  6636. $("#filterTextWithoutEmphasis").prop("checked", GM_getValue("filterTextWithoutEmphasis") === true);
  6637. $('#translation_retransAction').val(GM_getValue("retransAction"));
  6638. $("#clist_Authorization").val(GM_getValue("clist_Authorization"));
  6639. $("#problemPageCodeEditor").prop("checked", GM_getValue("problemPageCodeEditor") === true);
  6640. $("#beautifyPreBlocks").prop("checked", GM_getValue("beautifyPreBlocks") === true);
  6641. $("#isCodeSubmitConfirm").prop("checked", GM_getValue("isCodeSubmitConfirm") === true);
  6642. $("#alwaysConsumeMouseWheel").prop("checked", GM_getValue("alwaysConsumeMouseWheel") === true);
  6643. $("#submitButtonPosition").val(GM_getValue("submitButtonPosition"));
  6644. $("#cppCodeTemplateComplete").prop("checked", GM_getValue("cppCodeTemplateComplete") === true);
  6645. $("#useLSP").prop("checked", GM_getValue("useLSP") === true);
  6646. $("#OJBetter_Bridge_WorkUri").val(GM_getValue("OJBetter_Bridge_WorkUri"));
  6647. $("#OJBetter_Bridge_SocketUrl").val(GM_getValue("OJBetter_Bridge_SocketUrl"));
  6648. $("input[name='compiler'][value='" + OJBetter.monaco.onlineCompilerChoice + "']").prop("checked", true);
  6649. $("input[name='compiler']").css("color", "gray");
  6650. // 调试
  6651. $("#notWaiteLoaded").prop("checked", GM_getValue("notWaiteLoaded") === true);
  6652. $('#l10n_refreshScrpitCacheButton').click(clearI18nextCache);
  6653. $('#l10n_web_refreshScrpitCacheButton').click(clearWebsiteL10nData);
  6654. $("#isRuleMarkingEnabled").prop("checked", GM_getValue("isRuleMarkingEnabled") === true);
  6655. $('#indexedDB_clearButton').click(async () => { await clearDatabase(); });
  6656. $('#indexedDB_exportButton').click(async () => { downloadDataAsFile(await exportDatabase(), 'database_export.json') });
  6657. $('#indexedDB_importButton').click(() => { readFileInput(async (fileContent) => { await importDatabase(fileContent); }); });
  6658. $('#configuration_clearButton').click(deleteAllConfigSettings);
  6659. $('#configuration_exportButton').click(() => { downloadDataAsFile(exportSettingsToJSON(), 'configuration_export.json') });
  6660. $('#configuration_importButton').click(() => { readFileInput((fileContent) => { importSettingsFromJSON(fileContent); }) });
  6661. // 关于
  6662. $('#updateChannel').val(GM_getValue("updateChannel"));
  6663. $('#updateSource').val(GM_getValue("updateSource"));
  6664. $('#updateChannel').change(function () {
  6665. var selectedLang = $(this).val();
  6666. updateUpdateSourceSelectOptionsAvailability('#updateSource', selectedLang);
  6667. if (selectedLang == "dev") $('#thanksforDevChannelNotice').show();
  6668. else $('#thanksforDevChannelNotice').hide();
  6669. });
  6670. $('#updateChannel').change();
  6671.  
  6672. // 关闭
  6673. const $settingMenu = $(".OJBetter_setting_menu");
  6674. $settingMenu.on("click", ".btn-close", async () => {
  6675. // 设置的数据
  6676. const settings = {
  6677. darkMode: $("input[name='darkMode']:checked").val(),
  6678. showLoading: $("#showLoading").prop("checked"),
  6679. hoverTargetAreaDisplay: $("#hoverTargetAreaDisplay").prop("checked"),
  6680. expandFoldingblocks: $("#expandFoldingblocks").prop("checked"),
  6681. renderPerfOpt: $("#renderPerfOpt").prop("checked"),
  6682. selectElementPerfOpt: $("#selectElementPerfOpt").prop("checked"),
  6683. commentPaging: $("#commentPaging").prop("checked"),
  6684. standingsRecolor: $("#standingsRecolor").prop("checked"),
  6685. showJumpToLuogu: $("#showJumpToLuogu").prop("checked"),
  6686. showCF2vjudge: $("#showCF2vjudge").prop("checked"),
  6687. scriptL10nLanguage: $('#scriptL10nLanguage').val(),
  6688. localizationLanguage: $('#localizationLanguage').val(),
  6689. transTargetLang: $('#transTargetLang').val(),
  6690. translation: $("input[name='translation']:checked").val(),
  6691. deepl_type: $('#deepl_type').val(),
  6692. enableEmphasisProtection: $("#enableEmphasisProtection").prop("checked"),
  6693. enableLinkProtection: $("#enableLinkProtection").prop("checked"),
  6694. openai_isStream: $("#openai_isStream").prop("checked"),
  6695. commentTranslationChoice: $('#comment_translation_choice').val(),
  6696. iconButtonSize: $('#iconButtonSize').val(),
  6697. autoTranslation: $("#autoTranslation").prop("checked"),
  6698. shortTextLength: $('#shortTextLength').val(),
  6699. allowMixTrans: $("#allowMixTrans").prop("checked"),
  6700. mixedTranslation: (() => {
  6701. let mixedTranslation = [];
  6702. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6703. if ($(this).is(":checked")) {
  6704. mixedTranslation.push($(this).val());
  6705. }
  6706. });
  6707. return mixedTranslation;
  6708. })(),
  6709. commentTranslationMode: $('#comment_translation_mode').val(),
  6710. memoryTranslateHistory: $('#memoryTranslateHistory').prop("checked"),
  6711. transWaitTime: $('#transWaitTime').val(),
  6712. replaceSymbol: $('#translation_replaceSymbol').val(),
  6713. filterTextWithoutEmphasis: $('#filterTextWithoutEmphasis').prop("checked"),
  6714. retransAction: $('#translation_retransAction').val(),
  6715. showClistRating_contest: $('#showClistRating_contest').prop("checked"),
  6716. showClistRating_problemset: $('#showClistRating_problemset').prop("checked"),
  6717. showClistRating_problem: $('#showClistRating_problem').prop("checked"),
  6718. RatingHidden: $('#RatingHidden').prop("checked"),
  6719. clist_Authorization: $('#clist_Authorization').val(),
  6720. problemPageCodeEditor: $("#problemPageCodeEditor").prop("checked"),
  6721. beautifyPreBlocks: $("#beautifyPreBlocks").prop("checked"),
  6722. isCodeSubmitConfirm: $("#isCodeSubmitConfirm").prop("checked"),
  6723. alwaysConsumeMouseWheel: $("#alwaysConsumeMouseWheel").prop("checked"),
  6724. submitButtonPosition: $('#submitButtonPosition').val(),
  6725. cppCodeTemplateComplete: $("#cppCodeTemplateComplete").prop("checked"),
  6726. useLSP: $("#useLSP").prop("checked"),
  6727. OJBetter_Bridge_WorkUri: $('#OJBetter_Bridge_WorkUri').val().replace(/\\/g, '/').replace(/\/$/, ''),
  6728. OJBetter_Bridge_SocketUrl: $('#OJBetter_Bridge_SocketUrl').val(),
  6729. onlineCompilerChoice: $("input[name='compiler']:checked").val(),
  6730. notWaiteLoaded: $("#notWaiteLoaded").prop("checked"),
  6731. isRuleMarkingEnabled: $("#isRuleMarkingEnabled").prop("checked"),
  6732. updateChannel: $('#updateChannel').val(),
  6733. updateSource: $('#updateSource').val()
  6734. };
  6735. // tempConfigs的数据
  6736. const tempConfigs = {
  6737. 'deepl_config': configManager_deepl.getTempConfig(),
  6738. 'chatgpt_config': configManager_chatgpt.getTempConfig(),
  6739. 'Complet_config': configManager_complet.getTempConfig()
  6740. }
  6741.  
  6742. // 判断是否改变
  6743. let changes = {};
  6744. const combinedConfigs = Object.assign({}, settings, tempConfigs); // 合并settings和tempConfigs对象
  6745. for (const [key, value] of Object.entries(combinedConfigs)) {
  6746. const storedValue = GM_getValue(key);
  6747. if (!OJB_deepEquals(value, storedValue)) {
  6748. changes[key] = { oldValue: storedValue, newValue: value };
  6749. }
  6750. }
  6751.  
  6752. // 如果changes对象不为空,则有变化
  6753. if (Object.keys(changes).length > 0) {
  6754. console.log("Changes detected:", changes);
  6755. const shouldSave = await OJB_createDialog(
  6756. i18next.t('saveSetting.title', { ns: 'dialog' }),
  6757. i18next.t('saveSetting.content', { ns: 'dialog' }),
  6758. [
  6759. i18next.t('saveSetting.buttons.0', { ns: 'dialog' }),
  6760. i18next.t('saveSetting.buttons.1', { ns: 'dialog' })
  6761. ]
  6762. ); // 配置改变保存确认
  6763. if (shouldSave) {
  6764. // 数据校验
  6765. // TODO
  6766. if (settings.deepl_type !== 'free') {
  6767. let selectedIndex = $('input[name="deepl_config_config_item"]:checked').length > 0;
  6768. if (!selectedIndex) {
  6769. $('.deepl_config a').removeClass('active');
  6770. $('.settings-page').removeClass('active');
  6771. $('#sidebar-translation-settings').addClass('active');
  6772. $('#translation-settings').addClass('active');
  6773.  
  6774. $('#deepl_config').addClass('missing');
  6775. return;
  6776. } else {
  6777. $('#deepl_config').removeClass('missing');
  6778. }
  6779. }
  6780. if (settings.translation === "openai") {
  6781. let selectedIndex = $('input[name="chatgpt_config_config_item"]:checked').length > 0;
  6782. if (!selectedIndex) {
  6783. $('.chatgpt_config a').removeClass('active');
  6784. $('.settings-page').removeClass('active');
  6785. $('#sidebar-translation-settings').addClass('active');
  6786. $('#translation-settings').addClass('active');
  6787.  
  6788. $('#chatgpt_config').addClass('missing');
  6789. return;
  6790. } else {
  6791. $('#chatgpt_config').removeClass('missing');
  6792. }
  6793. }
  6794. {
  6795. let selectedIndex = $('input[name="translation"]:checked').length > 0;
  6796. if (!selectedIndex) {
  6797. $('.OJBetter_setting_sidebar a').removeClass('active');
  6798. $('.settings-page').removeClass('active');
  6799. $('#sidebar-translation-settings').addClass('active');
  6800. $('#translation-settings').addClass('active');
  6801.  
  6802. $('#translationServices').addClass('missing');
  6803. return;
  6804. } else {
  6805. $('#translationServices').removeClass('missing');
  6806. }
  6807. }
  6808.  
  6809. // 保存数据
  6810. let refreshPage = false; // 是否需要刷新页面
  6811. for (const [key, value] of Object.entries(settings)) {
  6812. if (!refreshPage && !(key == 'translation' || key == 'darkMode' || key == 'commentTranslationChoice')) {
  6813. if (GM_getValue(key) != value) refreshPage = true;
  6814. }
  6815. GM_setValue(key, value);
  6816. }
  6817. for (const [key, value] of Object.entries(tempConfigs)) {
  6818. if (!refreshPage && (JSON.stringify(GM_getValue(key)) != JSON.stringify(value))) refreshPage = true;
  6819. GM_setValue(key, value);
  6820. }
  6821.  
  6822. if (refreshPage) location.reload();
  6823. else {
  6824. // 切换黑暗模式
  6825. if (OJBetter.basic.darkMode != settings.darkMode) {
  6826. OJBetter.basic.darkMode = settings.darkMode;
  6827. // 移除旧的事件监听器
  6828. changeEventListeners.forEach(listener => {
  6829. mediaQueryList.removeEventListener('change', listener);
  6830. });
  6831.  
  6832. if (OJBetter.basic.darkMode == "follow") {
  6833. changeEventListeners.push(handleColorSchemeChange);
  6834. mediaQueryList.addEventListener('change', handleColorSchemeChange);
  6835. $('html').removeAttr('data-theme');
  6836. } else if (OJBetter.basic.darkMode == "dark") {
  6837. $('html').attr('data-theme', 'dark');
  6838. if (OJBetter.monaco.editor) {
  6839. monaco.editor.setTheme('vs-dark');
  6840. }
  6841. } else {
  6842. $('html').attr('data-theme', 'light');
  6843. if (OJBetter.monaco.editor) {
  6844. monaco.editor.setTheme('vs');
  6845. }
  6846. // 移除旧的事件监听器
  6847. const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  6848. window.matchMedia('(prefers-color-scheme: dark)');
  6849. }
  6850. }
  6851. // 更新配置信息
  6852. OJBetter.translation.choice = settings.translation;
  6853. OJBetter.translation.comment.choice = settings.commentTranslationChoice;
  6854. }
  6855. }
  6856. }
  6857. OJB_closeAndRemoveModal(settingMenu);
  6858. $settingBtns.prop("disabled", false).removeClass("open");
  6859. });
  6860. });
  6861. };
  6862.  
  6863. /**
  6864. * 初始化html2markdown转换器
  6865. */
  6866. async function initHTML2MarkDown() {
  6867. OJBetter.common.turndownService = new TurndownService({ bulletListMarker: '-' });
  6868.  
  6869. // 保留原始
  6870. OJBetter.common.turndownService.keep(['del']);
  6871.  
  6872. // 丢弃
  6873. OJBetter.common.turndownService.addRule('remove-by-class', {
  6874. filter: function (node) {
  6875. return node.classList.contains('sample-tests') ||
  6876. node.classList.contains('header') ||
  6877. node.classList.contains('overlay') ||
  6878. node.classList.contains('html2md-panel') ||
  6879. node.classList.contains('likeForm') ||
  6880. node.classList.contains('monaco-editor');
  6881. },
  6882. replacement: function (content, node) {
  6883. return "";
  6884. }
  6885. });
  6886. OJBetter.common.turndownService.addRule('remove-script', {
  6887. filter: function (node, options) {
  6888. return node.tagName.toLowerCase() == "script" && node.type.startsWith("math/tex");
  6889. },
  6890. replacement: function (content, node) {
  6891. return "";
  6892. }
  6893. });
  6894.  
  6895. // inline math
  6896. OJBetter.common.turndownService.addRule('inline-math', {
  6897. filter: function (node, options) {
  6898. return node.tagName.toLowerCase() == "span" && node.className == "MathJax";
  6899. },
  6900. replacement: function (content, node) {
  6901. var latex = $(node).next().text();
  6902. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  6903. return "$" + latex + "$";
  6904. }
  6905. });
  6906.  
  6907. // block math
  6908. OJBetter.common.turndownService.addRule('block-math', {
  6909. filter: function (node, options) {
  6910. return node.tagName.toLowerCase() == "div" && node.className == "MathJax_Display";
  6911. },
  6912. replacement: function (content, node) {
  6913. var latex = $(node).next().text();
  6914. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  6915. return "\n$$\n" + latex + "\n$$\n";
  6916. }
  6917. });
  6918.  
  6919. // texFontStyle
  6920. OJBetter.common.turndownService.addRule('texFontStyle', {
  6921. filter: function (node) {
  6922. return (
  6923. node.nodeName === 'SPAN' &&
  6924. node.classList.contains('tex-font-style-bf')
  6925. )
  6926. },
  6927. replacement: function (content) {
  6928. return '**' + content + '**'
  6929. }
  6930. })
  6931.  
  6932. // sectionTitle
  6933. OJBetter.common.turndownService.addRule('sectionTitle', {
  6934. filter: function (node) {
  6935. return (
  6936. node.nodeName === 'DIV' &&
  6937. node.classList.contains('section-title')
  6938. )
  6939. },
  6940. replacement: function (content) {
  6941. return '**' + content + '**'
  6942. }
  6943. })
  6944.  
  6945. // property-title
  6946. OJBetter.common.turndownService.addRule('property-title', {
  6947. filter: function (node) {
  6948. return (
  6949. node.nodeName === 'DIV' &&
  6950. node.classList.contains('property-title')
  6951. )
  6952. },
  6953. replacement: function (content) {
  6954. return content + ': '
  6955. }
  6956. })
  6957.  
  6958. // pre
  6959. OJBetter.common.turndownService.addRule('pre', {
  6960. filter: function (node, options) {
  6961. return node.tagName.toLowerCase() == "pre";
  6962. },
  6963. replacement: function (content, node) {
  6964. if (!!node.querySelector('code.prettyprint')) {
  6965. return "";
  6966. } else {
  6967. return "```\n" + content + "```\n";
  6968. }
  6969. }
  6970. });
  6971.  
  6972. // bordertable
  6973. OJBetter.common.turndownService.addRule('bordertable', {
  6974. filter: 'table',
  6975. replacement: function (content, node) {
  6976. if (node.classList.contains('bordertable')) {
  6977. var output = [],
  6978. thead = '',
  6979. trs = node.querySelectorAll('tr');
  6980. if (trs.length > 0) {
  6981. var ths = trs[0].querySelectorAll('td,th');
  6982. if (ths.length > 0) {
  6983. thead = '| ' + Array.from(ths).map(th => OJBetter.common.turndownService.turndown(th.innerHTML.trim())).join(' | ') + ' |\n'
  6984. + '| ' + Array.from(ths).map(() => ' --- ').join('|') + ' |\n';
  6985. }
  6986. }
  6987. var rows = node.querySelectorAll('tr');
  6988. Array.from(rows).forEach(function (row, i) {
  6989. if (i > 0) {
  6990. var cells = row.querySelectorAll('td,th');
  6991. var trow = '| ' + Array.from(cells).map(cell => OJBetter.common.turndownService.turndown(cell.innerHTML.trim())).join(' | ') + ' |';
  6992. output.push(trow);
  6993. }
  6994. });
  6995. return thead + output.join('\n');
  6996. } else {
  6997. return content;
  6998. }
  6999. }
  7000. });
  7001. };
  7002.  
  7003. /**
  7004. * 任务队列
  7005. */
  7006. class TaskQueue {
  7007. constructor() {
  7008. this.taskQueues = {};
  7009. this.isProcessing = {}; // 处理状态
  7010. this.delays = {}; // 等待时间(毫秒)
  7011. }
  7012.  
  7013. getDelay(type) {
  7014. if (type === 'openai') {
  7015. return 0;
  7016. } else {
  7017. return OJBetter.translation.waitTime;
  7018. }
  7019. }
  7020.  
  7021. /**
  7022. * 添加任务
  7023. * @param {string} type 任务类型
  7024. * @param {function} fn 任务函数
  7025. * @param {boolean} isNonQueueTask 是否为非队列任务
  7026. */
  7027. addTask(type, fn, isNonQueueTask = false) {
  7028. if (!this.taskQueues[type]) {
  7029. this.taskQueues[type] = [];
  7030. }
  7031.  
  7032. if (isNonQueueTask) {
  7033. fn();
  7034. } else {
  7035. this.taskQueues[type].push(fn);
  7036.  
  7037. if (!this.isProcessing[type]) {
  7038. this.processQueue(type);
  7039. }
  7040. }
  7041. }
  7042.  
  7043. async processQueue(type) {
  7044. this.isProcessing[type] = true;
  7045.  
  7046. while (this.taskQueues[type].length > 0) {
  7047. const task = this.taskQueues[type].shift();
  7048. await task();
  7049.  
  7050. if (this.taskQueues[type].length > 0) {
  7051. await this.wait(this.getDelay(type));
  7052. }
  7053. }
  7054.  
  7055. this.isProcessing[type] = false;
  7056. }
  7057.  
  7058. wait(delay) {
  7059. return new Promise(resolve => {
  7060. setTimeout(resolve, delay);
  7061. });
  7062. }
  7063. }
  7064.  
  7065. /**
  7066. * 检测为空文本
  7067. * @param {string} text 待检测的文本
  7068. * @returns {boolean} 是否为空文本
  7069. */
  7070. const isEmptyText = text => text.trim() === '';
  7071.  
  7072. /**
  7073. * 加载按钮相关函数
  7074. */
  7075. async function initButtonFunc() {
  7076. // 鼠标悬浮时为目标元素区域添加一个覆盖层
  7077. $.fn.addHoverOverlay = function (target) {
  7078. let position = $(target).css('position');
  7079. let display = $(target).css('display');
  7080.  
  7081. this.hover(() => {
  7082. $(target)
  7083. .addClass('overlay')
  7084. .css('position', 'relative');
  7085. if (display == "inline" || display == "contents") {
  7086. $(target).css('display', 'block');
  7087. }
  7088. }, () => {
  7089. $(target)
  7090. .removeClass('overlay')
  7091. .css('position', position);
  7092. if (display == "inline" || display == "contents") {
  7093. $(target).css('display', display);
  7094. }
  7095. })
  7096. }
  7097.  
  7098. /**
  7099. * 为按钮设置图标
  7100. * @param {string} icon 图标
  7101. * @returns {JQuery<HTMLElement>} 按钮
  7102. */
  7103. $.fn.setButtonIcon = function (icon) {
  7104. let i = this.find("i");
  7105. if (i.length != 0 && i.hasClass("iconfont")) {
  7106. i.html(icon);
  7107. } else {
  7108. i = OJB_safeCreateJQElement(`<i>${icon}</i>`);
  7109. this.prepend(i);
  7110. }
  7111. return this;
  7112. }
  7113.  
  7114. /**
  7115. * 设置按钮为加载等待状态
  7116. */
  7117. $.fn.setButtonLoading = function () {
  7118. this.addClass("loading");
  7119. this.prop("disabled", true);
  7120. return this;
  7121. }
  7122.  
  7123. /**
  7124. * 解除按钮的加载等待状态
  7125. */
  7126. $.fn.setButtonLoaded = function () {
  7127. this.removeClass("loading");
  7128. this.prop("disabled", false);
  7129. return this;
  7130. }
  7131.  
  7132. /**
  7133. * 为按钮设置popover提示文本
  7134. * @param {string} text 文本
  7135. * @returns {JQuery<HTMLElement>} 按钮
  7136. */
  7137. $.fn.setButtonPopover = function (text) {
  7138. // find if has popover_content class element
  7139. let popover_content = this.find(".popover_content");
  7140. if (popover_content.length != 0) {
  7141. popover_content.text(text);
  7142. } else {
  7143. popover_content = OJB_safeCreateJQElement(`<span class="popover_content">${text}</span>`);
  7144. this.append(popover_content);
  7145. }
  7146. return this;
  7147. }
  7148.  
  7149. /**
  7150. * 获取MarkDown
  7151. * @returns {string} MarkDown
  7152. */
  7153. $.fn.getMarkdown = function () {
  7154. const markdown = this.data('markdown');
  7155. if (markdown === undefined) {
  7156. const htmlContent = this.html();
  7157. const newMarkdown = OJBetter.common.turndownService.turndown(htmlContent);
  7158. this.data('markdown', newMarkdown);
  7159. return newMarkdown;
  7160. }
  7161. return markdown;
  7162. }
  7163.  
  7164. // 设置按钮状态
  7165. $.fn.setButtonState = function (state, popoverText = null, disabled = false) {
  7166. this.data('buttonState', state)
  7167. .prop('disabled', disabled)
  7168. .css('cursor', disabled ? 'not-allowed' : 'pointer')
  7169. .removeClass('running success enabled error loading redo');
  7170. if (popoverText) this.setButtonPopover(popoverText);
  7171.  
  7172. if (state !== 'initial') this.addClass(state);
  7173. return this;
  7174. };
  7175.  
  7176. // 为按钮添加鼠标悬浮重试
  7177. $.fn.setHoverRedo = function () {
  7178. this.hover(() => {
  7179. prevState = this.getButtonState();
  7180. if (prevState !== "normal" && prevState !== "running") {
  7181. this.setButtonState('redo');
  7182. }
  7183. }, () => {
  7184. const currentState = this.getButtonState();
  7185. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7186. this.setButtonState(prevState);
  7187. prevState = null;
  7188. }
  7189. });
  7190. };
  7191.  
  7192. // 获取按钮状态
  7193. $.fn.getButtonState = function () {
  7194. return this.data('buttonState') || 'normal';
  7195. };
  7196.  
  7197. // 设置翻译按钮状态
  7198. $.fn.setTransButtonState = function (state, text = null) {
  7199. const popoverText = text || i18next.t(`trans.${state}`, { ns: 'button' });
  7200. const disabled = state === 'running' || state === 'loading';
  7201. this.setButtonState(state, popoverText, disabled);
  7202. return this;
  7203. };
  7204.  
  7205. // 存翻译结果
  7206. $.fn.pushResultToTransButton = function (result) {
  7207. let resultStack = this.data('resultStack');
  7208. if (!resultStack) resultStack = [];
  7209. resultStack.push(result);
  7210. this.data('resultStack', resultStack);
  7211. }
  7212.  
  7213. // 获取翻译结果
  7214. $.fn.getResultFromTransButton = function () {
  7215. return this.data('resultStack');
  7216. }
  7217.  
  7218. // 标记为不自动翻译
  7219. $.fn.setNotAutoTranslate = function () {
  7220. this.data('notAutoTranslate', true);
  7221. }
  7222.  
  7223. // 获取是否为不自动翻译
  7224. $.fn.getNotAutoTranslate = function () {
  7225. return this.data('notAutoTranslate');
  7226. }
  7227.  
  7228. // 判断是否已经翻译
  7229. $.fn.IsTranslated = function () {
  7230. if (this.hasAttr('translated')) {
  7231. return true;
  7232. } else {
  7233. return false;
  7234. }
  7235. }
  7236.  
  7237. // 判断是否为评论区按钮
  7238. $.fn.IsCommentButton = function () {
  7239. let isCommentButton = this.data('isCommentButton');
  7240. if (isCommentButton == undefined) {
  7241. this.parents('.comments').length > 0 ? isCommentButton = true : isCommentButton = false;
  7242. this.data('isCommentButton', isCommentButton);
  7243. }
  7244. return isCommentButton;
  7245. }
  7246.  
  7247. // 按钮点击效果
  7248. $(document).on('mousedown', '.ojb_btn', function () {
  7249. $(this).addClass('active').on('animationend', () => $(this).removeClass('active'));
  7250. });
  7251. }
  7252.  
  7253. /**
  7254. * 添加题目markdown转换/复制/翻译按钮面板
  7255. * @param {HTMLElement} element 需要添加按钮面板的元素
  7256. * @param {string} suffix 按钮面板id后缀
  7257. * @param {string} type 按钮面板添加位置
  7258. * @param {boolean} is_simple 是否是简单模式
  7259. * @returns {object} 返回按钮面板元素
  7260. */
  7261. function addButtonPanel(element, suffix, type, is_simple = false) {
  7262. let text;
  7263. if (OJBetter.translation.comment.transMode == "1") text = i18next.t('trans.segment', { ns: 'button' });
  7264. else if (OJBetter.translation.comment.transMode == "2") text = i18next.t('trans.select', { ns: 'button' });
  7265. else text = i18next.t('trans.normal', { ns: 'button' });
  7266.  
  7267. let panel = OJB_safeCreateJQElement(`<div class='html2md-panel input-output-copier ${is_simple ? 'is_simple' : ''}'></div>`);
  7268. let viewButton = OJB_safeCreateJQElement(`
  7269. <button class='ojb_btn ojb_btn_popover top' id='html2md-view${suffix}'>
  7270. <i class="iconfont">&#xe7e5;</i>
  7271. <span class="popover_content">${i18next.t('md.normal', { ns: 'button' })}</span>
  7272. </button>`);
  7273. let copyButton = OJB_safeCreateJQElement(`
  7274. <button class='ojb_btn ojb_btn_popover top' id='html2md-cb${suffix}'>
  7275. <i class="iconfont">&#xe608;</i>
  7276. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  7277. </button>`);
  7278. let translateButton = OJB_safeCreateJQElement(`
  7279. <button class='ojb_btn translateButton ojb_btn_popover top' id='translateButton${suffix}'>
  7280. <i class="iconfont">&#xe6be;</i>
  7281. <span class="popover_content">${text}</span>
  7282. </button>`);
  7283. if (!is_simple) panel.append(viewButton);
  7284. if (!is_simple) panel.append(copyButton);
  7285. panel.append(translateButton);
  7286. if (type === "this_level") {
  7287. $(element).before(panel);
  7288. } else if (type === "child_level") {
  7289. $(element).prepend(panel);
  7290. }
  7291.  
  7292. return {
  7293. panel: panel,
  7294. viewButton: viewButton,
  7295. copyButton: copyButton,
  7296. translateButton: translateButton
  7297. }
  7298. }
  7299.  
  7300. /**
  7301. * 添加MD视图按钮
  7302. * @param {JQuery<HTMLElement>} button 按钮
  7303. * @param {JQuery<HTMLElement>} element 目标元素
  7304. * @param {string} suffix id后缀
  7305. * @param {string} type 类型
  7306. * @returns {void}
  7307. */
  7308. async function addButtonWithHTML2MD(button, element, suffix, type) {
  7309. /**
  7310. * 改变按钮状态
  7311. * @param {string} state 状态
  7312. */
  7313. function changeButtonState(state) {
  7314. if (state == "loading") {
  7315. button.setButtonLoading();
  7316. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7317. } else if (state == "loaded") {
  7318. button.setButtonLoaded();
  7319. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7320. } else if (state == "normal") {
  7321. button.removeClass("enabled");
  7322. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7323. } else if (state == "mdView") {
  7324. button.addClass("enabled");
  7325. button.setButtonPopover(i18next.t('md.reduction', { ns: 'button' }));
  7326. } else if (state == "disabled") {
  7327. button.prop("disabled", true);
  7328. button.setButtonPopover(i18next.t('md.disabled', { ns: 'button' }));
  7329. }
  7330. }
  7331.  
  7332. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7333. changeButtonState("disabled");
  7334. return;
  7335. } else {
  7336. changeButtonState("loading");
  7337. await waitForMathJaxIdle();
  7338. changeButtonState("loaded");
  7339. }
  7340.  
  7341. button.click(OJB_debounce(function () {
  7342. var target = $(element).get(0);
  7343.  
  7344. /**
  7345. * 检查是否是MarkDown视图
  7346. * @returns {boolean} 是否是MarkDown视图
  7347. */
  7348. function checkViewmd() {
  7349. if ($(element).attr("viewmd") === "true") {
  7350. return true;
  7351. } else {
  7352. return false;
  7353. }
  7354. }
  7355.  
  7356. /**
  7357. * 设置是否是MarkDown视图
  7358. * @param {boolean} value 是否是MarkDown视图
  7359. * @returns {void}
  7360. */
  7361. function setViewmd(value) {
  7362. $(element).attr("viewmd", value);
  7363. if (value) {
  7364. changeButtonState("mdView");
  7365. } else {
  7366. changeButtonState("normal");
  7367. }
  7368. }
  7369.  
  7370. if (checkViewmd()) {
  7371. setViewmd(false);
  7372. $(element).next(".mdViewContent").remove();
  7373. $(element).show();
  7374. } else {
  7375. setViewmd(true);
  7376. var markdown = $(element).getMarkdown();
  7377. var mdViewContent = OJB_safeCreateJQElement(`<span class="mdViewContent" style="width:auto; height:auto;">${markdown}</span>`);
  7378. $(element).after(mdViewContent);
  7379. $(element).hide();
  7380. }
  7381. }));
  7382.  
  7383. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7384. button.addHoverOverlay($(element));
  7385. }
  7386. }
  7387.  
  7388. /**
  7389. * 添加复制按钮
  7390. * @param {JQuery<HTMLElement>} button 按钮
  7391. * @param {JQuery<HTMLElement>} element 目标元素
  7392. * @param {string} suffix 后缀
  7393. * @param {string} type 类型
  7394. */
  7395. async function addButtonWithCopy(button, element, suffix, type) {
  7396. /**
  7397. * 改变按钮状态
  7398. * @param {string} state 状态
  7399. */
  7400. function changeButtonState(state) {
  7401. if (state == "loading") {
  7402. button.setButtonLoading();
  7403. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7404. } else if (state == "loaded") {
  7405. button.setButtonLoaded();
  7406. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7407. } else if (state == "normal") {
  7408. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7409. } else if (state == "copied") {
  7410. button.setButtonPopover(i18next.t('copy.copied', { ns: 'button' }));
  7411. } else if (state == "disabled") {
  7412. button.prop("disabled", true);
  7413. button.setButtonPopover(i18next.t('copy.disabled', { ns: 'button' }));
  7414. }
  7415. }
  7416.  
  7417. // 等待MathJax队列完成
  7418. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7419. changeButtonState("disabled");
  7420. return;
  7421. } else {
  7422. changeButtonState("loading");
  7423. await waitForMathJaxIdle();
  7424. changeButtonState("loaded");
  7425. }
  7426.  
  7427. button.click(OJB_debounce(function () {
  7428. var target = $(element).get(0);
  7429.  
  7430. var markdown = $(element).getMarkdown();
  7431.  
  7432. GM_setClipboard(markdown);
  7433.  
  7434. $(this).addClass("success");
  7435. changeButtonState("copied");
  7436.  
  7437.  
  7438. // 更新复制按钮文本
  7439. setTimeout(() => {
  7440. $(this).removeClass("success");
  7441. changeButtonState("normal")
  7442. }, 2000);
  7443. }));
  7444.  
  7445. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7446. button.addHoverOverlay($(element));
  7447. }
  7448. }
  7449.  
  7450. /**
  7451. * 添加翻译按钮
  7452. * @param {JQuery<HTMLElement>} button 按钮
  7453. * @param {JQuery<HTMLElement>} element 目标元素
  7454. * @param {string} suffix 后缀
  7455. * @param {string} type 类型
  7456. * @param {boolean} is_comment 是否是评论
  7457. */
  7458. async function addButtonWithTranslation(button, element, suffix, type, is_comment = false) {
  7459. /**
  7460. * 添加可指定翻译服务的方法调用
  7461. * @param {string} translation 翻译服务
  7462. */
  7463. button.data("translatedItBy", function (translation) {
  7464. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7465. OJBetter.common.taskQueue.addTask(translation, () => transTask(button, element, type, is_comment, translation), translation == 'openai');
  7466. });
  7467.  
  7468. // 等待MathJax队列完成
  7469. button.setButtonLoading();
  7470. await waitForMathJaxIdle();
  7471. button.setButtonLoaded();
  7472.  
  7473. // 标记目标文本区域不自动翻译
  7474. {
  7475. let text;
  7476. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7477. text = $(element).html();
  7478. } else {
  7479. text = $(element).getMarkdown();
  7480. }
  7481. let length = text.length;
  7482. if (length > OJBetter.translation.auto.shortTextLength || isEmptyText(text) || $(element).find('.spoiler').length > 0) {
  7483. button.setNotAutoTranslate();
  7484. }
  7485. // button.after(`<span>${length}</span>`); // 显示字符数
  7486. }
  7487.  
  7488. button.click(OJB_debounce(async function () {
  7489. // 重新翻译
  7490. let resultStack = $(this).getResultFromTransButton();
  7491. if (resultStack) {
  7492. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected");
  7493. for (let item of resultStack) {
  7494. if (OJBetter.translation.retransAction == "0") {
  7495. // 选段翻译不直接移除旧结果
  7496. if (OJBetter.translation.comment.transMode == "2") {
  7497. // 只移除即将要翻译的段的结果
  7498. if (pElements.is(item.translateDiv.getDiv().prev())) {
  7499. item.translateDiv.close();
  7500. }
  7501. } else {
  7502. item.translateDiv.close();
  7503. $($(element)).find(".translate-problem-statement, .translate-problem-statement-panel").remove();
  7504. }
  7505. } else {
  7506. item.translateDiv.foldMainDiv();
  7507. }
  7508. }
  7509. }
  7510.  
  7511. // 翻译
  7512. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7513. OJBetter.common.taskQueue.addTask(OJBetter.translation.choice, () => transTask(button, element, type, is_comment), OJBetter.translation.choice == 'openai');
  7514. }));
  7515.  
  7516. // 重新翻译提示
  7517. let prevState;
  7518. button.hover(() => {
  7519. prevState = button.getButtonState();
  7520. if (prevState !== "normal" && prevState !== "running") {
  7521. button.setTransButtonState('redo');
  7522. }
  7523. }, () => {
  7524. const currentState = button.getButtonState();
  7525. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7526. button.setTransButtonState(prevState);
  7527. prevState = null;
  7528. }
  7529. });
  7530.  
  7531. // 目标区域指示
  7532. if (OJBetter.preference.hoverTargetAreaDisplay) {
  7533. button.addHoverOverlay($(element));
  7534. }
  7535.  
  7536. // 翻译右键切换菜单
  7537. $(document).on('contextmenu', '#translateButton' + suffix, function (e) {
  7538. e.preventDefault();
  7539.  
  7540. // 是否为评论的翻译
  7541. let is_comment = button.IsCommentButton();
  7542.  
  7543. // 移除旧的
  7544. if (!$(e.target).closest('.OJBetter_contextmenu').length) {
  7545. $('.OJBetter_contextmenu').remove();
  7546. }
  7547.  
  7548. var menu = $('<div class="OJBetter_contextmenu"></div>');
  7549. var translations = [
  7550. { value: 'deepl', name: i18next.t('translation.options.services.deepl', { ns: 'settings' }) },
  7551. { value: 'iflyrec', name: i18next.t('translation.options.services.iflyrec', { ns: 'settings' }) },
  7552. { value: 'youdao', name: i18next.t('translation.options.services.youdao', { ns: 'settings' }) },
  7553. { value: 'google', name: i18next.t('translation.options.services.google', { ns: 'settings' }) },
  7554. { value: 'caiyun', name: i18next.t('translation.options.services.caiyun', { ns: 'settings' }) },
  7555. { value: 'openai', name: i18next.t('translation.options.services.openai.name', { ns: 'settings' }) }
  7556. ];
  7557.  
  7558. // Function to check if the service supports the target language
  7559. function supportsTargetLanguage(service, targetLang) {
  7560. return OJBetter.supportList.translationSupport[service] && OJBetter.supportList.translationSupport[service][targetLang] !== undefined;
  7561. }
  7562.  
  7563. if (is_comment) {
  7564. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="0">
  7565. <span class="OJBetter_contextmenu_label_text">
  7566. ${i18next.t('translation.preference.comment_translation_choice.services.follow', { ns: 'settings' })}
  7567. </span></label>`);
  7568. menu.append(label);
  7569. }
  7570. translations.forEach(function (translation) {
  7571. if (supportsTargetLanguage(translation.value, OJBetter.translation.targetLang)) {
  7572. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="${translation.value}">
  7573. <span class="OJBetter_contextmenu_label_text">${translation.name}</span></label>`);
  7574. menu.append(label);
  7575. }
  7576. });
  7577.  
  7578. // 初始化
  7579. if (is_comment) {
  7580. menu.find(`input[name="translation"][value="${OJBetter.translation.comment.choice}"]`).prop('checked', true);
  7581. } else {
  7582. menu.find(`input[name="translation"][value="${OJBetter.translation.choice}"]`).prop('checked', true);
  7583. }
  7584. menu.css({
  7585. top: e.pageY + 'px',
  7586. left: e.pageX + 'px'
  7587. }).appendTo('body');
  7588.  
  7589. $(document).one('change', 'input[name="translation"]', function () {
  7590. if (is_comment) {
  7591. OJBetter.translation.comment.choice = $('input[name="translation"]:checked').val();
  7592. GM_setValue("commentTranslationChoice", OJBetter.translation.comment.choice);
  7593. } else {
  7594. OJBetter.translation.choice = $('input[name="translation"]:checked').val();
  7595. GM_setValue("translation", OJBetter.translation.choice);
  7596. }
  7597. $('.OJBetter_contextmenu').remove();
  7598. });
  7599.  
  7600. // 点击区域外关闭菜单
  7601. function handleClick(event) {
  7602. if (!$(event.target).closest('.OJBetter_contextmenu').length) {
  7603. $('.OJBetter_contextmenu').remove();
  7604. $(document).off('change', 'input[name="translation"]');
  7605. } else {
  7606. $(document).one('click', handleClick);
  7607. }
  7608. }
  7609. $(document).one('click', handleClick);
  7610. });
  7611. }
  7612.  
  7613. /**
  7614. * 创建翻译任务
  7615. * @param {JQuery<HTMLElement>} button 按钮
  7616. * @param {HTMLElement} element 目标元素
  7617. * @param {string} type 类型
  7618. * @param {boolean} is_comment 是否是评论
  7619. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7620. */
  7621. async function transTask(button, element, type, is_comment, overrideTrans) {
  7622. /** @type {HTMLElement} 目标元素 */
  7623. let target;
  7624. /**
  7625. * 错误计数数据结构
  7626. * @typedef {Object} count
  7627. * @property {number} errerNum 错误数量
  7628. * @property {number} skipNum 跳过数量
  7629. */
  7630. const count = {
  7631. errerNum: 0,
  7632. skipNum: 0
  7633. };
  7634. if (OJBetter.translation.comment.transMode == "1") {
  7635. // 分段翻译
  7636. let pElements = $(element).find("p:not(li p), li, .OJBetter_acmsguru");
  7637. for (let i = 0; i < pElements.length; i++) {
  7638. target = $(pElements[i]).eq(0).clone();
  7639. element_node = pElements[i];
  7640. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7641. }
  7642. } else if (OJBetter.translation.comment.transMode == "2") {
  7643. // 选段翻译
  7644. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected, .OJBetter_acmsguru");
  7645. for (let i = 0; i < pElements.length; i++) {
  7646. target = $(pElements[i]).eq(0).clone();
  7647. element_node = pElements[i];
  7648. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7649. }
  7650. $(element).find("p.block_selected:not(li p), li.block_selected").removeClass('block_selected');
  7651. } else {
  7652. // 普通翻译
  7653. target = $(element).eq(0).clone();
  7654. if (type === "child_level") $(target).children(':first').remove();
  7655. element_node = $($(element)).get(0);
  7656. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7657. }
  7658.  
  7659. // 翻译完成
  7660. if (!count.errerNum && !count.skipNum) {
  7661. button.setTransButtonState('success');
  7662. }
  7663. }
  7664.  
  7665. /**
  7666. * 翻译处理
  7667. * @param {JQuery<HTMLElement>} button 按钮
  7668. * @param {HTMLElement} target 目标元素
  7669. * @param {HTMLElement} element_node 目标节点
  7670. * @param {string} type 类型
  7671. * @param {boolean} is_comment 是否是评论
  7672. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7673. */
  7674. async function process(button, target, element_node, type, is_comment, count, overrideTrans) {
  7675. if (type === "child_level") {
  7676. let div = $("<div>");
  7677. $(element_node).append(div);
  7678. element_node = div.get(0);
  7679. }
  7680.  
  7681. //是否跳过折叠块
  7682. if ($(target).find('.spoiler').length > 0) {
  7683. const shouldSkip = await OJB_createDialog(
  7684. i18next.t('skipFold.title', { ns: 'dialog' }),
  7685. i18next.t('skipFold.content', { ns: 'dialog' }),
  7686. [
  7687. i18next.t('skipFold.buttons.0', { ns: 'dialog' }),
  7688. i18next.t('skipFold.buttons.1', { ns: 'dialog' })
  7689. ],
  7690. true
  7691. ); //跳过折叠块确认
  7692. if (shouldSkip) {
  7693. $(target).find('.spoiler').remove();
  7694. } else {
  7695. $(target).find('.html2md-panel').remove();
  7696. }
  7697. }
  7698.  
  7699. // 等待并获取结果
  7700. button.setTransButtonState('running');
  7701. const result = await blockProcessing(button, target, element_node, is_comment, overrideTrans);
  7702. button.pushResultToTransButton(result);
  7703.  
  7704. if (result.status == "error") count.errerNum += 1;
  7705. else if (result.status == "skip") count.skipNum += 1;
  7706. $(target).remove();
  7707. }
  7708.  
  7709. /**
  7710. * 块处理
  7711. * @param {JQuery<HTMLElement>} button
  7712. * @param {HTMLElement} target 目标元素
  7713. * @param {HTMLElement} element_node 目标节点
  7714. * @param {boolean} is_comment 是否是评论
  7715. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7716. * @returns {TranslateResult} 翻译结果对象
  7717. */
  7718. async function blockProcessing(button, target, element_node, is_comment, overrideTrans) {
  7719. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7720. target.markdown = $(target).html();
  7721. } else if (!target.markdown) {
  7722. target.markdown = OJBetter.common.turndownService.turndown($(target).html());
  7723. }
  7724.  
  7725. const result = await translateProblemStatement(target.markdown, element_node, is_comment, overrideTrans);
  7726. if (result.status == "skip") {
  7727. button.setTransButtonState('error', i18next.t('trans.tooLong', { ns: 'button' }));
  7728. result.translateDiv.close();
  7729. } else if (result.status == "error" || !result.rawData.done) {
  7730. result.translateDiv.setError();
  7731. result.translateDiv.setRawData(result.rawData);
  7732. result.translateDiv.showDebugButton();
  7733. button.setTransButtonState('error', i18next.t('trans.error', { ns: 'button' }));
  7734. $(target).remove();
  7735. }
  7736. return result;
  7737. }
  7738.  
  7739. /**
  7740. * 选段翻译支持
  7741. */
  7742. async function multiChoiceTranslation() {
  7743. GM_addStyle(`
  7744. .topic .content .ttypography {
  7745. overflow: initial;
  7746. }
  7747. `);
  7748.  
  7749. $(document).on('click', 'p, li:not(:has(.comment)), .OJBetter_acmsguru', function (e) {
  7750. let $this = $(this);
  7751. e.stopPropagation();
  7752. if ($this.hasClass('block_selected')) {
  7753. $this.removeClass('block_selected');
  7754. // 移除对应的按钮
  7755. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + $this.attr('OJBetter_p_id'));
  7756. } else {
  7757. let id = OJB_getRandomNumber(8);
  7758. $this.attr('OJBetter_p_id', id);
  7759. $this.addClass('block_selected');
  7760. // 添加按钮
  7761. let menu = OJB_safeCreateJQElement(`<div class="OJBetter_MiniTranslateButton" id='translateButton_selected_${id}'>${translateIcon}</div>`)
  7762. .css({
  7763. left: $($this).outerWidth(true) + $($this).position().left + 10 + 'px',
  7764. });
  7765. $this.before(menu);
  7766.  
  7767. $("#translateButton_selected_" + id).click(async function () {
  7768. // 处理旧的结果
  7769. if ($this.attr('translated')) {
  7770. let result = $this.data("resultData");
  7771. if (OJBetter.translation.retransAction == "0") {
  7772. result.translateDiv.close();
  7773. } else {
  7774. result.translateDiv.foldMainDiv();
  7775. }
  7776. }
  7777. // 翻译
  7778. let target = $this.eq(0).clone();
  7779. let result = await blockProcessing(OJBetter.translation.choice, target, $this.eq(0), $("#translateButton_selected_" + id), false);
  7780. $this.data("resultData", result);
  7781. $this.removeClass('block_selected');
  7782. // 移除对应的按钮
  7783. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + id);
  7784. $this.attr('translated', '1'); // 标记已翻译
  7785. });
  7786. }
  7787. });
  7788. }
  7789.  
  7790. /**
  7791. * 为acmsguru题面重新划分div
  7792. */
  7793. async function acmsguruReblock() {
  7794. if (OJBetter.translation.comment.transMode == '0') {
  7795. // 普通模式下的划分方式
  7796. var html = $('.ttypography').children().html();
  7797. var separator = /(<div align="left" style="margin-top: 1\.0em;"><b>.*?<\/b><\/div>)/g;
  7798. var result = html.split(separator); // 分割代码
  7799. var outputHtml = '';
  7800. var header = '';
  7801. for (var i = 0; i < result.length; i++) {
  7802. if (separator.test(result[i])) {
  7803. header = result[i];
  7804. continue;
  7805. }
  7806. outputHtml += '<div class="ttypography">' + header + result[i] + '</div>';
  7807. header = '';
  7808. }
  7809. $('.ttypography').html(outputHtml);
  7810. }
  7811. else {
  7812. // 分段/选段模式下的划分方式
  7813. $('.ttypography').children().each(function () {
  7814. var html = $(this).html();
  7815. var replacedHtml = html.replace(/(?:<\/div>|<br><br>)(?<text>[\s\S]+?)(?=<br><br>)/g,
  7816. '<div align="left" class="OJBetter_acmsguru" >$<text></div>');
  7817. $(this).html(replacedHtml);
  7818. });
  7819. }
  7820. }
  7821.  
  7822. /**
  7823. * 添加MD/复制/翻译按钮
  7824. */
  7825. async function addConversionButton() {
  7826. let promises = []; // 用于收集所有的 Promise
  7827.  
  7828. // 题目页添加按钮
  7829. if (OJBetter.typeOfPage.is_problem) {
  7830. let exContentsPageClasses = ["sample-tests"];
  7831. $('.problem-statement').children('div').each((i, e) => {
  7832. var className = $(e).attr('class');
  7833. if (!exContentsPageClasses.includes(className)) {
  7834. var id = "_problem_" + OJB_getRandomNumber(8);
  7835. let panel = addButtonPanel(e, id, "this_level");
  7836. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7837. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7838. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7839. if (i == 0) panel.translateButton.setNotAutoTranslate(); // 题目标题块跳过,不自动翻译
  7840. }
  7841. });
  7842. }
  7843. // 添加按钮到ttypography部分
  7844. $(".ttypography").each((i, e) => {
  7845. // 是否为评论
  7846. let is_comment = false;
  7847. if ($(e).parents('.comments').length > 0) is_comment = true;
  7848. // 题目页不添加
  7849. if (!OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_acmsguru) {
  7850. let id = "_ttypography_" + OJB_getRandomNumber(8);
  7851. let panel = addButtonPanel(e, id, "this_level");
  7852. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7853. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7854. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level", is_comment));
  7855. }
  7856. });
  7857.  
  7858. // 完整题目集页特殊处理
  7859. if (OJBetter.typeOfPage.is_completeProblemset) {
  7860. let exContentsPageClasses = ["sample-tests"];
  7861. $('.problem-statement').each(function () {
  7862. $(this).children('div').each((i, e) => {
  7863. var className = $(e).attr('class');
  7864. if (!exContentsPageClasses.includes(className)) {
  7865. var id = "_problem_" + OJB_getRandomNumber(8);
  7866. let panel = addButtonPanel(e, id, "this_level");
  7867. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7868. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7869. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7870. if (i == 0) panel.translateButton.setNotAutoTranslate(); // 题目标题块跳过,不自动翻译
  7871. }
  7872. });
  7873. });
  7874. }
  7875.  
  7876. // 添加按钮到spoiler部分
  7877. $('.spoiler-content').each((i, e) => {
  7878. if ($(e).find('.html2md-panel').length === 0) {
  7879. let id = "_spoiler_" + OJB_getRandomNumber(8);
  7880. let panel = addButtonPanel(e, id, "child_level");
  7881. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "child_level"));
  7882. promises.push(addButtonWithCopy(panel.copyButton, e, id, "child_level"));
  7883. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  7884. }
  7885. });
  7886.  
  7887. // 添加按钮到titled部分
  7888. (function () {
  7889. var elements = [".Virtual.participation", ".Attention", ".Practice"];//只为部分titled添加
  7890. $.each(elements, (i, e) => {
  7891. $(e).each(function () {
  7892. let id = "_titled_" + OJB_getRandomNumber(8);
  7893. let nextDiv = $(e).next().children().get(0);
  7894. if (!nextDiv) return;
  7895. let panel = addButtonPanel(nextDiv, id, "child_level", true);
  7896. promises.push(addButtonWithTranslation(panel.translateButton, nextDiv, id, "child_level"));
  7897. });
  7898. });
  7899. })();
  7900. if (OJBetter.typeOfPage.is_mSite) {
  7901. $("div[class='_IndexPage_notice']").each((i, e) => {
  7902. let id = "_titled_" + OJB_getRandomNumber(8);
  7903. let panel = addButtonPanel(e, id, "this_level", true);
  7904. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7905. });
  7906. }
  7907.  
  7908. // 添加按钮到比赛QA部分
  7909. $(".question-response").each((i, e) => {
  7910. let id = "_question_" + OJB_getRandomNumber(8);
  7911. let panel = addButtonPanel(e, id, "this_level", true);
  7912. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7913. });
  7914. if (OJBetter.typeOfPage.is_mSite) {
  7915. $("div._ProblemsPage_announcements table tbody tr:gt(0)").each((i, e) => {
  7916. var $nextDiv = $(e).find("td:first");
  7917. let id = "_question_" + OJB_getRandomNumber(8);
  7918. let panel = addButtonPanel($nextDiv, id, "this_level", true);
  7919. promises.push(addButtonWithTranslation(panel.translateButton, $nextDiv, id, "this_level"));
  7920. });
  7921. }
  7922.  
  7923. // 添加按钮到弹窗confirm-proto部分
  7924. $(".confirm-proto").each((i, e) => {
  7925. let id = "_titled_" + OJB_getRandomNumber(8);
  7926. var $nextDiv = $(e).children().get(0);
  7927. let panel = addButtonPanel($nextDiv, id, "this_level", true);
  7928. promises.push(addButtonWithTranslation(panel.translateButton, $nextDiv, id, "this_level"));
  7929. });
  7930.  
  7931. // 添加按钮到_CatalogHistorySidebarFrame_item部分
  7932. $("._CatalogHistorySidebarFrame_item").each((i, e) => {
  7933. let id = "_history_sidebar_" + OJB_getRandomNumber(8);
  7934. let panel = addButtonPanel(e, id, "this_level", true);
  7935. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7936. });
  7937.  
  7938. $(".problem-lock-link").on("click", function () {
  7939. $(".popup .content div").each((i, e) => {
  7940. let id = "_popup_" + OJB_getRandomNumber(8);
  7941. let panel = addButtonPanel(e, id, "this_level", true);
  7942. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7943. });
  7944. });
  7945.  
  7946. // 添加按钮到弹窗alert部分
  7947. $(".alert:not(.OJBetter_alert)").each((i, e) => {
  7948. let id = "_alert_" + OJB_getRandomNumber(8);
  7949. let panel = addButtonPanel(e, id, "child_level", true);
  7950. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  7951. });
  7952.  
  7953. // 添加按钮到talk-text部分
  7954. $(".talk-text").each((i, e) => {
  7955. let id = "_talk-text_" + OJB_getRandomNumber(8);
  7956. let panel = addButtonPanel(e, id, "child_level", true);
  7957. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  7958. });
  7959.  
  7960. return Promise.all(promises).catch(error => {
  7961. console.error("One or more of the Add Button operations failed: ", error);
  7962. });
  7963. };
  7964.  
  7965. /**
  7966. * 等待LaTeX渲染队列全部完成
  7967. * @returns {Promise} 完成渲染
  7968. */
  7969. function waitForMathJaxIdle() {
  7970. return new Promise((resolve, reject) => {
  7971. // 检查MathJax对象是否存在
  7972. const checkMathJaxExists = () => {
  7973. if (typeof MathJax === 'undefined') {
  7974. // 如果MathJax不存在,稍后再次检查
  7975. OJB_delay(100).then(checkMathJaxExists);
  7976. } else {
  7977. // MathJax存在,开始监视渲染队列
  7978. startMonitoringQueue();
  7979. }
  7980. };
  7981.  
  7982. // 开始监视MathJax渲染队列
  7983. const startMonitoringQueue = () => {
  7984. const intervalId = setInterval(() => {
  7985. const queue = MathJax.Hub.queue;
  7986. if (queue.pending === 0 && queue.running === 0) {
  7987. clearInterval(intervalId);
  7988. resolve();
  7989. }
  7990. }, 100);
  7991. };
  7992.  
  7993. // 开始检查MathJax对象
  7994. checkMathJaxExists();
  7995. });
  7996. }
  7997.  
  7998. /**
  7999. * 翻译结果面板
  8000. */
  8001. class TranslateDiv {
  8002. /**
  8003. * 构造函数
  8004. * @param {string} id 指定翻译框的id
  8005. */
  8006. constructor(id) {
  8007. this.id = id;
  8008. this.div = $('<div>').attr('id', id).addClass('translateDiv bounce-in');
  8009. if (!OJBetter.typeOfPage.is_completeProblemset) {
  8010. this.div.addClass('input-output-copier');
  8011. }
  8012. this.panelDiv = $('<div>').addClass('translate-problem-statement-panel');
  8013. this.div.append(this.panelDiv);
  8014.  
  8015. // 主要信息
  8016. this.mainDiv = $('<div>').addClass('translate-problem-statement');
  8017. this.span = $('<span>');
  8018. this.mainDiv.append(this.span);
  8019. this.div.append(this.mainDiv);
  8020. this.mainDivState = {
  8021. current: 'transHTML',
  8022. transHTML: '',
  8023. rawDataHTML: ''
  8024. };
  8025.  
  8026. // 顶栏信息
  8027. this.topText = $('<div>').addClass('topText');
  8028. this.panelDiv.append(this.topText);
  8029.  
  8030. // 右侧
  8031. this.rightDiv = $('<div>').css('display', 'flex');
  8032. this.panelDiv.append(this.rightDiv);
  8033. this.debugButton = OJB_safeCreateJQElement(`
  8034. <button class='ojb_btn ojb_btn_popover top'>
  8035. <i class="iconfont">&#xe641;</i>
  8036. <span class="popover_content">${i18next.t('rawData.normal', { ns: 'button' })}</span>
  8037. </button>`).hide();
  8038. this.rightDiv.append(this.debugButton);
  8039. this.queryBalanceButton = OJB_safeCreateJQElement(`
  8040. <button class='ojb_btn ojb_btn_popover top'>
  8041. <i class="iconfont">&#xe6ae;</i>
  8042. <span class="popover_content">${i18next.t('queryBalance.normal', { ns: 'button' })}</span>
  8043. </button>`).hide();
  8044. this.rightDiv.append(this.queryBalanceButton);
  8045. this.copyButton = OJB_safeCreateJQElement(`
  8046. <button class='ojb_btn ojb_btn_popover top'>
  8047. <i class="iconfont">&#xe608;</i>
  8048. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  8049. </button>`);
  8050. this.rightDiv.append(this.copyButton);
  8051. this.upButton = OJB_safeCreateJQElement(`
  8052. <button class='ojb_btn ojb_btn_popover top'>
  8053. <i class="iconfont">&#xe601;</i>
  8054. <span class="popover_content">${i18next.t('fold.normal', { ns: 'button' })}</span>
  8055. </button>`);
  8056. this.rightDiv.append(this.upButton);
  8057. this.closeButton = OJB_safeCreateJQElement(`
  8058. <button class='ojb_btn ojb_btn_popover top'>
  8059. <i class="iconfont">&#xe614;</i>
  8060. <span class="popover_content">${i18next.t('close.normal', { ns: 'button' })}</span>
  8061. </button>`);
  8062. this.rightDiv.append(this.closeButton);
  8063. }
  8064.  
  8065. /**
  8066. * 获取翻译框
  8067. * @returns {JQuery<HTMLElement>} 返回翻译框
  8068. */
  8069. getDiv() {
  8070. return this.div;
  8071. }
  8072.  
  8073. /**
  8074. * 设置翻译框顶部的文本
  8075. * @param {string} text 翻译框顶部的文本
  8076. */
  8077. setTopText(text) {
  8078. this.div.attr("data-topText", text);
  8079. this.topText.text(text);
  8080. }
  8081.  
  8082. /**
  8083. * 获取翻译框顶部的文本
  8084. * @returns {string} 返回翻译框顶部的文本
  8085. */
  8086. getTopText() {
  8087. return this.topText.text();
  8088. }
  8089.  
  8090. /**
  8091. * 渲染一个元素内的LaTeX公式
  8092. * @param {HTMLElement} element 元素
  8093. */
  8094. renderLaTeX(element) {
  8095. MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]);
  8096. }
  8097.  
  8098. /**
  8099. * 更新翻译框内容
  8100. * @param {string} text 文本内容
  8101. * @param {boolean} is_escapeHTML 是否转义HTML标签,为true则HTML标签将作为普通文本处理,默认为true
  8102. * @param {boolean} is_renderLaTeX 是否渲染LaTeX,为true则会渲染LaTeX,默认为true
  8103. */
  8104. updateTranslateDiv(text, is_escapeHTML = true, is_renderLaTeX = true,) {
  8105. // 渲染MarkDown
  8106. let md = window.markdownit({
  8107. html: !is_escapeHTML,
  8108. });
  8109. if (!text) text = "";
  8110. let html = md.render(text);
  8111. this.mainDiv.html(html);
  8112. // 渲染Latex
  8113. if (is_renderLaTeX) {
  8114. // MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.mainDiv.get(0)]);
  8115. this.renderLaTeX(this.mainDiv.get(0));
  8116. }
  8117. // // 渲染代码块中的公式 (AtCoder)
  8118. // this.mainDiv.find('pre code').each((index, element) => {
  8119. // const codeText = $(element).text();
  8120. // const latexPattern = /\$\$(\\.|[^\$])*?\$\$|\$(\\.|[^\$])*?\$/;
  8121. // if (latexPattern.test(codeText)) {
  8122. // this.renderLaTeX(element);
  8123. // }
  8124. // });
  8125. }
  8126.  
  8127. /**
  8128. * 关闭元素
  8129. */
  8130. close() {
  8131. this.closeButton.click();
  8132. }
  8133.  
  8134. /**
  8135. * 注册收起按钮事件
  8136. */
  8137. registerUpButtonEvent() {
  8138. this.upButton.on("click", () => {
  8139. // 如果没有reverse类,说明是展开状态
  8140. if (!this.upButton.hasClass("reverse")) {
  8141. // 执行收起操作
  8142. this.upButton.addClass("reverse");
  8143. this.upButton.setButtonState('initial', i18next.t('fold.unfold', { ns: 'button' }));
  8144. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8145. } else {
  8146. // 执行展开操作
  8147. this.upButton.removeClass("reverse");
  8148. this.upButton.setButtonState('initial', i18next.t('fold.normal', { ns: 'button' }));
  8149. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8150. }
  8151. });
  8152. }
  8153.  
  8154. /**
  8155. * 注册关闭按钮事件
  8156. */
  8157. registerCloseButtonEvent() {
  8158. this.closeButton.on("click", () => {
  8159. $(this.div).remove();
  8160. $(this.panelDiv).remove();
  8161. if (OJBetter.typeOfPage.is_problem && OJBetter.translation.memory.enabled) {
  8162. OJBetter.translation.memory.ttTree.rmTransResultMap(this.id); // 移除ttTree中的数据
  8163. OJBetter.translation.memory.ttTree.refreshNode(".ttypography");
  8164. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新DB中的数据
  8165. }
  8166. });
  8167. }
  8168.  
  8169. /**
  8170. * 注册复制按钮事件
  8171. * @param {string} text 复制的文本
  8172. */
  8173. registerCopyButtonEvent(text) {
  8174. this.copyButton.on("click", () => {
  8175. GM_setClipboard(text);
  8176. this.copyButton.setButtonState('success', i18next.t('copy.copied', { ns: 'button' }));
  8177. // 复制提示
  8178. setTimeout(() => {
  8179. this.copyButton.setButtonState('initial', i18next.t('copy.normal', { ns: 'button' }));
  8180. }, 2000);
  8181. });
  8182. }
  8183.  
  8184. /**
  8185. * 禁用复制按钮
  8186. */
  8187. disableCopyButton() {
  8188. this.copyButton.css({ 'fill': '#ccc' });
  8189. this.copyButton.off("click");
  8190. }
  8191.  
  8192. /**
  8193. * 设置面板为error状态
  8194. */
  8195. setError() {
  8196. this.div.addClass('error');
  8197. this.panelDiv.addClass('error');
  8198. this.mainDiv.addClass('error');
  8199. }
  8200.  
  8201. /**
  8202. * 设置原始数据数据
  8203. * @param {Object} Object 原始数据
  8204. */
  8205. setRawData(Object) {
  8206. this.mainDivState.rawDataHTML = $("<pre>").text(JSON.stringify(Object, null, 4)).get(0);
  8207. if (this.mainDivState.current === 'rawDataHTML') {
  8208. this.renderMainDiv();
  8209. }
  8210. }
  8211.  
  8212. /**
  8213. * 切换结果面板与原始数据面板
  8214. */
  8215. switchMainDiv() {
  8216. // 在切换之前,保存当前内容的状态
  8217. this.mainDivState[this.mainDivState.current] = this.mainDiv.html();
  8218. // 切换当前状态
  8219. this.debugButton.setButtonState(this.mainDivState.current === 'transHTML' ? 'enabled' : 'initial');
  8220. this.mainDivState.current = this.mainDivState.current === 'transHTML' ? 'rawDataHTML' : 'transHTML';
  8221. // 渲染新的当前状态
  8222. this.renderMainDiv();
  8223. }
  8224.  
  8225. // 渲染当前内容到 mainDiv
  8226. renderMainDiv() {
  8227. requestAnimationFrame(() => {
  8228. this.mainDiv.html(this.mainDivState[this.mainDivState.current]);
  8229. });
  8230. }
  8231.  
  8232. /**
  8233. * 注册debug按钮事件
  8234. */
  8235. registerDebugButtonEvent() {
  8236. this.debugButton.on("click", () => {
  8237. this.switchMainDiv();
  8238. });
  8239. }
  8240.  
  8241. /**
  8242. * 显示debug按钮
  8243. */
  8244. showDebugButton() {
  8245. this.debugButton.show();
  8246. this.registerDebugButtonEvent();
  8247. }
  8248.  
  8249. /**
  8250. * 注册查询余额按钮事件
  8251. * @param {function} callback 查询回调函数
  8252. */
  8253. registerQueryBalanceButtonEvent(callback) {
  8254. this.queryBalanceButton.on("click", async () => {
  8255. this.queryBalanceButton.setButtonState('loading', i18next.t('queryBalance.loading', { ns: 'button' }));
  8256. try {
  8257. const balance = await callback();
  8258. this.queryBalanceButton.setButtonState('success', `${i18next.t('queryBalance.success', { ns: 'button' })} ${balance}`);
  8259. } catch (error) {
  8260. this.queryBalanceButton.setButtonState('error', `${i18next.t('queryBalance.error', { ns: 'button' })} ${error.message}`);
  8261. }
  8262. });
  8263. }
  8264.  
  8265. /**
  8266. * 显示余额查询按钮
  8267. * @param {string} server 服务名称
  8268. */
  8269. showQueryBalanceButton(server) {
  8270. if (server == 'deepl') {
  8271. const quotaConfig = OJBetter.deepl.config.quota;
  8272. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8273. this.queryBalanceButton.show();
  8274. this.registerQueryBalanceButtonEvent(() => {
  8275. return queryServerBalance(OJBetter.deepl.config.quota);
  8276. });
  8277. }
  8278. } else if (server == 'openai') {
  8279. const quotaConfig = OJBetter.chatgpt.config.quota;
  8280. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8281. this.queryBalanceButton.show();
  8282. this.registerQueryBalanceButtonEvent(() => {
  8283. return queryServerBalance(OJBetter.chatgpt.config.quota);
  8284. });
  8285. }
  8286. }
  8287. }
  8288. }
  8289.  
  8290. // 元素关系树
  8291. class ElementsTree {
  8292. constructor(elements) {
  8293. this.node = [];
  8294. this.transResultMap = {};
  8295. this.index = 0;
  8296. this.tagNames = ["DIV", "P", "UL", "LI"]
  8297. this.init($(elements));
  8298. }
  8299.  
  8300. // Iterate through all elements, because there may be multiple ttypography
  8301. init(elements) {
  8302. elements.each((i, e) => {
  8303. this.node.push({}); // add one element
  8304. this.index = 0; // reset index
  8305. this.create(i, $(e));
  8306. });
  8307. }
  8308.  
  8309. // 刷新关系树
  8310. refreshNode(elements) {
  8311. this.node = [];
  8312. this.index = 0;
  8313. this.init($(elements));
  8314. }
  8315.  
  8316. // 创建节点间的关系树
  8317. create(i_, element) {
  8318. var prev = null;
  8319. var node = this.node[i_];
  8320. element.children().each((i, e) => {
  8321. // only add element with tagNames
  8322. if (this.tagNames.includes($(e).prop("tagName"))) {
  8323. prev = this.addNode(i_, prev, e);
  8324. }
  8325. // recursively child element
  8326. if ($(e).children().length > 0 && prev !== null) {
  8327. node[prev].firstChild = this.index;
  8328. this.create(i_, $(e));
  8329. }
  8330. });
  8331. }
  8332.  
  8333. // 向树中添加一个节点
  8334. addNode(i_, prev, e) {
  8335. let node = this.node[i_];
  8336. node[this.index] = {
  8337. prev: prev,
  8338. next: null,
  8339. firstChild: null,
  8340. type: $(e).prop("tagName"),
  8341. isTranslateDiv: $(e).hasClass("translateDiv"),
  8342. topText: $(e).attr("data-topText"),
  8343. id: $(e).attr("id"),
  8344. };
  8345.  
  8346. if (prev !== null) {
  8347. node[prev].next = this.index;
  8348. }
  8349.  
  8350. prev = this.index;
  8351.  
  8352. this.index++;
  8353. return prev;
  8354. }
  8355.  
  8356. getNodeData() {
  8357. return this.node;
  8358. }
  8359.  
  8360. setNodeData(node) {
  8361. this.node = node;
  8362. }
  8363.  
  8364. getTransResultMap() {
  8365. return this.transResultMap;
  8366. }
  8367.  
  8368. setTransResultMap(transResultMap) {
  8369. this.transResultMap = transResultMap;
  8370. }
  8371.  
  8372. rmTransResultMap(id) {
  8373. delete this.transResultMap[id];
  8374. }
  8375.  
  8376. addTransResultMap(id, text) {
  8377. this.transResultMap[id] = text;
  8378. }
  8379.  
  8380. getTranslateDivNum(ttTree) {
  8381. var num = 0;
  8382. for (var i in ttTree) {
  8383. if (ttTree[i].isTranslateDiv) {
  8384. num++;
  8385. }
  8386. }
  8387. return num;
  8388. }
  8389.  
  8390. // 恢复目标元素中的translateDiv
  8391. recover(elements) {
  8392. elements.each((i, e) => {
  8393. var ttTreeNode = this.node[i];
  8394. var missingTranslateDivs = this.getTranslateDivNum(ttTreeNode);
  8395. if (missingTranslateDivs > 0) {
  8396. this.recoverOneElement($(e), ttTreeNode);
  8397. }
  8398. });
  8399. }
  8400.  
  8401. recoverOneElement(element, ttTreeNode) {
  8402. this.recoverOneFork(element.children().eq(0), ttTreeNode, 0);
  8403. }
  8404.  
  8405. // 恢复一个分支
  8406. recoverOneFork(pElement, ttTreeNode, index) {
  8407. do {
  8408. // only recover element with tagNames
  8409. if (!this.tagNames.includes(pElement.prop("tagName"))) {
  8410. if (pElement.next().length > 0) {
  8411. pElement = pElement.next();
  8412. } else {
  8413. return;
  8414. }
  8415. }
  8416. if (!ttTreeNode[index] || pElement.prop("tagName") !== ttTreeNode[index].type) {
  8417. // console.warn(`元素不存在或类型不同, 元素结构可能已经发生了变化: \nindex: ${index}`, pElement);
  8418. return;
  8419. } else {
  8420. // recursively child element
  8421. var node = ttTreeNode[index];
  8422. if (node.firstChild !== null) {
  8423. this.recoverOneFork(
  8424. pElement.children().eq(0),
  8425. ttTreeNode,
  8426. node.firstChild
  8427. );
  8428. }
  8429. // check if next node is translateDiv
  8430. if (node.next !== null) {
  8431. index = node.next;
  8432.  
  8433. var ne_node = ttTreeNode[index];
  8434. if (ne_node.isTranslateDiv) {
  8435. var id = ne_node.id;
  8436. var topText = ne_node.topText;
  8437. var text = this.transResultMap[id];
  8438. // create element after pElement
  8439. this.reCreateTransDiv(pElement, id, text, topText);
  8440. }
  8441. pElement = pElement.next(); // go to next element
  8442. }
  8443. }
  8444. } while (node.next !== null);
  8445. }
  8446.  
  8447. // 重新创建translateDiv
  8448. reCreateTransDiv(pElement, id, translatedText, topText) {
  8449. const translateDiv = new TranslateDiv(id);
  8450. pElement.after(translateDiv.getDiv());
  8451. translateDiv.setTopText(topText);
  8452. translateDiv.registerUpButtonEvent();
  8453. translateDiv.registerCloseButtonEvent();
  8454. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8455. translateDiv.registerCopyButtonEvent(translatedText);
  8456. } else {
  8457. translateDiv.disableCopyButton();
  8458. }
  8459. translateDiv.updateTranslateDiv(translatedText, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru));
  8460. // 标记已翻译并添加到翻译按钮的结果栈中
  8461. let transButton = pElement.prev('.html2md-panel').find('.translateButton');
  8462. if (transButton.length == 0) {
  8463. // 如果没有找到,则应该是得在父元素中找到
  8464. transButton = pElement.parent().prev('.html2md-panel').find('.translateButton');
  8465. }
  8466. transButton.pushResultToTransButton({
  8467. translateDiv: translateDiv,
  8468. status: 0
  8469. });
  8470. transButton.setTransButtonState('success');
  8471. }
  8472. }
  8473.  
  8474. // 更新TransDB中的翻译数据
  8475. async function updateTransDBData(nodeDate, transResultMap) {
  8476. var url = window.location.href.replace(/#/, "");
  8477. try {
  8478. await OJBetter.common.database.translateData.put({ url, transResultMap, nodeDate });
  8479. return 'translateData saved successfully';
  8480. } catch (error) {
  8481. throw new Error(`Failed to save translateData: ${error}`);
  8482. }
  8483. }
  8484.  
  8485. // 获取TransDB中保存的翻译数据
  8486. async function getTransDBData() {
  8487. var url = window.location.href.replace(/#/, "");
  8488. try {
  8489. const result = await OJBetter.common.database.translateData.get(url);
  8490. return result;
  8491. } catch (error) {
  8492. throw new Error(`Failed to get translateData: ${error}`);
  8493. }
  8494. }
  8495.  
  8496. /**
  8497. * 翻译结果恢复功能初始化
  8498. * @returns
  8499. */
  8500. async function initTransResultsRecover() {
  8501. OJBetter.translation.memory.ttTree = new ElementsTree(".ttypography"); // 初始化当前页面.ttypography元素的结构树
  8502. let result = await getTransDBData();
  8503. if (!result) return;
  8504. OJBetter.translation.memory.ttTree.setNodeData(result.nodeDate);
  8505. OJBetter.translation.memory.ttTree.setTransResultMap(result.transResultMap);
  8506. OJBetter.translation.memory.ttTree.recover($(".ttypography"));
  8507. }
  8508.  
  8509. /**
  8510. * 自动翻译
  8511. */
  8512. async function initTransWhenViewable() {
  8513. await waitForMathJaxIdle();
  8514.  
  8515. const elements = $('.ttypography, .comments').find('.translateButton');
  8516. const observers = [];
  8517.  
  8518. // Use a single Intersection Observer for all elements
  8519. const observer = new IntersectionObserver((entries, obs) => {
  8520. entries.forEach((entry) => {
  8521. if (entry.isIntersecting) {
  8522. const button = $(entry.target);
  8523. const state = button.getButtonState();
  8524. const notAutoTranslate = button.getNotAutoTranslate();
  8525. // Check if the button meets the criteria
  8526. if (state === 'normal' && !notAutoTranslate) {
  8527. let trans = OJBetter.translation.choice;
  8528.  
  8529. if (OJBetter.translation.auto.mixTrans.enabled && button.IsCommentButton() && OJBetter.translation.auto.mixTrans.servers.length > 0) {
  8530. const randomIndex = Math.floor(Math.random() * OJBetter.translation.auto.mixTrans.servers.length);
  8531. trans = OJBetter.translation.auto.mixTrans.servers[randomIndex];
  8532. }
  8533. button.data("translatedItBy")(trans);
  8534. }
  8535.  
  8536. // Stop observing the element
  8537. obs.unobserve(entry.target);
  8538. }
  8539. });
  8540. });
  8541.  
  8542. // Observe each element
  8543. elements.each((i, e) => {
  8544. observer.observe(e);
  8545. });
  8546.  
  8547. // Store the observer in case you need to disconnect it later
  8548. observers.push(observer);
  8549. }
  8550.  
  8551. /**
  8552. * 翻译返回结果结构体
  8553. * @typedef {Object} TranslateResult
  8554. * @property {string} status 翻译状态
  8555. * @property {TranslateDiv} translateDiv 翻译结果面板
  8556. * @property {TransRawData} rawData 原始翻译数据
  8557. */
  8558.  
  8559. /**
  8560. * 翻译主方法
  8561. * @param {string} text 待翻译文本
  8562. * @param {HTMLElement} element_node 元素节点
  8563. * @param {Boolean} is_comment 是否为评论区文本
  8564. * @param {string} overrideTrans 覆盖全局翻译服务设定
  8565. * @returns {TranslateResult} 翻译结果对象
  8566. */
  8567. async function translateProblemStatement(text, element_node, is_comment, overrideTrans) {
  8568. /** @type {number} 翻译结果的ID*/
  8569. const id = OJB_getRandomNumber(8);
  8570. /** @type {TextBlockReplacer} 文本块替换/恢复实例*/
  8571. const textBlockReplacer = new TextBlockReplacer();
  8572. /** @type {string} 翻译结果文本*/
  8573. let translatedText = "";
  8574.  
  8575. /** @type {string} 当前实际应用的翻译服务 */
  8576. const realTransServer = overrideTrans ||
  8577. (is_comment && OJBetter.translation.comment.choice != "0" ?
  8578. OJBetter.translation.comment.choice :
  8579. OJBetter.translation.choice);
  8580.  
  8581. /** @type {TranslateResult} 翻译结果对象 */
  8582. const translateResult = {
  8583. status: "ok",
  8584. rawData: {
  8585. done: false
  8586. }
  8587. }
  8588.  
  8589. /**
  8590. * LaTeX替换
  8591. * @param {string} text 待翻译文本
  8592. * @returns {string} 处理后的文本
  8593. */
  8594. const replaceLatex = function (text) {
  8595. if (OJBetter.typeOfPage.is_oldLatex) {
  8596. const regex = /<span\s+class="tex-span">.*?<\/span>/gi;
  8597. text = textBlockReplacer.replace(text, regex);
  8598. text = text.replace(/<p>(.*?)<\/p>/g, "$1\n\n"); // <p/>标签换为换行
  8599. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8600. const regex = /<i>.*?<\/i>|<sub>.*?<\/sub>|<sup>.*?<\/sup>|<pre>.*?<\/pre>/gi;
  8601. text = textBlockReplacer.replace(text, regex);
  8602. } else if (realTransServer != "openai") {
  8603. // 使用GPT翻译时不必替换latex公式
  8604. const regex = /\$\$(\\.|[^\$])*?\$\$|\$(\\.|[^\$])*?\$/g;
  8605. text = textBlockReplacer.replace(text, regex);
  8606.  
  8607. // 替换行间代码块```
  8608. const regex2 = /```[\s\S]*?```/g;
  8609. text = textBlockReplacer.replace(text, regex2);
  8610. }
  8611. return text;
  8612. }
  8613.  
  8614. /**
  8615. * LaTeX恢复
  8616. * @param {string} text 已翻译的文本
  8617. * @returns {string} 恢复后的文本
  8618. */
  8619. const recoverLatex = function (text) {
  8620. // 两个公式之间加个空格,防止有些LaTeX解析器解析错误
  8621. let resultText = text
  8622. .replace(/】【/g, '】 【')
  8623. .replace(/\]\[/g, '] [')
  8624. .replace(/\}\{/g, '} {');
  8625.  
  8626. if (OJBetter.typeOfPage.is_oldLatex) {
  8627. resultText = resultText.replace(/(.+?)(\n\n|$)/g, "<p>$1</p>"); // 换行符还原为<p/>标签
  8628. resultText = textBlockReplacer.recover(resultText);
  8629. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8630. resultText = textBlockReplacer.recover(resultText);
  8631. } else if (realTransServer != "openai") {
  8632. resultText = textBlockReplacer.recover(resultText);
  8633. }
  8634. return resultText;
  8635. }
  8636.  
  8637. /**
  8638. * 格式化翻译结果
  8639. * @param {string} text
  8640. * @returns {string} 处理后的翻译结果
  8641. */
  8642. const formatText = function (text) {
  8643. // 转义LaTex中的特殊符号
  8644. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8645. // 先替换掉行间代码块
  8646. const replacer = new TextBlockReplacer();
  8647. text = replacer.replace(text, /```[\s\S]*?```/g);
  8648.  
  8649. // 处理LaTeX公式
  8650. const escapeRules = [
  8651. { pattern: /(?<!\\)>(?!\s)/g, replacement: " &gt; " }, // >符号
  8652. { pattern: /(?<!\\)</g, replacement: " &lt; " }, // <符号
  8653. { pattern: /(?<!\\)\*/g, replacement: " &#42; " }, // *符号
  8654. { pattern: /(?<!\\)_/g, replacement: " &#95; " }, // _符号
  8655. { pattern: /(?<!\\)\\\\(?=\s)/g, replacement: "\\\\\\\\" }, // \\符号
  8656. { pattern: /(?<!\\)\\(?![\\a-zA-Z0-9])/g, replacement: "\\\\" }, // \符号
  8657. ];
  8658.  
  8659. let latexMatches = [...text.matchAll(/\$\$([\s\S]*?)\$\$|\$(.*?)\$|\$([\s\S]*?)\$/g)];
  8660. for (const match of latexMatches) {
  8661. const matchedText = match[0];
  8662. let escapedText = matchedText;
  8663.  
  8664. for (const rule of escapeRules) {
  8665. escapedText = escapedText.replaceAll(rule.pattern, rule.replacement);
  8666. }
  8667. escapedText = escapedText.replace(/\$\$/g, "$$$$$$$$");// $$符号(因为后面需要作为replacement,双倍消耗)
  8668. text = text.replace(matchedText, escapedText);
  8669. }
  8670.  
  8671. // 恢复行间代码块
  8672. text = replacer.recover(text);
  8673. }
  8674.  
  8675. // 使符合mathjx的转换语法
  8676. const mathjaxRuleMap = [
  8677. { pattern: /\$/g, replacement: "$$$$$$" }, // $$ 行间
  8678. ];
  8679. mathjaxRuleMap.forEach(({ pattern, replacement }) => {
  8680. text = text.replace(pattern, replacement);
  8681. });
  8682.  
  8683. // markdown修正
  8684. const mdRuleMap = [
  8685. { pattern: /(\s_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: "$1 $2" }, // 斜体
  8686. { pattern: /(_[\u4e00-\u9fa5]+_\s)([\u4e00-\u9fa5]+)/g, replacement: " $1$2" },
  8687. { pattern: /(_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: " $1 $2" },
  8688. { pattern: /(([\s\S]*?))/g, replacement: "($1)" }, // 中文()
  8689. // { pattern: /:/g, replacement: ":" }, // 中文:
  8690. { pattern: /\*\* (.*?) \*\*/g, replacement: "\*\*$1\*\*" } // 加粗
  8691. ];
  8692. mdRuleMap.forEach(({ pattern, replacement }) => {
  8693. text = text.replace(pattern, replacement);
  8694. });
  8695.  
  8696. return text;
  8697. }
  8698.  
  8699. // 创建翻译结果元素并放在element_node的后面
  8700. translateResult.translateDiv = new TranslateDiv(id);
  8701. $(element_node).after(translateResult.translateDiv.getDiv());
  8702.  
  8703. // 顶栏左侧信息
  8704. translateResult.translateDiv.setTopText(i18next.t('servers.' + realTransServer, { ns: 'translator' }) +
  8705. i18next.t('translateDiv.topTextSuffix', { ns: 'translator' }));
  8706.  
  8707. // 注册按钮
  8708. translateResult.translateDiv.registerUpButtonEvent();
  8709. translateResult.translateDiv.registerCloseButtonEvent();
  8710. if (OJBetter.translation.choice == 'openai' || OJBetter.translation.choice == 'deepl') {
  8711. translateResult.translateDiv.showQueryBalanceButton(OJBetter.translation.choice); // 显示额度查询
  8712. }
  8713.  
  8714. // 翻译内容是否为空文本
  8715. if (isEmptyText(text)) {
  8716. const shouldContinue = await OJB_createDialog(
  8717. i18next.t('isEmptyText.title', { ns: 'dialog' }),
  8718. i18next.t('isEmptyText.content', { ns: 'dialog' }),
  8719. [
  8720. i18next.t('isEmptyText.buttons.0', { ns: 'dialog' }),
  8721. i18next.t('isEmptyText.buttons.1', { ns: 'dialog' })
  8722. ],
  8723. true
  8724. );
  8725. if (shouldContinue) {
  8726. translateResult.status = "skip";
  8727. return translateResult;
  8728. }
  8729. }
  8730.  
  8731. // 替换latex公式
  8732. text = replaceLatex(text);
  8733.  
  8734. // 过滤**号
  8735. if (OJBetter.translation.filterTextWithoutEmphasis && GM_getValue("translation") !== "openai") { // TODO
  8736. text = text.replace(/\*\*/g, "");
  8737. }
  8738.  
  8739. // 字符数上限
  8740. const translationLimits = {
  8741. deepl: 5000,
  8742. iflyrec: 2000,
  8743. youdao: 600,
  8744. google: 5000,
  8745. caiyun: 5000
  8746. };
  8747. if (translationLimits.hasOwnProperty(realTransServer) && text.length > translationLimits[realTransServer]) {
  8748. let textLength = translationLimits[realTransServer];
  8749. let realTextLength = text.length;
  8750. const shouldContinue = await OJB_createDialog(
  8751. i18next.t('transTextLimits.title', { ns: 'dialog' }),
  8752. i18next.t('transTextLimits.content', { ns: 'dialog', textLength: textLength, realTextLength: realTextLength }),
  8753. [
  8754. i18next.t('transTextLimits.buttons.0', { ns: 'dialog' }),
  8755. i18next.t('transTextLimits.buttons.1', { ns: 'dialog' })
  8756. ],
  8757. true
  8758. ); // 字数超限确认
  8759. if (shouldContinue) {
  8760. translateResult.status = "skip";
  8761. return translateResult;
  8762. }
  8763. }
  8764.  
  8765. /**
  8766. * 调用各个翻译服务
  8767. * @param {string} transServer 翻译服务
  8768. * @returns {TransRawData} 原始翻译数据
  8769. */
  8770. async function translate(transServer) {
  8771. const is_renderLaTeX = !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru);
  8772. const servername = i18next.t('servers.' + realTransServer, { ns: 'translator' });
  8773. /** @type {TransRawData} 原始翻译数据*/
  8774. let rawData = {};
  8775. try {
  8776. if (transServer == "deepl") {
  8777. if (OJBetter.deepl.config.type == 'free') {
  8778. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8779. rawData = await translate_deepl(text);
  8780. } else if (OJBetter.deepl.config.type == 'api') {
  8781. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.deeplApi', { ns: 'translator', deepl_configName: OJBetter.deepl.config.name })}`, is_renderLaTeX);
  8782. if (OJBetter.deepl.config.apiGenre == 'deeplx') {
  8783. rawData = await translate_deeplx(text);
  8784. } else {
  8785. if (OJBetter.deepl.enableEmphasisProtection) text = convertBoldMarkdownToHTML(text);
  8786. if (OJBetter.deepl.enableLinkProtection) text = convertLinksMarkdownToHTML(text);
  8787. if (OJBetter.deepl.config.apiGenre == 'api-free') {
  8788. rawData = await translate_deepl_api_free(text);
  8789. } else if (OJBetter.deepl.config.apiGenre == 'api-pro') {
  8790. rawData = await translate_deepl_api_pro(text);
  8791. }
  8792. if (OJBetter.deepl.enableEmphasisProtection) rawData.text = convertBoldHTMLToMarkdown(rawData.text);
  8793. if (OJBetter.deepl.enableLinkProtection) rawData.text = convertLinksHTMLToMarkdown(rawData.text);
  8794. }
  8795. }
  8796. } else if (transServer == "iflyrec") {
  8797. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8798. rawData = await translate_iflyrec(text);
  8799. } else if (transServer == "youdao") {
  8800. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8801. rawData = await translate_youdao_mobile(text);
  8802. } else if (transServer == "google") {
  8803. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8804. rawData = await translate_gg(text);
  8805. } else if (transServer == "caiyun") {
  8806. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8807. rawData = await translate_caiyun(text);
  8808. } else if (transServer == "openai") {
  8809. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.openai', { ns: 'translator', openai_name: OJBetter.chatgpt.config.name })}${!OJBetter.chatgpt.isStream
  8810. ? i18next.t('transingTip.openai_isStream', { ns: 'translator' }) : ""}`,
  8811. is_renderLaTeX);
  8812. if (OJBetter.chatgpt.isStream) {
  8813. // 流式传输
  8814. rawData = await translate_openai_stream(text, translateResult.translateDiv);
  8815. } else {
  8816. // 普通模式
  8817. rawData = await translate_openai(text);
  8818. }
  8819. }
  8820. translatedText = rawData.text;
  8821. if (!rawData.done) {
  8822. translateResult.status = "error";
  8823. }
  8824. } catch (e) {
  8825. translateResult.status = "error";
  8826. rawData.message = i18next.t('error.unexpected', { ns: 'translator' });
  8827. console.warn(e);
  8828. }
  8829. return rawData;
  8830. }
  8831. translateResult.rawData = await translate(realTransServer);
  8832.  
  8833. if (translateResult.status == "error") {
  8834. translateResult.translateDiv.updateTranslateDiv(translateResult.rawData.message);
  8835. return translateResult;
  8836. }
  8837.  
  8838. // 还原latex公式
  8839. translatedText = recoverLatex(translatedText);
  8840.  
  8841. // 注册结果复制按钮
  8842. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8843. translateResult.translateDiv.registerCopyButtonEvent(translatedText);
  8844. } else {
  8845. translateResult.translateDiv.disableCopyButton();
  8846. }
  8847.  
  8848. // 翻译结果格式化
  8849. translatedText = formatText(translatedText);
  8850.  
  8851. // 保存翻译结果
  8852. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  8853. OJBetter.translation.memory.ttTree.refreshNode(".ttypography"); // 刷新当前页面.ttypography元素的结构树实例
  8854. OJBetter.translation.memory.ttTree.addTransResultMap(id, translatedText);
  8855. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新翻译结果到transDB
  8856. }
  8857.  
  8858. // 翻译结果面板更新
  8859. translateResult.translateDiv.updateTranslateDiv(translatedText, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru));
  8860.  
  8861. return translateResult;
  8862. }
  8863.  
  8864. //弹窗翻译
  8865. function alertZh() {
  8866. // var _alert = window.alert;
  8867. // window.alert = async function (msg) {
  8868. // _alert(msg + "\n=========翻译=========\n" + await translate_deepl(msg));
  8869. // return true;
  8870. // }
  8871. };
  8872.  
  8873. /**
  8874. * 折叠块展开
  8875. */
  8876. function ExpandFoldingblocks() {
  8877. $('.spoiler').addClass('spoiler-open');
  8878. $('.spoiler-content').attr('style', '');
  8879. };
  8880.  
  8881. /**
  8882. * 折叠块渲染优化
  8883. */
  8884. function RenderPerfOpt() {
  8885. GM_addStyle(`
  8886. .spoiler-content {
  8887. contain: layout style;
  8888. }
  8889. `);
  8890. }
  8891.  
  8892. /**
  8893. * 下拉选择框性能优化
  8894. */
  8895. async function SelectElementPerfOpt() {
  8896. // TODO 10
  8897. // 加载库资源
  8898. await OJB_LoadJS("https://aowuucdn.oss-accelerate.aliyuncs.com/js/selectpage.min.js");
  8899. /**
  8900. * 将一个<select>元素转换为SelectPage控件
  8901. * @param {HTMLElement|string} selector - 要转换的<select>元素或其选择器
  8902. */
  8903. const OJB_transformSelectToSelectPage = (selector) => {
  8904. const $select = $(selector);
  8905. if ($select.length === 0 || !$select.is('select')) {
  8906. console.error('Invalid select element provided.');
  8907. return;
  8908. }
  8909.  
  8910. // 隐藏原生的<select>元素
  8911. $select.hide();
  8912.  
  8913. // 创建一个新的<input>元素用于SelectPage控件
  8914. const $inputForSelectPage = $('<input>', {
  8915. type: 'text',
  8916. class: 'selectpage-input',
  8917. autocomplete: 'off'
  8918. });
  8919. $select.after($inputForSelectPage);
  8920.  
  8921. // 准备SelectPage所需的数据格式
  8922. const data = $select.find('option').map((_, option) => ({
  8923. id: option.value,
  8924. text: option.text
  8925. })).get();
  8926.  
  8927. // 初始化SelectPage
  8928. $inputForSelectPage.selectPage({
  8929. showField: 'text',
  8930. keyField: 'id',
  8931. data,
  8932. lang: 'en',
  8933. // 当选中一个选项时,更新隐藏的<select>元素的值
  8934. eSelect: (data) => {
  8935. $select.val(data.id).trigger('change');
  8936. },
  8937. // 初始化时根据<select>的当前值设置SelectPage
  8938. initRecord: $select.val()
  8939. });
  8940. };
  8941.  
  8942. // 遍历页面上的所有select
  8943. $('select').each((_, select) => {
  8944. // 选项大于500才优化
  8945. if ($(select).find('option').length > 500) {
  8946. OJB_transformSelectToSelectPage(select);
  8947. }
  8948. });
  8949. }
  8950.  
  8951. /**
  8952. * 评论区分页
  8953. */
  8954. function CommentPagination() {
  8955. GM_addStyle(`
  8956. .comments > .comment {
  8957. display: none;
  8958. }
  8959. #next-page-btn, #prev-page-btn {
  8960. display: none;
  8961. }
  8962. #jump-input, #items-per-page{
  8963. height: 22px;
  8964. border: 1px solid #dcdfe6;
  8965. border-radius: 0.3rem;
  8966. }
  8967. #jump-input:focus-visible{
  8968. border-style: solid;
  8969. border-color: #3f51b5;
  8970. outline: none;
  8971. }
  8972. `);
  8973. $('.comments').after(`
  8974. <div id="pagBar" style="display: flex; align-items: center; justify-content: center; color: #606266;">
  8975. <label for="items-per-page">${i18next.t('perpage', { ns: 'comments' })}</label>
  8976. <select id="items-per-page" style="margin-right: 15px;">
  8977. <option value="5">5</option>
  8978. <option value="10">10</option>
  8979. <option value="20">20</option>
  8980. </select>
  8981. <div class="paging" style="margin-right: 15px;">
  8982. <span id="current-page">1</span> / <span id="total-pages"></span>
  8983. </div>
  8984. <input type="text" id="jump-input" placeholder="${i18next.t('jumpTo', { ns: 'comments' })}">
  8985. <button type="button" id="jump-btn" class="ojb_btn">${i18next.t('jump', { ns: 'comments' })}</button>
  8986. <button id="prev-page-btn" class="ojb_btn">${i18next.t('prev', { ns: 'comments' })}</button>
  8987. <button id="next-page-btn" class="ojb_btn">${i18next.t('next', { ns: 'comments' })}</button>
  8988. </div>
  8989. `);
  8990.  
  8991. let batchSize = 5;
  8992. let elements = $(".comments > .comment").slice(0, -1);
  8993. let start = 0;
  8994. let end = batchSize;
  8995. let currentPage = 1;
  8996. let displayedIndexes = []; // 存储已显示元素的索引
  8997.  
  8998. function showBatch(start, end) {
  8999. // 隐藏上一页
  9000. for (var i = 0; i < displayedIndexes.length; i++) {
  9001. elements.eq(displayedIndexes[i]).hide();
  9002. }
  9003.  
  9004. displayedIndexes = [];
  9005.  
  9006. // 显示当前页
  9007. elements.slice(start, end).each(function (index) {
  9008. $(this).show();
  9009. displayedIndexes.push(start + index);
  9010. });
  9011.  
  9012. // 更新页码和翻页按钮
  9013. $("#current-page").text(currentPage);
  9014. $("#total-pages").text(Math.ceil(elements.length / batchSize));
  9015.  
  9016. if (currentPage === 1) $("#prev-page-btn").hide();
  9017. else $("#prev-page-btn").show();
  9018.  
  9019. if (end >= elements.length) $("#next-page-btn").hide();
  9020. else $("#next-page-btn").show();
  9021. }
  9022.  
  9023. // 初始化
  9024. var commentID = null;
  9025. var pageURL = window.location.href;
  9026. if (pageURL.includes("#comment-")) {
  9027. // 带评论区锚点的链接
  9028. var startIndex = pageURL.lastIndexOf("#comment-") + 9;
  9029. commentID = pageURL.substring(startIndex);
  9030. var indexInComments = null;
  9031. $(".comments > .comment").each(function (index) {
  9032. $(this).find(".comment-table").each(function () {
  9033. var tableCommentID = $(this).attr("commentid");
  9034. if (tableCommentID === commentID) {
  9035. indexInComments = index;
  9036. return false;
  9037. }
  9038. });
  9039. });
  9040. let page = Math.ceil((indexInComments + 1) / batchSize);
  9041. currentPage = !page ? 1 : page;
  9042. showBatch((currentPage - 1) * batchSize, currentPage * batchSize);
  9043. setTimeout(function () {
  9044. window.location.href = pageURL;
  9045. }, 1000);
  9046. } else {
  9047. showBatch(0, batchSize);
  9048. }
  9049.  
  9050. $("#prev-page-btn").on("click", function () {
  9051. var itemsPerPage = parseInt($("#items-per-page").val());
  9052. start = (currentPage - 2) * itemsPerPage;
  9053. end = (currentPage - 1) * itemsPerPage;
  9054. currentPage--;
  9055. showBatch(start, end);
  9056. });
  9057.  
  9058. $("#next-page-btn").on("click", function () {
  9059. var itemsPerPage = parseInt($("#items-per-page").val());
  9060. start = currentPage * itemsPerPage;
  9061. end = (currentPage + 1) * itemsPerPage;
  9062. currentPage++;
  9063. showBatch(start, end);
  9064. });
  9065.  
  9066. $("#jump-btn").on("click", function () {
  9067. var inputPage = parseInt($("#jump-input").val());
  9068.  
  9069. if (inputPage >= 1 && inputPage <= Math.ceil(elements.length / parseInt($("#items-per-page").val()))) {
  9070. var itemsPerPage = parseInt($("#items-per-page").val());
  9071. start = (inputPage - 1) * itemsPerPage;
  9072. end = inputPage * itemsPerPage;
  9073. currentPage = inputPage; // 更新当前页码
  9074. showBatch(start, end);
  9075. }
  9076. });
  9077.  
  9078. $("#items-per-page").on("change", function () {
  9079. batchSize = parseInt($(this).val());
  9080. let page = Math.ceil(end / batchSize);
  9081. currentPage = !page ? 1 : page;
  9082. let maxPage = Math.ceil(elements.length / batchSize);
  9083. if (currentPage > maxPage) currentPage = maxPage;
  9084. showBatch((currentPage - 1) * batchSize, currentPage * batchSize);
  9085. });
  9086. }
  9087.  
  9088. /**
  9089. * 题目页相关链接栏
  9090. */
  9091. class ProblemPageLinkbar {
  9092. constructor() {
  9093. this.containerElement = this.createToolbar();
  9094. this.commandInvoker = new CommandInvoker();
  9095. }
  9096.  
  9097. /**
  9098. * 创建工具栏
  9099. */
  9100. createToolbar() {
  9101. const toolbarElement = $("<div>").attr("id", "problemToolbar").insertBefore($(".problemindexholder"));
  9102. return new DOMContainer(toolbarElement);
  9103. }
  9104.  
  9105. /**
  9106. * 添加按钮
  9107. * @param {string} id 按钮id
  9108. * @param {string} url 按钮链接
  9109. * @param {string} text 按钮文字
  9110. * @param {JQueryObject} icon 按钮图标
  9111. * @param {string} iconHeight 图标高度
  9112. * @returns {object} 按钮对象
  9113. */
  9114. addLinkButton(id, url, text, icon = $('<div>'), iconHeight = "22px") {
  9115. const linkElement = $("<a>")
  9116. .attr("href", url)
  9117. .attr("target", "_blank")
  9118. .addClass("ojb_btn")
  9119. .attr("id", id);
  9120.  
  9121. linkElement.append(icon);
  9122. icon.css("height", iconHeight);
  9123.  
  9124. const textSpan = $("<span>").html(text);
  9125. linkElement.append(textSpan);
  9126.  
  9127. this.commandInvoker.execute(new AddElementCommand(this.containerElement, linkElement));
  9128. return {
  9129. element: linkElement,
  9130. text: textSpan,
  9131. icon: icon
  9132. };
  9133. }
  9134.  
  9135. /**
  9136. * 更新链接
  9137. * @param {object} button 按钮对象
  9138. * @param {string} url 按钮链接
  9139. */
  9140. updateUrl(button, url) {
  9141. button.element.attr("href", url);
  9142. }
  9143.  
  9144. /**
  9145. * 更新文字
  9146. * @param {object} button 按钮对象
  9147. * @param {string} text 按钮文字
  9148. */
  9149. updateText(button, text) {
  9150. button.text.html(text);
  9151. }
  9152.  
  9153. /**
  9154. * 设置文字为粗体
  9155. * @param {object} button 按钮对象
  9156. */
  9157. setBold(button) {
  9158. button.text.css("font-weight", "bold");
  9159. }
  9160.  
  9161. /**
  9162. * 更新图标
  9163. * @param {object} button 按钮对象
  9164. * @param {JQueryObject} icon 按钮图标
  9165. * @param {string} iconHeight 图标高度
  9166. */
  9167. updateIcon(button, icon, iconHeight = "16px") {
  9168. button.icon.remove();
  9169. button.text.prepend(icon);
  9170. icon.css("height", iconHeight);
  9171. button.icon = icon;
  9172. }
  9173.  
  9174. /**
  9175. * 添加类
  9176. * @param {object} button 按钮对象
  9177. * @param {string} className 类名
  9178. */
  9179. addClass(button, className) {
  9180. button.element.addClass(className);
  9181. }
  9182.  
  9183. /**
  9184. * 禁用链接按钮
  9185. * @param {object} button 按钮对象
  9186. */
  9187. disableButton(button) {
  9188. button.element.addClass("disabled");
  9189. }
  9190.  
  9191. /**
  9192. * 启用链接按钮
  9193. * @param {object} button 按钮对象
  9194. */
  9195. enableButton(button) {
  9196. button.element.removeClass("disabled");
  9197. }
  9198. }
  9199.  
  9200. /**
  9201. * 获取题目的id
  9202. * @param {String} url 题目的链接
  9203. * @returns 题目的id,形如2000A
  9204. */
  9205. function getProblemId(url) {
  9206. const regex = url.includes('/contest/')
  9207. ? /\/contest\/(\d+)\/problem\/([A-Za-z\d]+)/
  9208. : /\/problemset\/problem\/(\d+)\/([A-Za-z\d]+)/;
  9209. const matchResult = url.match(regex);
  9210. return matchResult && matchResult.length >= 3 ? `${matchResult[1]}${matchResult[2]}` : '';
  9211. };
  9212.  
  9213. /**
  9214. * 跳转到洛谷
  9215. * @param {ProblemPageLinkbar} problemToolbar
  9216. */
  9217. async function CF2luogu(problemToolbar) {
  9218. const url = window.location.href;
  9219. const problemId = getProblemId(url);
  9220. const luoguButton = problemToolbar.addLinkButton(
  9221. "luoguButton",
  9222. "https://www.luogu.com.cn/",
  9223. i18next.t('state.loading', { ns: 'button' }),
  9224. $("<img>").attr("src", "https://cdn.luogu.com.cn/fe/logo.png")
  9225. );
  9226.  
  9227. const checkLinkExistence = async (url) => {
  9228. return OJB_promiseRetryWrapper(async () => {
  9229. const response = await OJB_GMRequest({
  9230. method: "GET",
  9231. url
  9232. });
  9233. return !response.responseText.match(/出错了/g);
  9234. }, {
  9235. maxRetries: 3,
  9236. retryInterval: 1000
  9237. });
  9238. };
  9239.  
  9240. const LuoguUrl = `https://www.luogu.com.cn/problem/CF${problemId}`;
  9241. try {
  9242. const result = await checkLinkExistence(LuoguUrl);
  9243. if (problemId && result) {
  9244. problemToolbar.updateText(luoguButton, "");
  9245. problemToolbar.updateUrl(luoguButton, LuoguUrl);
  9246. } else {
  9247. problemToolbar.updateText(luoguButton, i18next.t('state.404', { ns: 'button' }));
  9248. problemToolbar.disableButton(luoguButton);
  9249. }
  9250. } catch (error) {
  9251. if (error instanceof OJB_GMError && error.type == "error") {
  9252. problemToolbar.updateText(luoguButton, i18next.t('state.netError', { ns: 'button' }));
  9253. problemToolbar.disableButton(luoguButton);
  9254. }
  9255. }
  9256. }
  9257.  
  9258. /**
  9259. * 跳转到 Virtual Judge
  9260. * @param {ProblemPageLinkbar} problemToolbar
  9261. */
  9262. async function CF2vjudge(problemToolbar) {
  9263. const url = window.location.href;
  9264. const problemId = getProblemId(url);
  9265. const vjudgeButton = problemToolbar.addLinkButton(
  9266. "vjudgeButton",
  9267. "https://vjudge.net/",
  9268. i18next.t('state.loading', { ns: 'button' }),
  9269. $("<img>").attr("src", "https://aowuucdn.oss-accelerate.aliyuncs.com/vjudge.ico")
  9270. );
  9271.  
  9272. const checkLinkExistence = async (url) => {
  9273. return OJB_promiseRetryWrapper(async () => {
  9274. const response = await OJB_GMRequest({
  9275. method: "HEAD",
  9276. url: url,
  9277. });
  9278. if (response.status >= 200 && response.status < 300) return true;
  9279. else if (response.status == 404) return false;
  9280. else throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9281. }, {
  9282. maxRetries: 3,
  9283. retryInterval: 1000
  9284. });
  9285. };
  9286.  
  9287. const VjudgeUrl = `https://vjudge.net/problem/CodeForces-${problemId}`;
  9288. try {
  9289. const result = await checkLinkExistence(VjudgeUrl);
  9290. if (problemId && result) {
  9291. problemToolbar.updateText(vjudgeButton, "VJudge");
  9292. problemToolbar.updateUrl(vjudgeButton, VjudgeUrl);
  9293. } else {
  9294. problemToolbar.updateText(vjudgeButton, i18next.t('state.404', { ns: 'button' }));
  9295. problemToolbar.disableButton(vjudgeButton);
  9296. }
  9297. } catch (error) {
  9298. if (error instanceof OJB_GMError && error.type == "error") {
  9299. problemToolbar.updateText(vjudgeButton, i18next.t('state.netError', { ns: 'button' }));
  9300. problemToolbar.disableButton(vjudgeButton);
  9301. }
  9302. }
  9303. }
  9304.  
  9305. // RatingClass
  9306. const ratingClassMap = {
  9307. NaN: "rating_by_clist_colorNaN",
  9308. 0: "rating_by_clist_color0",
  9309. 1200: "rating_by_clist_color1",
  9310. 1400: "rating_by_clist_color2",
  9311. 1600: "rating_by_clist_color3",
  9312. 1900: "rating_by_clist_color4",
  9313. 2100: "rating_by_clist_color5",
  9314. 2300: "rating_by_clist_color6",
  9315. 2400: "rating_by_clist_color7",
  9316. 2600: "rating_by_clist_color8",
  9317. 3000: "rating_by_clist_color9"
  9318. };
  9319. const cssMap = {
  9320. "rating_by_clist_colorNaN": "#cccccc",
  9321. "rating_by_clist_color0": "#808080",
  9322. "rating_by_clist_color1": "#73e473",
  9323. "rating_by_clist_color2": "#77ddbb",
  9324. "rating_by_clist_color3": "#aaaaff",
  9325. "rating_by_clist_color4": "#ff88ff",
  9326. "rating_by_clist_color5": "#ffcc88",
  9327. "rating_by_clist_color6": "#ffbb55",
  9328. "rating_by_clist_color7": "#ff7777",
  9329. "rating_by_clist_color8": "#ff3333",
  9330. "rating_by_clist_color9": "#aa0000"
  9331. };
  9332. // TODO 7
  9333. /**
  9334. * clist 访问有效性检查
  9335. * @param {boolean} onlyCookie 是否只检查Cookie
  9336. * @returns {Promise<boolean>} 是否有效
  9337. */
  9338. async function validateClistConnection(onlyCookie = false) {
  9339. const clistApiUrl = "https://clist.by:443/api/v4/contest/?limit=1&resource_id=1";
  9340. const requestOptions = {
  9341. method: "GET",
  9342. url: clistApiUrl,
  9343. timeout: 5000,
  9344. };
  9345.  
  9346. // 尝试发送请求
  9347. async function tryRequest(options) {
  9348. try {
  9349. const response = await OJB_GMRequest(options);
  9350. if (response.status === 200) {
  9351. return { ok: true };
  9352. } else if (response.status === 401) {
  9353. throw new Error('unauthorized');
  9354. } else if (response.status === 404) {
  9355. throw new Error('not_found');
  9356. } else {
  9357. throw new Error('other_error');
  9358. }
  9359. } catch (error) {
  9360. console.warn(`Error accessing clist.by: ${error.message}`);
  9361. return { ok: false, error: error.message };
  9362. }
  9363. }
  9364.  
  9365. // 尝试携带Key发送请求
  9366. let result = await tryRequest(requestOptions);
  9367. if (!onlyCookie && !result.ok) {
  9368. requestOptions.headers = { "Authorization": OJBetter.clist.authorization };
  9369. result = await tryRequest(requestOptions);
  9370. }
  9371.  
  9372. // 根据结果显示错误信息
  9373. if (!result.ok) {
  9374. let errorType = result.error;
  9375. const loadingMessage = new LoadingMessage();
  9376. let state;
  9377. if (errorType === 'not_found') {
  9378. state = i18next.t('error.clist.404', { ns: 'alert' });
  9379. } else if (errorType === 'unauthorized') {
  9380. state = i18next.t('error.clist.cookie', { ns: 'alert' });
  9381. } else {
  9382. state = i18next.t('error.clist.other', { ns: 'alert' });
  9383. }
  9384. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${state}`, 'error');
  9385. }
  9386. return result.ok;
  9387. }
  9388.  
  9389. /**
  9390. * 创建Rating相关css
  9391. * @param {boolean} [hasBorder=true] 是否有边框
  9392. */
  9393. function creatRatingCss(hasBorder = true) {
  9394. const defaultBorderColor = '#dcdfe6';
  9395. let dynamicCss = "";
  9396. let hoverSelector = OJBetter.clist.ratingHidden ? ":hover" : "";
  9397. for (let cssClass in cssMap) {
  9398. dynamicCss += `a.${cssClass}${hoverSelector}, a.${cssClass}${hoverSelector}:link {\n`;
  9399. let borderColor = hasBorder ? cssMap[cssClass] : defaultBorderColor;
  9400. dynamicCss += ` color: ${cssMap[cssClass]} ${OJBetter.clist.ratingHidden ? "!important" : ""};\n`;
  9401. dynamicCss += `}\n`;
  9402. }
  9403. GM_addStyle(dynamicCss);
  9404. if (OJBetter.clist.ratingHidden) {
  9405. GM_addStyle(`
  9406. #clistButton {
  9407. color: #ffffff00;
  9408. }
  9409. `);
  9410. }
  9411. }
  9412.  
  9413. /**
  9414. * 模拟clist网页访问获取rating
  9415. * @param {string} problem 题目名称
  9416. * @param {string} problem_url 题目链接
  9417. * @param {string} contest 比赛名称
  9418. * @returns {Promise<{rating: number, problem: string}>} 题目难度
  9419. */
  9420. async function getRatingFromHTML(problem, problem_url, contest = null) {
  9421. // 去除题目名称中的括号,以及首尾的空白符
  9422. problem = problem.replace(/\([\s\S]*?\)/g, '').trim();
  9423.  
  9424. return OJB_promiseRetryWrapper(async () => {
  9425. const queryString = `search=${encodeURIComponent(problem)}&resource=1`;
  9426. const response = await OJB_GMRequest({
  9427. method: 'GET',
  9428. url: `https://clist.by/problems/?${queryString}`,
  9429. });
  9430.  
  9431. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9432. const html = response.responseText;
  9433. const cleanedHtml = html.replace(/src=(.|\s)*?"/g, '');
  9434. const parser = new DOMParser();
  9435. const doc = parser.parseFromString(cleanedHtml, 'text/html');
  9436. const trs = doc.querySelectorAll('table tbody tr');
  9437.  
  9438. for (let tr of trs) {
  9439. const rating = tr.querySelector('.problem-rating-column').textContent.trim();
  9440. const link = OJB_cleanLink(tr.querySelector('.problem-name-column a:nth-of-type(2)')?.href);
  9441.  
  9442. if (link === problem_url || link === problem_url + '/') {
  9443. return {
  9444. rating: parseInt(rating),
  9445. problem: problem
  9446. };
  9447. } else if (contest !== null) {
  9448. const contestTitles = [...tr.querySelectorAll('.problem-name-column .pull-right a[title], .problem-name-column .pull-right span[title]')].map(el => el.title);
  9449. if (contestTitles.includes(contest)) {
  9450. return {
  9451. rating: parseInt(rating),
  9452. problem: problem
  9453. };
  9454. }
  9455. }
  9456. }
  9457. console.warn(`No data found for the question: ${problem}`);
  9458. }, {
  9459. maxRetries: 3,
  9460. retryInterval: 500
  9461. });
  9462. }
  9463.  
  9464. /**
  9465. * 从clist API获取题目的rating
  9466. * @param {string} problem_name 题目名
  9467. * @param {string} problem_url 题目链接
  9468. * @returns {Promise<number>} 题目rating
  9469. *
  9470. * 使用两个Map对象来存储和快速访问题目信息:
  9471. * - problemsMap: 通过题目的URL作为键来存储题目信息。
  9472. * - nameMap: 通过题目的名称作为键来存储题目信息。
  9473. *
  9474. * 每个题目信息是一个对象,包含以下属性:
  9475. * @typedef {Object} ProblemInfo
  9476. * @property {string} name 题目名称
  9477. * @property {string} url 题目URL
  9478. * @property {number} rating 题目评分,如果没有评分信息则为NaN
  9479. */
  9480. async function getRatingFromApi_problem(problem_name, problem_url) {
  9481. return OJB_promiseRetryWrapper(async () => {
  9482. const response = await OJB_GMRequest({
  9483. method: "GET",
  9484. url: `https://clist.by:443/api/v4/problem/?name=${encodeURIComponent(problem_name)}&resource__regex=codeforces.com`,
  9485. headers: { "Authorization": OJBetter.clist.authorization }
  9486. });
  9487.  
  9488. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9489. let data = JSON.parse(response.responseText);
  9490. /**
  9491. * 使用题目的URL作为键来存储题目信息。
  9492. * @type {Map<string, ProblemInfo>}
  9493. */
  9494. let problemsMap = new Map();
  9495.  
  9496. /**
  9497. * 使用题目的名称作为键来存储题目信息。
  9498. * @type {Map<string, ProblemInfo>}
  9499. */
  9500. let nameMap = new Map();
  9501.  
  9502. data.objects.forEach(problem => {
  9503. /** @type {ProblemInfo} 题目信息*/
  9504. let problemInfo = {
  9505. name: problem.name,
  9506. url: problem.url,
  9507. rating: problem.rating ? problem.rating : NaN
  9508. };
  9509. problemsMap.set(OJB_cleanLink(problem.url), problemInfo);
  9510. nameMap.set(problem.name, problemInfo);
  9511. });
  9512.  
  9513. if (problemsMap.has(problem_url)) {
  9514. return problemsMap.get(problem_url).rating;
  9515. } else if (nameMap.has(problem_name)) {
  9516. return nameMap.get(problem_name).rating;
  9517. } else {
  9518. console.warn('Problem not found in the response');
  9519. }
  9520. }, {
  9521. maxRetries: 5,
  9522. retryInterval: 1000
  9523. });
  9524. }
  9525.  
  9526. /**
  9527. * 获取字符串中的关键词列表
  9528. * @param {string} text 字符串文本
  9529. * @returns {array<string>} 返回关键词列表
  9530. */
  9531. function getKeywords(text) {
  9532. // 定义要过滤掉的高频词
  9533. const highFrequencyWords = ['Educational', 'Codeforces', 'Round', 'Div'];
  9534.  
  9535. // 使用正则表达式替换掉特殊符号(保留空格以便分词)
  9536. const sanitizedText = text.replace(/[^\w\s]|_/g, '').replace(/\s+/g, ' ');
  9537.  
  9538. // 将字符串拆分为单词数组
  9539. const words = sanitizedText.split(' ');
  9540.  
  9541. // 过滤掉高频词和空字符串
  9542. const filteredWords = words.filter(word => {
  9543. return word && highFrequencyWords.indexOf(word) === -1;
  9544. });
  9545.  
  9546. // 返回关键词列表
  9547. return filteredWords;
  9548. }
  9549.  
  9550. /**
  9551. * 根据关键词从 Clist API 中获取实际比赛名称
  9552. * @param {string} contestName 比赛名
  9553. * @param {string} contestUrl 比赛链接
  9554. * @returns {string|null} 该比赛在Clist中的实际名字
  9555. */
  9556. async function getContestNameFromApi(contestName, contestUrl) {
  9557. return OJB_promiseRetryWrapper(async () => {
  9558. const options = {
  9559. method: "GET",
  9560. url: `https://clist.by:443/api/v4/contest/?resource_id=1&event__regex=${encodeURIComponent(contestName)}`,
  9561. headers: {
  9562. "Authorization": OJBetter.clist.authorization
  9563. }
  9564. };
  9565.  
  9566. let response = await OJB_GMRequest(options);
  9567.  
  9568. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9569.  
  9570. let data = JSON.parse(response.responseText);
  9571. let objects = data.objects;
  9572.  
  9573. if (objects.length > 0) {
  9574. for (const contest of objects) {
  9575. const href = contest.href.replace(/\/contests\//i, '/contest/'); // 链接可能是contests而不是contest,换回来
  9576. if (OJB_cleanLink(href) == contestUrl) {
  9577. return contest.event;
  9578. }
  9579. }
  9580. }
  9581. return null;
  9582. }, {
  9583. maxRetries: 5,
  9584. retryInterval: 1000
  9585. });
  9586. }
  9587.  
  9588. /**
  9589. * 获取在clist中的实际比赛名称
  9590. * @param {string} contestName 待搜索的比赛名称
  9591. * @param {string} contestUrl 比赛的url
  9592. * @returns {Promise<string|null>} 在clist中的实际比赛名称,如果没有找到,则返回null
  9593. */
  9594. async function getActualContestName(contestName, contestUrl) {
  9595. // 首先尝试使用完整的比赛名称进行搜索
  9596. let actualContestName = await getContestNameFromApi(contestName, contestUrl);
  9597. if (actualContestName) {
  9598. return actualContestName;
  9599. }
  9600.  
  9601. // 如果使用完整名称没有找到,则尝试使用关键词进行搜索
  9602. const keywords = getKeywords(contestName);
  9603. const maxKeywordAttempts = 1; // 最多尝试到第几个关键词(因为Clist API有频率限制)
  9604. for (let i = 0; i < Math.min(keywords.length, maxKeywordAttempts); i++) {
  9605. actualContestName = await getContestNameFromApi(keywords[i], contestUrl);
  9606. if (actualContestName) {
  9607. return actualContestName;
  9608. }
  9609. }
  9610.  
  9611. // 如果全部尝试后仍没有找到,返回null
  9612. return null;
  9613. }
  9614.  
  9615. /**
  9616. * 从clist API获取比赛题目集的rating
  9617. * @param {string} contestName 比赛名
  9618. * @returns {Promise<Map<string, number>>} 题目rating
  9619. */
  9620. async function getRatingFromApi_contest(contestName, contestUrl) {
  9621. const actualContestName = await getActualContestName(contestName, contestUrl);
  9622. return OJB_promiseRetryWrapper(async () => {
  9623. const options = {
  9624. method: "GET",
  9625. url: `https://clist.by:443/api/v4/contest/?resource_id=1&with_problems=true&event=${encodeURIComponent(actualContestName)}`,
  9626. headers: {
  9627. "Authorization": OJBetter.clist.authorization
  9628. }
  9629. };
  9630.  
  9631. let response = await OJB_GMRequest(options);
  9632.  
  9633. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9634.  
  9635. let data = JSON.parse(response.responseText);
  9636. let objects = data.objects;
  9637. let problemsMap = new Map();
  9638.  
  9639. if (objects.length > 0 && objects[0].problems) {
  9640. objects[0].problems.forEach(problem => {
  9641. problemsMap.set(OJB_cleanLink(problem.url), problem.rating ? problem.rating : NaN);
  9642. });
  9643. }
  9644.  
  9645. return problemsMap;
  9646. }, {
  9647. maxRetries: 5,
  9648. retryInterval: 1000
  9649. });
  9650. }
  9651.  
  9652. /**
  9653. * 根据rating获取对应的颜色class名
  9654. * @param {number} rating 题目rating
  9655. * @returns {string} 颜色class名
  9656. */
  9657. function getClassNameByRating(rating) {
  9658. let className = "rating_by_clist_color9";
  9659. if (Number.isNaN(rating)) {
  9660. className = "rating_by_clist_colorNaN";
  9661. } else {
  9662. let keys = Object.keys(ratingClassMap);
  9663. for (let i = 0; i < keys.length; i++) {
  9664. if (rating < keys[i]) {
  9665. className = ratingClassMap[keys[i - 1]];
  9666. break;
  9667. }
  9668. }
  9669. }
  9670. return className;
  9671. }
  9672.  
  9673. /**
  9674. * problem题目页显示Rating
  9675. * @param {ProblemPageLinkbar} problemToolbar
  9676. * @returns {Promise<void>}
  9677. */
  9678. async function showRatingByClist_problem(problemToolbar) {
  9679. // 题目名
  9680. const problem = $('.header .title').eq(0).text().replace(/[\s\S]*?. /, '');
  9681. if (OJBetter.typeOfPage.is_acmsguru) problem = $('h4').eq(0).text().replace(/[\s\S]*?. /, '');
  9682.  
  9683. // 创建Rating按钮元素
  9684. creatRatingCss(false);
  9685. // TODO
  9686. const clistButton = problemToolbar.addLinkButton(
  9687. 'clistButton',
  9688. `https://clist.by/problems/?search=${problem}&resource=1`,
  9689. i18next.t('state.wait', { ns: 'button' }),
  9690. $("<img>").attr("src", "https://clist.by/static/img/logo-48.png"),
  9691. "15px"
  9692. );
  9693.  
  9694. // 检测clist连接
  9695. if (!await validateClistConnection()) {
  9696. problemToolbar.updateText(clistButton, i18next.t('state.netError', { ns: 'button' }));
  9697. return;
  9698. }
  9699.  
  9700. // 题目链接
  9701. let problem_url = window.location.href;
  9702. if (problem_url.includes('/contest/')) {
  9703. problem_url = problem_url.replace(/\/contest\/(\d+)\/problem\/(\w+)[^\w]*/, '/contest/$1/problem/$2');
  9704. } else {
  9705. problem_url = problem_url.replace(/\/problemset\/problem\/(\d+)\/(\w+)/, '/contest/$1/problem/$2');
  9706. }
  9707. if (OJBetter.typeOfPage.is_mSite) problem_url = problem_url.replace(/\/\/(\w+).codeforces.com/, '//codeforces.com'); // 轻量站
  9708.  
  9709. // 比赛名
  9710. // let contest = $('#sidebar').children().first().find('.rtable th').first().text();
  9711.  
  9712. // rating
  9713. problemToolbar.updateText(clistButton, i18next.t('state.loading', { ns: 'button' }));
  9714. let rating = await getRatingFromApi_problem(problem, problem_url);
  9715. if (rating) {
  9716. let className = getClassNameByRating(rating);
  9717. problemToolbar.updateText(clistButton, rating);
  9718. problemToolbar.setBold(clistButton);
  9719. problemToolbar.addClass(clistButton, className);
  9720. } else {
  9721. problemToolbar.updateText(clistButton, i18next.t('state.404', { ns: 'button' }));
  9722. problemToolbar.disableButton(clistButton);
  9723. }
  9724. }
  9725.  
  9726. /**
  9727. * contest页显示Rating
  9728. * @returns {Promise<void>}
  9729. */
  9730. async function showRatingByClist_contest() {
  9731. // 创建Rating显示框
  9732. creatRatingCss();
  9733. let ratingBadges = {};
  9734. $('.datatable .id.left').each(function () {
  9735. let href = 'https://codeforces.com' + $(this).find('a').attr('href');
  9736. let badge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge">${i18next.t('state.wait', { ns: 'button' })}</a>`);
  9737. $(this).find('a').after(badge);
  9738. ratingBadges[href] = badge;
  9739. });
  9740.  
  9741. // 检测clist连接
  9742. if (!await validateClistConnection()) {
  9743. for (let href in ratingBadges) {
  9744. ratingBadges[href].text('error').addClass('ratingBadge_error');
  9745. }
  9746. return;
  9747. }
  9748.  
  9749. // 显示loading
  9750. for (let href in ratingBadges) {
  9751. ratingBadges[href].text(i18next.t('state.loading', { ns: 'button' })).addClass('ratingBadge_loading');
  9752. }
  9753.  
  9754. // 获取Rating
  9755. let contestName = $('#sidebar').children().first().find('.rtable th').first().text();
  9756. let contestUrl = OJB_cleanLink(window.location.href);
  9757. try {
  9758. let problemsMap = await getRatingFromApi_contest(contestName, contestUrl);
  9759.  
  9760. // 填充数据
  9761. for (let href in ratingBadges) {
  9762. if (problemsMap.has(href)) {
  9763. let rating = problemsMap.get(href);
  9764. let className = getClassNameByRating(rating);
  9765. ratingBadges[href].text(rating).addClass(className);
  9766. } else {
  9767. ratingBadges[href].text(i18next.t('state.404', { ns: 'button' })).addClass('ratingBadge_no');
  9768. }
  9769. }
  9770. } catch (error) {
  9771. // 填充数据
  9772. for (let href in ratingBadges) {
  9773. ratingBadges[href].text(i18next.t('state.netError', { ns: 'button' })).addClass('ratingBadge_no');
  9774. }
  9775. console.warn(error);
  9776. }
  9777. }
  9778.  
  9779. /**
  9780. * problemset页显示Rating
  9781. * @returns {Promise<void>}
  9782. */
  9783. async function showRatingByClist_problemset() {
  9784. creatRatingCss();
  9785. let ratingBadges = [];
  9786. const $problems = $('.problems');
  9787. const $trs = $problems.find('tbody tr:gt(0)');
  9788.  
  9789. // 先创建Rating显示框,并将关系存进数组ratingBadges
  9790. for (let i = 0; i < $trs.length; i++) {
  9791. const $tds = $($trs[i]).find('td');
  9792. const $firstDiv = $($tds[1]).find('div:first');
  9793. let problem = $firstDiv.text();
  9794. let problem_url = $firstDiv.find('a').attr('href');
  9795. problem_url = problem_url.replace(/^\/problemset\/problem\/(\d+)\/(\w+)/, 'https://codeforces.com/contest/$1/problem/$2');
  9796.  
  9797. const ratingBadge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge"></a>`);
  9798. const rating = OJB_safeCreateJQElement(`<span class="rating">${i18next.t('state.wait', { ns: 'button' })}</span>`);
  9799. ratingBadge.append(rating);
  9800. $($tds[0]).find('a').after(ratingBadge);
  9801. ratingBadges.push({ ratingBadge, rating, problem, problem_url });
  9802. }
  9803.  
  9804. // 检测clist连接
  9805. if (!await validateClistConnection()) {
  9806. for (let i = 0; i < rating.length; i++) {
  9807. ratingBadges[i].rating.text(i18next.t('state.netError', { ns: 'button' }));
  9808. }
  9809. return;
  9810. }
  9811.  
  9812. // 每次只获取3个rating
  9813. for (let i = 0; i < ratingBadges.length; i += 3) {
  9814. const promises = [];
  9815. const endIndex = Math.min(i + 3, ratingBadges.length);
  9816.  
  9817. for (let j = i; j < endIndex; j++) {
  9818. const ratingBadge = ratingBadges[j];
  9819. // 显示请求中
  9820. ratingBadge.rating.text(i18next.t('state.loading', { ns: 'button' }));
  9821. promises.push(getRatingFromHTML(ratingBadge.problem, ratingBadge.problem_url).catch(error => console.warn(error)));
  9822. }
  9823.  
  9824. const results = await Promise.all(promises);
  9825.  
  9826. for (let j = i; j < endIndex; j++) {
  9827. const result = results[j - i];
  9828. const ratingBadge = ratingBadges[j];
  9829. if (result) {
  9830. let className = getClassNameByRating(result.rating);
  9831. ratingBadge.ratingBadge.addClass(className);
  9832. ratingBadge.rating.text(result.rating);
  9833. } else {
  9834. ratingBadge.rating.text(i18next.t('state.404', { ns: 'button' }));
  9835. }
  9836. }
  9837. }
  9838. }
  9839.  
  9840. /**
  9841. * cf赛制榜单重新着色
  9842. */
  9843. async function recolorStandings() {
  9844. function getColorValue(value) {
  9845. value = Math.max(0, Math.min(1, value));
  9846.  
  9847. const scale = chroma.scale(['#b71c1c', '#ff9800', '#ffc107', '#00aa00']).mode('lch').domain([0, 0.45, 0.7, 1]);
  9848. return scale(value).hex();
  9849. }
  9850. var maxScores = $('.standings tr:first th:nth-child(n+5)')
  9851. .map(function () {
  9852. return $(this).find('span').text();
  9853. })
  9854. .get();
  9855. $('.standings tr:not(:first):not(:last)').each(function () {
  9856. var thElements = $(this).find('td:nth-child(n+5)');
  9857. thElements.each(function (index) {
  9858. var spanElement = $(this).find('span:first');
  9859. var value = parseInt(spanElement.text());
  9860. if (value <= 0 || /[a-zA-Z]/.test(maxScores[index])) return;
  9861. var colorValue = getColorValue(value / maxScores[index]);
  9862. spanElement.css('color', colorValue);
  9863. });
  9864. });
  9865. }
  9866.  
  9867. /**
  9868. * 存放编辑器语言select的值与Monaco语言对应关系的map.
  9869. * @type {Object.<string, string>}
  9870. */
  9871. const value_monacoLanguageMap = {
  9872. "4": "pascal", "6": "php", "7": "python", "9": "csharp", "13": "perl", "20": "scala", "31": "python",
  9873. "32": "go", "34": "javascript", "36": "java", "40": "python", "41": "python", "43": "cpp",
  9874. "50": "cpp", "51": "pascal", "52": "cpp", "54": "cpp", "55": "javascript", "59": "cpp", "60": "java",
  9875. "61": "cpp", "65": "csharp", "67": "ruby", "70": "python", "73": "cpp", "74": "java", "75": "rust",
  9876. "77": "kotlin", "79": "csharp", "80": "cpp", "83": "kotlin", "87": "java"
  9877. };
  9878.  
  9879. /**
  9880. * 更新代码提交页的HTML
  9881. * @param {string} submitUrl 提交页面的URL
  9882. * @param {string} cacheKey 本地缓存的键名
  9883. * @returns {Promise<jQuery<HTMLElement>>} 返回 jQuery 包装的 HTML 元素
  9884. */
  9885. async function CloneOriginalHTML(submitUrl, cacheKey) {
  9886. return OJB_promiseRetryWrapper(async () => {
  9887. const response = await OJB_GMRequest({
  9888. method: 'GET',
  9889. url: submitUrl
  9890. });
  9891. const html = response.responseText;
  9892. const parser = new DOMParser();
  9893. const doc = parser.parseFromString(html, 'text/html');
  9894. const cloneHTML = $(doc.body).html();
  9895. localStorage.setItem(cacheKey, html);
  9896. return $(cloneHTML);
  9897. }, {
  9898. maxRetries: 5,
  9899. retryInterval: 1000,
  9900. errorHandler: (err) => {
  9901. console.error('A network error occurred while retrieving the HTML for the code submission page.', submitUrl);
  9902. }
  9903. });
  9904. }
  9905.  
  9906. /**
  9907. * 获取代码提交页的HTML元素
  9908. * @param {string} submitUrl
  9909. * @returns {Promise<jQuery>}
  9910. */
  9911. async function getSubmitHTML(submitUrl) {
  9912. const cacheKey = 'OJBetter_CloneOriginalHTML';
  9913. const cookieKey = 'OJBetter_CloneOriginalHTML_time';
  9914. if (OJB_getCookie(cookieKey) === '1') {
  9915. // 存在缓存
  9916. CloneOriginalHTML(submitUrl, cacheKey);
  9917. // 校验
  9918. let cloneHTML = $(localStorage.getItem(cacheKey));
  9919. if (cloneHTML.find('form.submit-form').length > 0) {
  9920. return cloneHTML;
  9921. } else {
  9922. // 存在错误,更新缓存
  9923. console.warn(`Cache error detected!\nattempting to update, cache destination submitUrl:\n${submitUrl}`);
  9924. return await CloneOriginalHTML(submitUrl, cacheKey);
  9925. }
  9926.  
  9927. } else {
  9928. // 没有缓存,更新
  9929. document.cookie = `${cookieKey}=1; path=/`;
  9930. return await CloneOriginalHTML(submitUrl, cacheKey);
  9931. }
  9932. }
  9933.  
  9934. // 代码自动保存
  9935. async function saveCode(url, code) {
  9936. try {
  9937. await OJBetter.common.database.editorCode.put({ url, code });
  9938. return 'Code saved successfully';
  9939. } catch (error) {
  9940. throw new Error('Failed to save code');
  9941. }
  9942. }
  9943.  
  9944. async function getCode(url) {
  9945. try {
  9946. const result = await OJBetter.common.database.editorCode.get(url);
  9947. return result ? result.code : null;
  9948. } catch (error) {
  9949. throw new Error('Failed to get code');
  9950. }
  9951. }
  9952.  
  9953. // 创建代码编辑调试表单元素
  9954. async function createCodeEditorForm(submitUrl, cloneHTML) {
  9955. // 表单
  9956. let formDiv = $('<form method="post" id="OJBetter_SubmitForm" class="input-output-copier"></form>');
  9957. $('.ttypography').after(formDiv);
  9958. formDiv.attr('action', submitUrl + "?csrf_token=" + OJBetter.common.cf_csrf_token);
  9959.  
  9960. // 顶部区域
  9961. let topDiv = OJB_safeCreateJQElement(`<div class="topDiv"></div>`);
  9962. let selectLang = cloneHTML.find('select[name="programTypeId"]'); // 语言选择
  9963. selectLang.css({ 'margin': '10px 0px' }).attr('id', 'programTypeId');
  9964. topDiv.append(selectLang);
  9965. let topRightDiv = OJB_safeCreateJQElement(`<div class="topRightDiv"></div>`);
  9966. topDiv.append(topRightDiv);
  9967. formDiv.append(topDiv);
  9968.  
  9969. // 问题选择/编号
  9970. let selectProblem = $('<input name="submittedProblemIndex" style="display:none;"></input>');
  9971. let problemCode;
  9972. if (OJBetter.typeOfPage.is_acmsguru) {
  9973. problemCode = $('h4').eq(0).text();
  9974. let matchResult = problemCode.match(/([A-Z0-9]+)/);
  9975. problemCode = matchResult[0];
  9976. } else if (OJBetter.typeOfPage.is_problemset_problem) {
  9977. let match = window.location.href.match(/\/problem\/([0-9]+?)\/([A-Z0-9]+?)(?!=[A-Z0-9])/);
  9978. problemCode = match[1] + match[2];
  9979. selectProblem.attr('name', 'submittedProblemCode');
  9980. } else {
  9981. problemCode = $('.header .title').eq(0).text();
  9982. let matchResult = problemCode.match(/([A-Z0-9]+)/);
  9983. problemCode = matchResult[0];
  9984. }
  9985. selectProblem.val(problemCode);
  9986. formDiv.append(selectProblem);
  9987.  
  9988. // 隐藏的代码记录
  9989. let sourceDiv = $('<textarea id="sourceCodeTextarea" name="source" style="display: none;"></textarea>');
  9990. formDiv.append(sourceDiv);
  9991.  
  9992. // 代码编辑器
  9993. let editorDiv = $('<div id="OJBetter_editor"></div>');
  9994. formDiv.append(editorDiv);
  9995.  
  9996. // monaco
  9997. let monaco = $('<div id="OJBetter_monaco"></div>');
  9998. editorDiv.append(monaco);
  9999.  
  10000. // 自定义调试
  10001. let customTestDiv = OJB_safeCreateJQElement(`
  10002. <details id="customTestBlock">
  10003. <summary >${i18next.t('customTestBlock.title', { ns: 'codeEditor' })}</summary>
  10004. <div id="customTests" style="min-height: 30px;"></div>
  10005. <div id="control" style="display:flex;">
  10006. <div style="display: flex;margin: 5px;">
  10007. <input type="checkbox" id="onlyCustomTest"}><label for="onlyCustomTest">
  10008. ${i18next.t('customTestBlock.onlyCustom', { ns: 'codeEditor' })}
  10009. </label>
  10010. </div>
  10011. <div style="display: flex;margin: 5px;">
  10012. <input type="checkbox" id="DontShowDiff"}>
  10013. <label for="DontShowDiff">
  10014. ${i18next.t('customTestBlock.DontShowDiff', { ns: 'codeEditor' })}
  10015. </label>
  10016. </div>
  10017. <button type="button" id="addCustomTest">${i18next.t('customTestBlock.add', { ns: 'codeEditor' })}</button>
  10018. </div>
  10019. </details>
  10020. `)
  10021. formDiv.append(customTestDiv);
  10022.  
  10023. // 调试/提交
  10024. let submitDiv = $('<div id="OJBetter_submitDiv"></div>');
  10025. let CompilerArgsInput = $('<input type="text" id="CompilerArgsInput">');
  10026. submitDiv.append(CompilerArgsInput);
  10027.  
  10028. let runButton = OJB_safeCreateJQElement(`
  10029. <button type="button" id="RunTestButton" class="ojb_btn ojb_btn_popover top">
  10030. <i class="iconfont">&#xe6c1;</i>
  10031. <span class="popover_content">${i18next.t('runTestButton.initial', { ns: 'codeEditor' })}</span>
  10032. </button>
  10033. `);
  10034. let submitButton = OJB_safeCreateJQElement(`
  10035. <button id="SubmitButton" class="ojb_btn ojb_btn_popover top" type="submit">
  10036. <i class="iconfont">&#xe633;</i>
  10037. <span class="popover_content">${i18next.t('submitButton', { ns: 'codeEditor' })}</span>
  10038. </button>
  10039. `);
  10040. if (OJBetter.monaco.setting.submitButtonPosition == "bottom") {
  10041. // 添加测试/提交按钮到底部
  10042. submitDiv.append(runButton);
  10043. submitDiv.append(submitButton);
  10044. }
  10045.  
  10046. formDiv.append(submitDiv);
  10047. let CompilerSetting = OJB_safeCreateJQElement(`
  10048. <div id="CompilerSetting"></div>
  10049. `);
  10050. formDiv.append(CompilerSetting);
  10051. let statePanel = OJB_safeCreateJQElement(`
  10052. <div id="statePanel"></div>
  10053. `);
  10054. formDiv.append(statePanel);
  10055.  
  10056. let from = {
  10057. formDiv: formDiv,
  10058. selectLang: selectLang,
  10059. topRightDiv: topRightDiv,
  10060. sourceDiv: sourceDiv,
  10061. editorDiv: editorDiv,
  10062. monaco: monaco,
  10063. runButton: runButton,
  10064. submitButton: submitButton,
  10065. submitDiv: submitDiv,
  10066. CompilerSetting: CompilerSetting,
  10067. statePanel: statePanel
  10068. };
  10069. return from;
  10070. }
  10071.  
  10072. // 解析ace格式的补全规则(acwing)
  10073. function parseAceCompleter(rules, range) {
  10074. const suggestions = [];
  10075. if (rules && rules.templates && rules.templates.items) {
  10076. const items = rules.templates.items;
  10077. for (let i = 0; i < items.length; i++) {
  10078. const item = items[i];
  10079. const parts = item.caption.split(' ');
  10080. for (let i = 0; i < parts.length; i++) {
  10081. if (item.value.startsWith(parts[i])) {
  10082. item.value = item.value.replace(parts[i], parts.slice(0, i + 1).join(' '));
  10083. break;
  10084. }
  10085. }
  10086. const completionItem = {
  10087. label: item.caption,
  10088. kind: monaco.languages.CompletionItemKind.Function,
  10089. insertText: item.value,
  10090. range: range
  10091. };
  10092. suggestions.push(completionItem);
  10093. }
  10094. }
  10095. return { suggestions };
  10096. }
  10097.  
  10098. // 解析monaco格式的补全规则
  10099. function parseMonacoCompleter(rules, range) {
  10100. const suggestions_ = [];
  10101. if (rules && rules.suggestions) {
  10102. const suggestion = rules.suggestions;
  10103. for (let i = 0; i < rules.suggestions.length; i++) {
  10104. const item = suggestion[i];
  10105. const completionItem = {
  10106. ...item,
  10107. range: range
  10108. };
  10109. suggestions_.push(completionItem);
  10110. }
  10111. }
  10112. return { suggestions: suggestions_ };
  10113. }
  10114.  
  10115. /**
  10116. * 创建monaco编辑器的一个实例
  10117. */
  10118. async function createMonacoEditor(language, form, support) {
  10119. // 判断monacoLoader是否加载完毕
  10120. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  10121.  
  10122. /**
  10123. * 通用参数
  10124. */
  10125. var id = 0; // 协议中的id标识
  10126. var workspace = language + "_workspace";
  10127. var rootUri = OJBetter.monaco.lsp.workUri + "/" + workspace;
  10128. // 文件名
  10129. var InstanceID = OJB_getRandomNumber(8).toString();
  10130. var filename = language == "java" ? "hello/src/" + InstanceID : InstanceID;
  10131. // 后缀名
  10132. var fileExtension =
  10133. language === "cpp"
  10134. ? ".cpp"
  10135. : language === "python"
  10136. ? ".py"
  10137. : language === "java"
  10138. ? ".java"
  10139. : "";
  10140. var uri = rootUri + "/" + filename + fileExtension;
  10141. var initialized = false; // 是否已初始化
  10142. var serverInfo; // 服务器返回的支持信息
  10143. var model; // model
  10144. var OJBetter_monaco = {};
  10145. window.OJBetter_monaco = OJBetter_monaco; // 全局方法
  10146.  
  10147. /**
  10148. * 一些工具函数
  10149. */
  10150. // 将lsp格式的rang转换为Monaco格式
  10151. OJBetter_monaco.lspRangeToMonacoRange = function (range) {
  10152. const { start, end } = range;
  10153. return new monaco.Range(
  10154. start.line + 1,
  10155. start.character + 1,
  10156. end.line + 1,
  10157. end.character + 1
  10158. );
  10159. };
  10160. // 将Monaco格式的rang转为lsp格式
  10161. OJBetter_monaco.MonacoRangeTolspRange = function (range) {
  10162. return {
  10163. start: {
  10164. line: range.startLineNumber - 1,
  10165. character: range.startColumn - 1,
  10166. },
  10167. end: {
  10168. line: range.endLineNumber - 1,
  10169. character: range.endColumn - 1,
  10170. },
  10171. };
  10172. };
  10173. // 将Monaco格式的position转为lsp格式的
  10174. OJBetter_monaco.MonacoPositionTolspPosition = function (position) {
  10175. return {
  10176. line: position.lineNumber - 1,
  10177. character: position.column - 1,
  10178. };
  10179. };
  10180. // 将Monaco格式的severity转为lsp格式的
  10181. OJBetter_monaco.MonacoSeverityTolspSeverity = function (severity) {
  10182. switch (severity) {
  10183. case 8:
  10184. return 1;
  10185. case 1:
  10186. return 4;
  10187. case 2:
  10188. return 3;
  10189. case 4:
  10190. return 2;
  10191. default:
  10192. return severity;
  10193. }
  10194. };
  10195. // 将lsp格式的severity转为Monaco格式的
  10196. OJBetter_monaco.lspSeverityToMonacoSeverity = function (severity) {
  10197. switch (severity) {
  10198. case 1:
  10199. return 8;
  10200. case 4:
  10201. return 1;
  10202. case 3:
  10203. return 2;
  10204. case 2:
  10205. return 4;
  10206. default:
  10207. return severity;
  10208. }
  10209. };
  10210. // 收集Monaco数据中的rang数据
  10211. OJBetter_monaco.CollectRange = function (item) {
  10212. return {
  10213. startLineNumber: item.startLineNumber,
  10214. startColumn: item.startColumn,
  10215. endLineNumber: item.endLineNumber,
  10216. endColumn: item.endColumn,
  10217. };
  10218. };
  10219. // 收集Monaco position数据中的rang数据
  10220. OJBetter_monaco.CollectRangeByPosition = function (item) {
  10221. var word = model.getWordUntilPosition(item);
  10222. return {
  10223. startLineNumber: item.lineNumber,
  10224. endLineNumber: item.lineNumber,
  10225. startColumn: word.startColumn,
  10226. endColumn: word.endColumn,
  10227. };
  10228. };
  10229. // 将lsp格式的Edit转换为Monaco格式
  10230. OJBetter_monaco.lspEditToMonacoEdit = function (edit) {
  10231. const edits = [];
  10232.  
  10233. if (language == "python") {
  10234. for (const item1 of edit.documentChanges) {
  10235. for (const item2 of item1.edits) {
  10236. const newElement = {
  10237. textEdit: {
  10238. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10239. text: item2.newText,
  10240. },
  10241. resource: monaco.Uri.parse(item1.textDocument.uri),
  10242. versionId: model.getVersionId(),
  10243. };
  10244. edits.push(newElement);
  10245. }
  10246. }
  10247. } else if (language == "java") {
  10248. for (const item1 in edit.changes) {
  10249. edit.changes[item1].forEach((item2) => {
  10250. const newElement = {
  10251. textEdit: {
  10252. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10253. text: item2.newText,
  10254. },
  10255. resource: uri,
  10256. versionId: model.getVersionId(),
  10257. };
  10258. edits.push(newElement);
  10259. });
  10260. }
  10261. } else {
  10262. for (const key in edit.changes) {
  10263. const arr = edit.changes[key];
  10264. for (const item of arr) {
  10265. const newElement = {
  10266. textEdit: {
  10267. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  10268. text: item.newText,
  10269. },
  10270. resource: monaco.Uri.parse(key),
  10271. versionId: model.getVersionId(),
  10272. };
  10273. edits.push(newElement);
  10274. }
  10275. }
  10276. }
  10277. return { edits: edits };
  10278. };
  10279.  
  10280. /**
  10281. * 实例化一个editor
  10282. */
  10283. uri = monaco.Uri.file(uri);
  10284. model = monaco.editor.createModel('', language, uri);
  10285. OJBetter.monaco.editor = monaco.editor.create(document.getElementById("OJBetter_monaco"), {
  10286. model: model,
  10287. rootUri: rootUri,
  10288. fontSize: 15,
  10289. tabSize: 4,
  10290. theme: OJBetter.basic.darkMode == "dark" ? "vs-dark" : "vs",
  10291. bracketPairColorization: {
  10292. enabled: true,
  10293. independentColorPoolPerBracketType: true,
  10294. },
  10295. automaticLayout: true,
  10296. lineNumbersMinChars: 3,
  10297. matchOnWordStartOnly: false,
  10298. wordWrap: "on",
  10299. wrappingIndent: "same",
  10300. glyphMargin: true,
  10301. formatOnType: true,
  10302. scrollbar: {
  10303. verticalScrollbarSize: 10,
  10304. horizontalScrollbarSize: 10,
  10305. alwaysConsumeMouseWheel: OJBetter.monaco.setting.alwaysConsumeMouseWheel
  10306. },
  10307. suggest: {
  10308. selectionMode: 'never' // 代码建议不自动选择
  10309. }
  10310. });
  10311.  
  10312. /**
  10313. * 添加快捷功能
  10314. */
  10315. (OJBetter_monaco.addShortCuts = async () => {
  10316. // 从配置信息更新字体大小
  10317. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(OJBetter.monaco.setting.fontsize) });
  10318.  
  10319. // 调整字体大小
  10320. let changeSize = OJB_safeCreateJQElement(`
  10321. <div class="ojb_btn ojb_btn_popover top">
  10322. <input type="number" id="fontSizeInput" value="${OJBetter.monaco.setting.fontsize}">
  10323. <span class="popover_content">${i18next.t('fontSizeInput', { ns: 'codeEditor' })}</span>
  10324. </div>`)
  10325. form.topRightDiv.append(changeSize);
  10326. changeSize.find('input#fontSizeInput').on('input', function () {
  10327. var size = $(this).val();
  10328. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(size) });
  10329. GM_setValue('editorFontSize', size);
  10330. });
  10331.  
  10332. // 全屏按钮
  10333. let fullscreenButton = OJB_safeCreateJQElement(`
  10334. <button type="button" class="ojb_btn ojb_btn_popover top">
  10335. <i class="iconfont">&#xe606;</i>
  10336. <span class="popover_content">${i18next.t('fullscreenButton', { ns: 'codeEditor' })}</span>
  10337. </button>
  10338. `);
  10339. form.topRightDiv.append(fullscreenButton);
  10340. fullscreenButton.on('click', enterFullscreen);
  10341.  
  10342. // 固定到底部按钮
  10343. let fixToBottomButton = OJB_safeCreateJQElement(`
  10344. <button type="button" class="ojb_btn ojb_btn_popover top">
  10345. <i class="iconfont">&#xe607;</i>
  10346. <span class="popover_content">${i18next.t('fixToBottomButton', { ns: 'codeEditor' })}</span>
  10347. </button>
  10348. `);
  10349. form.topRightDiv.append(fixToBottomButton);
  10350. fixToBottomButton.on('click', fixToBottom);
  10351.  
  10352. // 固定到右侧按钮
  10353. let fixToRightButton = OJB_safeCreateJQElement(`
  10354. <button type="button" class="ojb_btn ojb_btn_popover top">
  10355. <i class="iconfont">&#xe605;</i>
  10356. <span class="popover_content">${i18next.t('fixToRightButton', { ns: 'codeEditor' })}</span>
  10357. </button>
  10358. `);
  10359. form.topRightDiv.append(fixToRightButton);
  10360. fixToRightButton.on('click', fixToRight);
  10361.  
  10362. // 添加测试/提交按钮到顶部
  10363. if (OJBetter.monaco.setting.submitButtonPosition == "top") {
  10364. form.topRightDiv.append(form.runButton);
  10365. form.topRightDiv.append(form.submitButton);
  10366. }
  10367.  
  10368. // 选择记忆
  10369. if (!OJBetter.monaco.setting.position_initialized) {
  10370. OJBetter.monaco.setting.position_initialized = true; // 标记是否已经初始化过
  10371. if (OJBetter.monaco.setting.position == "full") {
  10372. fullscreenButton.click();
  10373. } else if (OJBetter.monaco.setting.position == "bottom") {
  10374. fixToBottomButton.click();
  10375. } else if (OJBetter.monaco.setting.position == "right") {
  10376. fixToRightButton.click();
  10377. }
  10378. }
  10379.  
  10380. // 禁用按钮
  10381. function disableButtons() {
  10382. fullscreenButton.prop("disabled", true);
  10383. fixToBottomButton.prop("disabled", true);
  10384. fixToRightButton.prop("disabled", true);
  10385. }
  10386.  
  10387. // 启用按钮
  10388. function enableButtons() {
  10389. fullscreenButton.prop("disabled", false);
  10390. fixToBottomButton.prop("disabled", false);
  10391. fixToRightButton.prop("disabled", false);
  10392. }
  10393.  
  10394. // 进入全屏
  10395. function enterFullscreen() {
  10396. let editor = $('#OJBetter_editor');
  10397. editor.addClass('fullscreen');
  10398.  
  10399. // 取消按钮
  10400. let cancelButton = OJB_safeCreateJQElement(`
  10401. <button type="button" class="ojb_btn ojb_btn_popover top primary exit_button_bottom">
  10402. <i class="iconfont">&#xe60b;</i>
  10403. <span class="popover_content">${i18next.t('exitFullscreenButton', { ns: 'codeEditor' })}</span>
  10404. </button>
  10405. `).on('click', () => exitFullscreen(cancelButton));
  10406. $('body').append(cancelButton);
  10407.  
  10408. disableButtons();
  10409. GM_setValue("monacoEditor_position", "full");
  10410. }
  10411.  
  10412. // 退出全屏
  10413. const exitFullscreen = (cancelButton) => {
  10414. let editor = $('#OJBetter_editor');
  10415. editor.removeClass('fullscreen');
  10416. cancelButton.remove();
  10417. enableButtons();
  10418. GM_setValue("monacoEditor_position", "initial");
  10419. };
  10420.  
  10421. // 固定到底部
  10422. function fixToBottom() {
  10423. let editor = $('#OJBetter_editor');
  10424. editor.addClass('bottom');
  10425.  
  10426. let halfHeight = $(window).height() * 0.5;
  10427. let blankSpace = $('<div>', {
  10428. 'class': 'blank-space',
  10429. 'style': 'height: ' + (halfHeight + 30) + 'px;'
  10430. });
  10431. $('body').append(blankSpace);
  10432.  
  10433. let cancelButton = OJB_safeCreateJQElement(`
  10434. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10435. <i class="iconfont">&#xe625;</i>
  10436. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10437. </button>
  10438. `).on('click', () => cancelFixingToBottom(cancelButton, blankSpace));
  10439. $('body').append(cancelButton);
  10440.  
  10441. disableButtons();
  10442. GM_setValue("monacoEditor_position", "bottom");
  10443. }
  10444.  
  10445. // 取消固定到底部
  10446. const cancelFixingToBottom = (cancelButton, blankSpace) => {
  10447. let editor = $('#OJBetter_editor');
  10448. editor.removeClass('bottom');
  10449. cancelButton.remove();
  10450. blankSpace.remove();
  10451. enableButtons();
  10452. GM_setValue("monacoEditor_position", "initial");
  10453. };
  10454.  
  10455. // 固定到右侧边栏
  10456. function fixToRight() {
  10457. const sidebar = $('#sidebar').hide();
  10458.  
  10459. // 添加样式
  10460. const styleElement = GM_addStyle(`
  10461. #body {
  10462. min-width: 50vw;
  10463. max-width: 50vw;
  10464. max-height: 100vh;
  10465. overflow-x: hidden;
  10466. overflow-y: auto;
  10467. padding: 1rem;
  10468. box-sizing: border-box;
  10469. }
  10470. body {
  10471. margin: 0px;
  10472. }
  10473. .content-with-sidebar {
  10474. margin-right: 0px !important;
  10475. }
  10476. .menu-list li {
  10477. margin-right: 0.5em;
  10478. }
  10479. .menu-list li a {
  10480. font-size: 1.4rem;
  10481. }
  10482. #OJBetter_editor{
  10483. height: 100vh;
  10484. width: 50vw;
  10485. }
  10486. `);
  10487.  
  10488. // 包装一层div
  10489. $('#body').wrap('<div id="right-side-wrapper" style="display:flex; max-width: 100vw; overflow: hidden;"></div>');
  10490. const blankSpace = $('<div>').appendTo('#right-side-wrapper');
  10491.  
  10492. // 移动编辑器
  10493. const editor = $('#OJBetter_editor').prependTo(blankSpace).addClass('right-side');
  10494.  
  10495. // 取消按钮
  10496. const cancelButton = OJB_safeCreateJQElement(`
  10497. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10498. <i class="iconfont">&#xe625;</i>
  10499. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10500. </button>
  10501. `).on('click', () => cancelFixingToRight(sidebar, styleElement, editor, cancelButton)).appendTo('body');
  10502.  
  10503. disableButtons();
  10504. GM_setValue("monacoEditor_position", "right");
  10505.  
  10506. // 补丁:修复固定到右侧导致的样例元素.sample-test相关代码重复执行的问题(具体原因未查)
  10507. $('.sample-test').find('.title').each((i, e) => {
  10508. if ($(e).find('.input-output-copier').length > 1) {
  10509. $(e).find('.input-output-copier').first().remove();
  10510. }
  10511. });
  10512. darkModeStyleAdjustment();
  10513. }
  10514.  
  10515. const cancelFixingToRight = (sidebar, styleElement, editor, cancelButton) => {
  10516. sidebar.show();
  10517. // 移回来
  10518. editor.insertAfter(form.sourceDiv).removeClass('right-side');
  10519.  
  10520. // 移除包装
  10521. $('#body').unwrap();
  10522. cancelButton.remove();
  10523. styleElement.remove(); // 移除添加的样式
  10524.  
  10525. enableButtons();
  10526. GM_setValue("monacoEditor_position", "initial");
  10527. }
  10528.  
  10529. // 代码同步与保存
  10530. var nowUrl = window.location.href;
  10531. nowUrl = nowUrl.replace(/#/, ""); // 当页面存在更改时url会多出一个#,去掉
  10532. const code = await getCode(nowUrl);
  10533. if (code) {
  10534. OJBetter.monaco.editor.setValue(code); // 恢复代码
  10535. form.sourceDiv.val(code);
  10536. }
  10537. OJBetter.monaco.editor.onDidChangeModelContent(async () => {
  10538. // 将monaco editor的内容同步到sourceDiv
  10539. const code = OJBetter.monaco.editor.getValue();
  10540. form.sourceDiv.val(code);
  10541. await saveCode(nowUrl, code);
  10542. });
  10543. })();
  10544.  
  10545. /**
  10546. * 注册本地自动补全
  10547. */
  10548. (OJBetter_monaco.RegisterLocalComplet = async () => {
  10549. // 补全器注册函数
  10550. function registMyCompletionItemProvider(language, genre, rule) {
  10551. if (genre == "monaco") {
  10552. monaco.languages.registerCompletionItemProvider(language, {
  10553. provideCompletionItems: function (model, position) {
  10554. return parseMonacoCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10555. }
  10556. })
  10557. } else if (genre == "ace") {
  10558. monaco.languages.registerCompletionItemProvider(language, {
  10559. provideCompletionItems: function (model, position) {
  10560. return parseAceCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10561. }
  10562. })
  10563. }
  10564. }
  10565.  
  10566. // 注册acwing cpp 模板
  10567. if (language == "cpp" && OJBetter.monaco.complet.cppCodeTemplate) {
  10568. try {
  10569. var acwing_cpp_code_completer = JSON.parse(GM_getResourceText("acwing_cpp_code_completer"));
  10570. registMyCompletionItemProvider('cpp', 'ace', acwing_cpp_code_completer);
  10571. } catch (error) {
  10572. console.error("Error registering acwing cpp template:", error);
  10573. }
  10574. }
  10575.  
  10576. // 注册自定义的补全
  10577. let complet_length = OJBetter.monaco.complet.customConfig.configurations.length;
  10578. if (complet_length > 0) {
  10579. for (let i = 0; i < complet_length; i++) {
  10580. let item = OJBetter.monaco.complet.customConfig.configurations[i];
  10581. if (item.isChoose && item.language == language) {
  10582. try {
  10583. let rule = await OJB_getExternalJSON(item.jsonUrl);
  10584. registMyCompletionItemProvider(item.language, item.genre, rule);
  10585. } catch (error) {
  10586. console.error(`Error registering custom completer for ${item.language}:`, error);
  10587. }
  10588. }
  10589. }
  10590. }
  10591. })();
  10592.  
  10593. if (!support || !OJBetter.monaco.lsp.enabled) { return; } // 如果不支持lsp,则到此为止
  10594.  
  10595. /**
  10596. * LSP连接状态指示
  10597. */
  10598. const lspStateButton = OJB_safeCreateJQElement(`
  10599. <div id="lspStateDiv" class="ojb_btn ojb_btn_popover top loading">
  10600. <i class="iconfont">&#xe658;</i>
  10601. <span class="popover_content">${i18next.t('lsp.connect', { ns: 'codeEditor' })}</span>
  10602. </div>
  10603. `).on('click', () => {
  10604. OJB_showModal(LSPLogDiv);
  10605. LSPLogDiv.show();
  10606. });
  10607. form.topRightDiv.prepend(lspStateButton);
  10608.  
  10609. const LSPLogDiv = OJB_safeCreateJQElement(`
  10610. <dialog id="LSPLog" style="display: none;">
  10611. <button class="ojb_btn">${i18next.t('close', { ns: 'common' })}</button>
  10612. <div id="LSPLogList" style="overflow: auto;"></div>
  10613. <dialog>`);
  10614. $('body').append(LSPLogDiv);
  10615.  
  10616. const LSPLogList = $('<ul></ul>');
  10617. $('#LSPLogList').append(LSPLogList);
  10618.  
  10619. const closeButton = LSPLogDiv.find('button');
  10620. closeButton.on('click', function () {
  10621. OJB_closeModal(LSPLogDiv);
  10622. });
  10623.  
  10624. /**
  10625. * 推送新的消息到LSP日志中
  10626. * @param {'error' | 'warn' | 'info'} status
  10627. * @param {string} msg
  10628. * @param {boolean} data
  10629. */
  10630. function pushLSPLogMessage(status, msg, data) {
  10631. var li = $('<li>').text('[' + new Date().toLocaleString() + '] ' + msg);
  10632. if (status === 'error') {
  10633. li.attr('style', 'color: #f44336;');
  10634. } else if (status === 'warn') {
  10635. li.attr('style', 'color: #ff9800;');
  10636. } else if (status === 'info') {
  10637. li.attr('style', 'color: #616161;');
  10638. }
  10639. if (data) {
  10640. var jsonText = JSON.stringify(data, null, 2);
  10641. var details = $('<details>');
  10642. var summary = $('<summary>').text('Data');
  10643. var pre = $('<pre>').text(jsonText);
  10644. details.append(summary, pre);
  10645. li.append(details);
  10646. }
  10647. LSPLogList.append(li);
  10648. }
  10649.  
  10650. /**
  10651. * 添加状态底栏
  10652. */
  10653. var statusBar = $('<div id="OJBetter_statusBar">');
  10654. form.editorDiv.append(statusBar);
  10655.  
  10656. /**
  10657. * languageSocket
  10658. */
  10659. var url = OJBetter.monaco.lsp.socketUrl;
  10660. var languageSocket = new WebSocket(url + language);
  10661. OJBetter.monaco.lsp.socket.push(languageSocket);
  10662. var languageSocketState = false;
  10663. var responseHandlers = new Map(); // 映射表,需要等待返回数据的请求 -> 对应的事件触发函数
  10664.  
  10665. languageSocket.onopen = () => {
  10666. languageSocketState = true;
  10667. lspStateButton.setButtonPopover(i18next.t('lsp.waitingAnswer', { ns: 'codeEditor' }));
  10668. pushLSPLogMessage("info", `languageSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10669. };
  10670. languageSocket.onmessage = (event) => {
  10671. const message = JSON.parse(event.data);
  10672. if (message.id === 0 && message.result) {
  10673. // 初始化完成
  10674. lspStateButton.setButtonState('success', i18next.t('lsp.connected', { ns: 'codeEditor' }));
  10675. pushLSPLogMessage("info", `Initialization ${i18next.t('lsp.state.finished', { ns: 'logMessage' })}`);
  10676. serverInfo = message.result; // 存下服务器支持信息
  10677. OJBetter_monaco.openDocRequest(); // 打开文档
  10678. if (!OJBetter.monaco.setting.language.includes(language)) {
  10679. OJBetter.monaco.setting.language.push(language);
  10680. OJBetter_monaco.RegistrationAfterInit(); // 注册语言及功能
  10681. } else {
  10682. location.reload(); // 这里有问题,先贴个补丁
  10683. }
  10684. OJBetter_monaco.PassiveReceiveHandler(); // 注册被动接收函数
  10685. } else if (message.id === 0 && message.error) {
  10686. pushLSPLogMessage("warn", `Initialization ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`);
  10687. } else if (message.id !== undefined && responseHandlers.has(message.id)) {
  10688. // 如果收到带有id字段的消息,则回传给对应的事件触发函数
  10689. const handler = responseHandlers.get(message.id);
  10690. if (handler) {
  10691. handler(message);
  10692. responseHandlers.delete(message.id); // 删除已处理的事件触发函数
  10693. }
  10694. } else if (message.method == "textDocument/publishDiagnostics") {
  10695. // 接收代码诊断推送
  10696. OJBetter_monaco.updateMarkers(message);
  10697. } else if (message.method == "workspace/applyEdit") {
  10698. // 应用服务器推送的更改
  10699. OJBetter_monaco.applyEdit(message);
  10700. }
  10701. };
  10702. languageSocket.onerror = (error) => {
  10703. pushLSPLogMessage("error", `languageSocket ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`, error);
  10704. console.warn(`Error connecting to languageSocket: ${error}`)
  10705. };
  10706. languageSocket.onclose = (event) => {
  10707. languageSocketState = false;
  10708. lspStateButton.setButtonState('error', i18next.t('lsp.error', { ns: 'codeEditor' }));
  10709. pushLSPLogMessage("warn", `languageSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`);
  10710. };
  10711.  
  10712. /**
  10713. * 等待LanguageSocketState
  10714. */
  10715. async function waitForLanguageSocketState() {
  10716. return new Promise((resolve) => {
  10717. const checkInitialized = () => {
  10718. if (languageSocketState) {
  10719. resolve();
  10720. } else {
  10721. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10722. }
  10723. };
  10724. checkInitialized();
  10725. });
  10726. }
  10727.  
  10728. // 等待lsp响应初始化结果
  10729. async function waitForInitialized() {
  10730. return new Promise((resolve) => {
  10731. const checkInitialized = () => {
  10732. if (initialized) {
  10733. resolve();
  10734. } else {
  10735. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10736. }
  10737. };
  10738. checkInitialized();
  10739. });
  10740. }
  10741.  
  10742. /**
  10743. * 与languageSocket通信的包装方法
  10744. */
  10745. async function sendMessage(data, requiresResponse, callback) {
  10746. if (!initialized) {
  10747. await waitForInitialized(); // 等待initialized为真
  10748. }
  10749. if (requiresResponse) {
  10750. responseHandlers.set(data.id, callback) // 将事件触发函数与id关联起来
  10751. }
  10752. if (!languageSocketState) await waitForLanguageSocketState();
  10753. languageSocket.send(JSON.stringify(data));
  10754. }
  10755. // 发送消息并等待返回结果
  10756. function fetchData(params, callback) {
  10757. sendMessage(params, true, callback);
  10758. }
  10759. // 发送消息,不需要等待返回结果
  10760. function sendData(data) {
  10761. sendMessage(data, false);
  10762. }
  10763.  
  10764. /**
  10765. * 代码文件更新fileWebSocket
  10766. */
  10767. var fileWebSocket = new WebSocket(url + "file");
  10768. var fileWebSocketState = false;
  10769. OJBetter.monaco.lsp.socket.push(fileWebSocket);
  10770. fileWebSocket.onopen = () => {
  10771. fileWebSocketState = true;
  10772. pushLSPLogMessage("info", `fileWebSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10773. };
  10774. fileWebSocket.onclose = (ev) => {
  10775. fileWebSocketState = false;
  10776. pushLSPLogMessage("warn", `fileWebSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`, ev);
  10777. };
  10778. fileWebSocket.onmessage = (ev) => {
  10779. let message = JSON.parse(ev.data);
  10780. if (message.result !== "ok")
  10781. pushLSPLogMessage("error", `update file failed: ${ev}`);
  10782. };
  10783. fileWebSocket.onerror = (error) => {
  10784. console.warn(`Error connecting to fileWebSocket: ${error}`);
  10785. };
  10786. async function updateFile(workspace, filename, fileExtension, code) {
  10787. async function waitForfileWebSocketState() {
  10788. return new Promise((resolve) => {
  10789. const checkInitialized = () => {
  10790. if (fileWebSocketState) {
  10791. resolve();
  10792. } else {
  10793. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10794. }
  10795. };
  10796. checkInitialized();
  10797. });
  10798. }
  10799. if (!fileWebSocketState) await waitForfileWebSocketState();
  10800. fileWebSocket.send(
  10801. JSON.stringify({
  10802. type: "update",
  10803. workspace,
  10804. filename,
  10805. fileExtension,
  10806. code,
  10807. })
  10808. );
  10809. }
  10810.  
  10811. /**
  10812. * 发送初始化请求
  10813. */
  10814. OJBetter_monaco.Initialize = () => {
  10815. //初始化initialize
  10816. const capabilities = {
  10817. workspace: {
  10818. applyEdit: true,
  10819. },
  10820. textDocument: {
  10821. publishDiagnostics: {
  10822. relatedInformation: true,
  10823. versionSupport: false,
  10824. tagSupport: {
  10825. valueSet: [1, 2],
  10826. },
  10827. codeDescriptionSupport: true,
  10828. },
  10829. completion: {
  10830. contextSupport: true,
  10831. completionItem: {
  10832. snippetSupport: true,
  10833. commitCharactersSupport: true,
  10834. documentationFormat: ["markdown", "plaintext"],
  10835. deprecatedSupport: true,
  10836. preselectSupport: true,
  10837. tagSupport: {
  10838. valueSet: [1],
  10839. },
  10840. insertReplaceSupport: true,
  10841. resolveSupport: {
  10842. properties: [
  10843. "documentation",
  10844. "detail",
  10845. "additionalTextEdits",
  10846. ],
  10847. },
  10848. insertTextModeSupport: {
  10849. valueSet: [1, 2],
  10850. },
  10851. },
  10852. },
  10853. hover: {
  10854. dynamicRegistration: true,
  10855. contentFormat: ["markdown", "plaintext"],
  10856. },
  10857. signatureHelp: {
  10858. signatureInformation: {
  10859. documentationFormat: ["markdown", "plaintext"],
  10860. parameterInformation: {
  10861. labelOffsetSupport: true,
  10862. },
  10863. activeParameterSupport: true,
  10864. },
  10865. contextSupport: true,
  10866. },
  10867. definition: {
  10868. dynamicRegistration: true,
  10869. linkSupport: true,
  10870. },
  10871. references: {
  10872. dynamicRegistration: true,
  10873. },
  10874. documentHighlight: {
  10875. dynamicRegistration: true,
  10876. },
  10877. codeAction: {
  10878. codeActionLiteralSupport: {
  10879. codeActionKind: {
  10880. valueSet:
  10881. language == "java"
  10882. ? []
  10883. : [
  10884. "",
  10885. "quickfix",
  10886. "refactor",
  10887. "refactor.extract",
  10888. "refactor.inline",
  10889. "refactor.rewrite",
  10890. "source",
  10891. "source.organizeImports",
  10892. ],
  10893. },
  10894. },
  10895. },
  10896. rename: {
  10897. dynamicRegistration: true,
  10898. prepareSupport: true,
  10899. prepareSupportDefaultBehavior: 1,
  10900. honorsChangeAnnotations: true,
  10901. },
  10902. documentLink: {
  10903. tooltipSupport: true,
  10904. },
  10905. typeDefinition: {
  10906. dynamicRegistration: true,
  10907. linkSupport: true,
  10908. },
  10909. implementation: {
  10910. dynamicRegistration: true,
  10911. linkSupport: true,
  10912. },
  10913. colorProvider: {
  10914. dynamicRegistration: true,
  10915. },
  10916. foldingRange: {
  10917. dynamicRegistration: true,
  10918. rangeLimit: 5000,
  10919. lineFoldingOnly: true,
  10920. },
  10921. declaration: {
  10922. dynamicRegistration: true,
  10923. linkSupport: true,
  10924. },
  10925. semanticTokens: {
  10926. dynamicRegistration: true,
  10927. tokenTypes: [
  10928. "namespace",
  10929. "type",
  10930. "class",
  10931. "enum",
  10932. "interface",
  10933. "struct",
  10934. "typeParameter",
  10935. "parameter",
  10936. "variable",
  10937. "property",
  10938. "enumMember",
  10939. "event",
  10940. "function",
  10941. "method",
  10942. "macro",
  10943. "keyword",
  10944. "modifier",
  10945. "comment",
  10946. "string",
  10947. "number",
  10948. "regexp",
  10949. "operator",
  10950. ],
  10951. tokenModifiers: [
  10952. "declaration",
  10953. "definition",
  10954. "readonly",
  10955. "static",
  10956. "deprecated",
  10957. "abstract",
  10958. "async",
  10959. "modification",
  10960. "documentation",
  10961. "defaultLibrary",
  10962. ],
  10963. formats: ["relative"],
  10964. requests: {
  10965. range: true,
  10966. full: {
  10967. delta: true,
  10968. },
  10969. },
  10970. multilineTokenSupport: false,
  10971. overlappingTokenSupport: false,
  10972. },
  10973. callHierarchy: {
  10974. dynamicRegistration: true,
  10975. },
  10976. },
  10977. window: {
  10978. showMessage: {
  10979. messageActionItem: {
  10980. additionalPropertiesSupport: true,
  10981. },
  10982. },
  10983. showDocument: {
  10984. support: true,
  10985. },
  10986. workDoneProgress: true,
  10987. },
  10988. general: {
  10989. regularExpressions: {
  10990. engine: "ECMAScript",
  10991. version: "ES2020",
  10992. },
  10993. markdown: {
  10994. parser: "marked",
  10995. version: "1.1.0",
  10996. },
  10997. },
  10998. };
  10999.  
  11000. const initializeRequest = {
  11001. id: id++,
  11002. jsonrpc: "2.0",
  11003. method: "initialize",
  11004. params: {
  11005. processId: null,
  11006. clientInfo: {
  11007. name: "CFMonaco" + InstanceID,
  11008. },
  11009. locale: "zh-CN",
  11010. rootPath: null,
  11011. rootUri: null,
  11012. capabilities: capabilities,
  11013. trace: "off",
  11014. workspaceFolders: [
  11015. {
  11016. uri:
  11017. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  11018. name:
  11019. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  11020. },
  11021. ],
  11022. },
  11023. };
  11024. languageSocket.send(JSON.stringify(initializeRequest));
  11025.  
  11026. // 打开文档函数
  11027. OJBetter_monaco.openDocRequest = function () {
  11028. const initializ = {
  11029. jsonrpc: "2.0",
  11030. method: "initialized",
  11031. params: {},
  11032. };
  11033. languageSocket.send(JSON.stringify(initializ));
  11034. const openDocRequest = {
  11035. jsonrpc: "2.0",
  11036. method: "textDocument/didOpen",
  11037. params: {
  11038. textDocument: {
  11039. uri: model.uri.toString(),
  11040. languageId: language,
  11041. version: model.getVersionId(),
  11042. text: model.getValue(),
  11043. },
  11044. },
  11045. };
  11046. languageSocket.send(JSON.stringify(openDocRequest));
  11047. initialized = true; // 初始化完成,这里确认逻辑待完善
  11048. };
  11049.  
  11050. // 初始化更新文件
  11051. updateFile(workspace, filename, fileExtension, model.getValue());
  11052. }
  11053.  
  11054. /**
  11055. * 注册语言及功能
  11056. */
  11057. OJBetter_monaco.RegistrationAfterInit = () => {
  11058. // 注册语言
  11059. monaco.languages.register({ id: language });
  11060.  
  11061. // 注册"Command"
  11062. (function registerCommand() {
  11063. serverInfo.capabilities.executeCommandProvider.commands.forEach(
  11064. (item) => {
  11065. pushLSPLogMessage("info", `${i18next.t('lsp.server.regist', { ns: 'logMessage' })}`, item);
  11066. monaco.editor.registerCommand(item, (accessor, ...args) => {
  11067. sendData({
  11068. jsonrpc: "2.0",
  11069. id: id++,
  11070. method: "workspace/executeCommand",
  11071. params: {
  11072. command: item,
  11073. arguments: args,
  11074. },
  11075. });
  11076. });
  11077. }
  11078. );
  11079. })();
  11080.  
  11081. // 注册"增量更新"
  11082. model.onDidChangeContent((event) => {
  11083. updateFile(workspace, filename, fileExtension, model.getValue()); // 更新文件
  11084. const changeDocRequest = {
  11085. jsonrpc: "2.0",
  11086. method: "textDocument/didChange",
  11087. params: {
  11088. textDocument: {
  11089. uri: model.uri.toString(),
  11090. version: model.getVersionId(),
  11091. },
  11092. contentChanges: event.changes.map((change) => ({
  11093. range: OJBetter_monaco.MonacoRangeTolspRange(change.range),
  11094. rangeLength: change.rangeLength,
  11095. text: change.text,
  11096. })),
  11097. },
  11098. };
  11099. sendData(changeDocRequest);
  11100. });
  11101.  
  11102. //注册"自动补全"
  11103. monaco.languages.registerCompletionItemProvider(language, {
  11104. provideCompletionItems: (model, position, context) => {
  11105. const request = {
  11106. jsonrpc: "2.0",
  11107. id: id++,
  11108. method: "textDocument/completion",
  11109. params: {
  11110. textDocument: {
  11111. uri: model.uri.toString(),
  11112. },
  11113. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11114. context: {
  11115. triggerKind: context.triggerKind + 1, // 这里要+1,两边的定义不一样。。。
  11116. triggerCharacter: context.triggerCharacter,
  11117. },
  11118. },
  11119. };
  11120. return new Promise((resolve, reject) => {
  11121. fetchData(request, (response) => {
  11122. const result = response.result;
  11123. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11124. if (!result) return resolve(null);
  11125. const CompletionItems = {
  11126. suggestions: result.items.map(
  11127. ({
  11128. label,
  11129. kind,
  11130. filterText,
  11131. insertText,
  11132. insertTextFormat,
  11133. sortText,
  11134. textEdit,
  11135. documentation,
  11136. additionalTextEdits,
  11137. }) => ({
  11138. additionalTextEdits: additionalTextEdits
  11139. ? additionalTextEdits.map(({ newText, range }) => ({
  11140. text: newText,
  11141. range: OJBetter_monaco.lspRangeToMonacoRange(range),
  11142. }))
  11143. : [],
  11144. documentation: documentation ? documentation.value : "",
  11145. filterText,
  11146. insertText: insertText ? insertText : textEdit.newText,
  11147. insertTextRules:
  11148. insertTextFormat === 2
  11149. ? monaco.languages.CompletionItemInsertTextRule
  11150. .InsertAsSnippet
  11151. : monaco.languages.CompletionItemInsertTextRule
  11152. .KeepWhitespace,
  11153. kind,
  11154. label,
  11155. sortText,
  11156. range: textEdit
  11157. ? textEdit.range
  11158. ? OJBetter_monaco.lspRangeToMonacoRange(textEdit.range)
  11159. : OJBetter_monaco.lspRangeToMonacoRange(textEdit.insert)
  11160. : null,
  11161. })
  11162. ),
  11163. };
  11164. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, CompletionItems);
  11165. resolve(CompletionItems);
  11166. });
  11167. });
  11168. },
  11169. });
  11170.  
  11171. // 注册"代码修复"
  11172. monaco.languages.registerCodeActionProvider(language, {
  11173. provideCodeActions: (model, range, context) => {
  11174. const request = {
  11175. id: id++,
  11176. jsonrpc: "2.0",
  11177. method: "textDocument/codeAction",
  11178. params: {
  11179. textDocument: {
  11180. uri: model.uri.toString(),
  11181. },
  11182. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11183. context: {
  11184. diagnostics: context.markers.map((item) => ({
  11185. range: OJBetter_monaco.MonacoRangeTolspRange({
  11186. startLineNumber: item.startLineNumber,
  11187. startColumn: item.startColumn,
  11188. endLineNumber: item.endLineNumber,
  11189. endColumn: item.endColumn,
  11190. }),
  11191. severity: OJBetter_monaco.MonacoSeverityTolspSeverity(
  11192. item.severity
  11193. ),
  11194. code: item.code,
  11195. source: item.source,
  11196. message: item.message,
  11197. tags: item.tags,
  11198. relatedInformation: item.relatedInformation
  11199. ? item.relatedInformation.map((item) => ({
  11200. location: {
  11201. uri: item.resource.toString(),
  11202. range: OJBetter_monaco.MonacoRangeTolspRange({
  11203. startLineNumber: item.startLineNumber,
  11204. startColumn: item.startColumn,
  11205. endLineNumber: item.endLineNumber,
  11206. endColumn: item.endColumn,
  11207. }),
  11208. },
  11209. message: item.message,
  11210. }))
  11211. : null,
  11212. })),
  11213. only: context.only ? [context.only] : [],
  11214. triggerKind: context.trigger,
  11215. },
  11216. },
  11217. };
  11218.  
  11219. return new Promise((resolve, reject) => {
  11220. fetchData(request, (response) => {
  11221. const result = response.result;
  11222. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11223. if (!result) return resolve(null);
  11224. const codeAction = {
  11225. actions: result.map((item) => ({
  11226. title: item.title,
  11227. kind: item.kind ? item.kind : "quickfix",
  11228. command: item.command
  11229. ? item.command.command
  11230. ? {
  11231. id: item.command.command,
  11232. arguments: item.command.arguments,
  11233. title: item.command.title,
  11234. }
  11235. : null
  11236. : null,
  11237. diagnostics: item.diagnostics
  11238. ? item.diagnostics.map((item) => ({
  11239. code: item.code,
  11240. message: item.message,
  11241. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11242. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(
  11243. item.severity
  11244. ),
  11245. source: item.source,
  11246. }))
  11247. : null,
  11248. edit: item.edit
  11249. ? OJBetter_monaco.lspEditToMonacoEdit(item.edit)
  11250. : item.arguments
  11251. ? {
  11252. edits: item.arguments.flatMap(
  11253. (item1) => OJBetter_monaco.lspEditToMonacoEdit(item1).edits
  11254. ),
  11255. }
  11256. : null,
  11257. })),
  11258. dispose: () => { },
  11259. };
  11260. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, codeAction);
  11261.  
  11262. resolve(codeAction);
  11263. });
  11264. });
  11265. },
  11266. });
  11267.  
  11268. // 注册"hover提示"
  11269. monaco.languages.registerHoverProvider(language, {
  11270. provideHover: (model, position) => {
  11271. const request = {
  11272. jsonrpc: "2.0",
  11273. id: id++,
  11274. method: "textDocument/hover",
  11275. params: {
  11276. textDocument: {
  11277. uri: model.uri.toString(),
  11278. },
  11279. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11280. },
  11281. };
  11282.  
  11283. return new Promise((resolve, reject) => {
  11284. fetchData(request, (response) => {
  11285. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11286. const result = response.result;
  11287.  
  11288. if (!result) return resolve(null);
  11289. const Hover = {
  11290. range: result.range
  11291. ? OJBetter_monaco.lspRangeToMonacoRange(result.range)
  11292. : new monaco.Range(
  11293. position.lineNumber,
  11294. position.column,
  11295. position.lineNumber,
  11296. position.column
  11297. ),
  11298. contents: Array.isArray(result.contents)
  11299. ? result.contents.map((item) => ({
  11300. value: item.value ? item.value : item,
  11301. }))
  11302. : [
  11303. {
  11304. value: result.contents.value,
  11305. },
  11306. ],
  11307. };
  11308. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, Hover);
  11309. resolve(Hover);
  11310. });
  11311. });
  11312. },
  11313. });
  11314.  
  11315. // 注册"inlay提示"
  11316. if (language == "cpp" || language == "java")
  11317. monaco.languages.registerInlayHintsProvider(language, {
  11318. provideInlayHints: (model, range, token) => {
  11319. return new Promise((resolve, reject) => {
  11320. const request = {
  11321. jsonrpc: "2.0",
  11322. id: id++,
  11323. method: "textDocument/inlayHint",
  11324. params: {
  11325. textDocument: {
  11326. uri: model.uri.toString(),
  11327. },
  11328. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11329. },
  11330. };
  11331.  
  11332. fetchData(request, (response) => {
  11333. const result = response.result;
  11334. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11335.  
  11336. if (!result) return resolve(null);
  11337.  
  11338. const inlayHints = {
  11339. hints: result.map((item) => {
  11340. return {
  11341. kind: item.kind,
  11342. label: item.label,
  11343. paddingLeft: item.paddingLeft,
  11344. paddingRight: item.paddingRight,
  11345. position: {
  11346. lineNumber: item.position.line + 1,
  11347. column: item.position.character + 1,
  11348. },
  11349. };
  11350. }),
  11351. };
  11352. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, inlayHints);
  11353.  
  11354. resolve(inlayHints);
  11355. });
  11356. });
  11357. },
  11358. });
  11359.  
  11360. // 注册"转到定义"
  11361. monaco.languages.registerDefinitionProvider(language, {
  11362. provideDefinition: (model, position) => {
  11363. const request = {
  11364. jsonrpc: "2.0",
  11365. id: id++,
  11366. method: "textDocument/definition",
  11367. params: {
  11368. textDocument: {
  11369. uri: model.uri.toString(),
  11370. },
  11371. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11372. },
  11373. };
  11374.  
  11375. return new Promise((resolve, reject) => {
  11376. fetchData(request, (response) => {
  11377. const result = response.result;
  11378. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11379.  
  11380. if (result.length == 0) return resolve(null);
  11381. const definition = result.map((item) => ({
  11382. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11383. uri: monaco.Uri.parse(item.uri), //
  11384. }));
  11385. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, definition);
  11386.  
  11387. resolve(definition);
  11388. });
  11389. });
  11390.  
  11391. return null; // 如果没有内容,则返回null
  11392. },
  11393. });
  11394.  
  11395. // 注册"转到引用"
  11396. monaco.languages.registerReferenceProvider(language, {
  11397. provideReferences: (model, position, context) => {
  11398. const request = {
  11399. jsonrpc: "2.0",
  11400. id: id++,
  11401. method: "textDocument/references",
  11402. params: {
  11403. context: context,
  11404. textDocument: {
  11405. uri: model.uri.toString(),
  11406. },
  11407. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11408. },
  11409. };
  11410.  
  11411. return new Promise((resolve, reject) => {
  11412. fetchData(request, (response) => {
  11413. const result = response.result;
  11414. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11415.  
  11416. if (result.length == 0) return resolve([]);
  11417.  
  11418. const references = result.map((item) => ({
  11419. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11420. uri: monaco.Uri.parse(item.uri), //
  11421. }));
  11422. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, references);
  11423. resolve(references);
  11424. });
  11425. });
  11426. return []; // 如果没有内容,则返回空数组
  11427. },
  11428. });
  11429.  
  11430. // 注册"符号引用点击高亮"
  11431. monaco.languages.registerDocumentHighlightProvider(language, {
  11432. provideDocumentHighlights: (model, position) => {
  11433. const request = {
  11434. jsonrpc: "2.0",
  11435. id: id++,
  11436. method: "textDocument/documentHighlight",
  11437. params: {
  11438. textDocument: {
  11439. uri: model.uri.toString(),
  11440. },
  11441. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11442. },
  11443. };
  11444.  
  11445. return new Promise((resolve, reject) => {
  11446. fetchData(request, (response) => {
  11447. const result = response.result;
  11448. pushLSPLogMessage("info", `documentHighlight ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11449.  
  11450. if (!result || result.length == 0) return resolve([]);
  11451. const highlights = result.map((item) => ({
  11452. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11453. kind: item.kind,
  11454. }));
  11455. pushLSPLogMessage("info",
  11456. `documentHighlight ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11457. highlights
  11458. );
  11459.  
  11460. resolve(highlights);
  11461. });
  11462. });
  11463. return []; // 如果没有内容,则返回空数组
  11464. },
  11465. });
  11466.  
  11467. // 注册"文件链接"
  11468. if (language == "cpp" || language == "java")
  11469. monaco.languages.registerLinkProvider(language, {
  11470. provideLinks: (model) => {
  11471. const request = {
  11472. jsonrpc: "2.0",
  11473. id: id++,
  11474. method: "textDocument/documentLink",
  11475. params: {
  11476. textDocument: {
  11477. uri: model.uri.toString(),
  11478. },
  11479. },
  11480. };
  11481.  
  11482. return new Promise((resolve, reject) => {
  11483. fetchData(request, (response) => {
  11484. const result = response.result;
  11485. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11486.  
  11487. if (!result) return resolve(null);
  11488. const links = {
  11489. links: result.map((item) => ({
  11490. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11491. url: item.target.toString(),
  11492. tooltip: item.tooltip ? item.tooltip : null,
  11493. })),
  11494. };
  11495. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, links);
  11496. resolve(links);
  11497. });
  11498. });
  11499. },
  11500. });
  11501.  
  11502. // 注册"格式化"
  11503. monaco.languages.registerDocumentFormattingEditProvider(language, {
  11504. provideDocumentFormattingEdits: (model, options, token) => {
  11505. const request = {
  11506. jsonrpc: "2.0",
  11507. id: id++,
  11508. method: "textDocument/formatting",
  11509. params: {
  11510. textDocument: {
  11511. uri: model.uri.toString(),
  11512. },
  11513. options: options,
  11514. },
  11515. };
  11516.  
  11517. return new Promise((resolve, reject) => {
  11518. fetchData(request, (response) => {
  11519. const result = response.result;
  11520. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11521.  
  11522. const TextEdit = result.map((edit) => ({
  11523. range: OJBetter_monaco.lspRangeToMonacoRange(edit.range),
  11524. text: edit.newText,
  11525. }));
  11526. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, TextEdit);
  11527. resolve(TextEdit);
  11528. });
  11529. });
  11530. },
  11531. });
  11532.  
  11533. // 注册"部分格式化"
  11534. monaco.languages.registerDocumentRangeFormattingEditProvider(language, {
  11535. provideDocumentRangeFormattingEdits: (model, range, options) => {
  11536. const request = {
  11537. jsonrpc: "2.0",
  11538. id: id++,
  11539. method: "textDocument/rangeFormatting",
  11540. params: {
  11541. textDocument: {
  11542. uri: model.uri.toString(),
  11543. },
  11544. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11545. options,
  11546. },
  11547. };
  11548.  
  11549. return new Promise((resolve, reject) => {
  11550. fetchData(request, (response) => {
  11551. const result = response.result;
  11552. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11553.  
  11554. if (!result || result.length == 0) return resolve([]);
  11555. const edits = result.map((item) => ({
  11556. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11557. text: item.newText,
  11558. }));
  11559. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11560. resolve(edits);
  11561. });
  11562. });
  11563. },
  11564. });
  11565.  
  11566. // 注册"重命名"
  11567. monaco.languages.registerRenameProvider(language, {
  11568. provideRenameEdits: (model, position, newName, token) => {
  11569. const request = {
  11570. jsonrpc: "2.0",
  11571. id: id++,
  11572. method: "textDocument/rename",
  11573. params: {
  11574. textDocument: {
  11575. uri: model.uri.toString(),
  11576. },
  11577. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11578. newName: newName,
  11579. },
  11580. };
  11581.  
  11582. return new Promise((resolve, reject) => {
  11583. fetchData(request, (response) => {
  11584. const result = response.result;
  11585. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11586.  
  11587. const rename = OJBetter_monaco.lspEditToMonacoEdit(result);
  11588. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, rename);
  11589. resolve(rename);
  11590. });
  11591. });
  11592. },
  11593. });
  11594.  
  11595. // 注册"折叠范围分析"
  11596. monaco.languages.registerFoldingRangeProvider(language, {
  11597. provideFoldingRanges: (model) => {
  11598. const request = {
  11599. jsonrpc: "2.0",
  11600. id: id++,
  11601. method: "textDocument/foldingRange",
  11602. params: {
  11603. textDocument: {
  11604. uri: model.uri.toString(),
  11605. },
  11606. },
  11607. };
  11608.  
  11609. return new Promise((resolve, reject) => {
  11610. fetchData(request, (response) => {
  11611. const result = response.result;
  11612. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11613.  
  11614. if (!result) return resolve([]);
  11615. const foldingRanges = result.map((item) => ({
  11616. start: item.startLine + 1,
  11617. end: item.endLine + 1,
  11618. kind: monaco.languages.FoldingRangeKind.fromValue(item.kind),
  11619. }));
  11620. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, foldingRanges);
  11621. resolve(foldingRanges);
  11622. });
  11623. });
  11624. },
  11625. });
  11626.  
  11627. // 注册"方法签名提示"
  11628. monaco.languages.registerSignatureHelpProvider(language, {
  11629. signatureHelpTriggerCharacters:
  11630. serverInfo.capabilities.signatureHelpProvider.triggerCharacters,
  11631. provideSignatureHelp: (model, position, token, context) => {
  11632. const request = {
  11633. jsonrpc: "2.0",
  11634. id: id++,
  11635. method: "textDocument/signatureHelp",
  11636. params: {
  11637. textDocument: {
  11638. uri: model.uri.toString(),
  11639. },
  11640. position: {
  11641. line: position.lineNumber - 1,
  11642. character: position.column - 1,
  11643. },
  11644. context: {
  11645. triggerKind: context.triggerKind,
  11646. triggerCharacter: context.triggerCharacter,
  11647. isRetrigger: context.isRetrigger,
  11648. activeSignatureHelp: context.activeSignatureHelp,
  11649. },
  11650. },
  11651. };
  11652.  
  11653. return new Promise((resolve, reject) => {
  11654. fetchData(request, (response) => {
  11655. const result = response.result;
  11656.  
  11657. pushLSPLogMessage("info", `signatureHelp ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11658.  
  11659. if (!result) return resolve(null);
  11660. const SignatureHelpResult = {
  11661. value: {
  11662. activeParameter: result.activeParameter,
  11663. activeSignature: result.activeSignature,
  11664. signatures: result.signatures,
  11665. },
  11666. dispose: () => { },
  11667. };
  11668.  
  11669. pushLSPLogMessage("info",
  11670. `signatureHelp ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11671. SignatureHelpResult
  11672. );
  11673. resolve(SignatureHelpResult);
  11674. });
  11675. });
  11676. },
  11677. });
  11678.  
  11679. // 注册"渐进式自动格式化" 如果server有这个
  11680. if (serverInfo.capabilities.documentOnTypeFormattingProvider)
  11681. monaco.languages.registerOnTypeFormattingEditProvider(language, {
  11682. autoFormatTriggerCharacters: [
  11683. serverInfo.capabilities.documentOnTypeFormattingProvider
  11684. .firstTriggerCharacter,
  11685. ],
  11686. provideOnTypeFormattingEdits: (model, position, ch, options) => {
  11687. const request = {
  11688. jsonrpc: "2.0",
  11689. id: id++,
  11690. method: "textDocument/onTypeFormatting",
  11691. params: {
  11692. textDocument: {
  11693. uri: model.uri.toString(),
  11694. },
  11695. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11696. ch,
  11697. options,
  11698. },
  11699. };
  11700.  
  11701. return new Promise((resolve, reject) => {
  11702. fetchData(request, (response) => {
  11703. const result = response.result;
  11704. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11705.  
  11706. if (!result || result.length == 0) return resolve([]);
  11707.  
  11708. const edits = result.map((item) => ({
  11709. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11710. text: item.newText,
  11711. }));
  11712. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11713. resolve(edits);
  11714. });
  11715. });
  11716. },
  11717. });
  11718. };
  11719.  
  11720. /**
  11721. * 被动式接收处理
  11722. */
  11723. OJBetter_monaco.PassiveReceiveHandler = () => {
  11724.  
  11725. // "实时代码诊断"
  11726. OJBetter_monaco.updateMarkers = function (message) {
  11727. const params = message.params;
  11728. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11729.  
  11730. if (!params) return;
  11731. const markers = params.diagnostics.map((item1) => ({
  11732. code: item1.code,
  11733. message: item1.message,
  11734. ...OJBetter_monaco.lspRangeToMonacoRange(item1.range),
  11735. relatedInformation: item1.relatedInformation
  11736. ? item1.relatedInformation.map((item2) => ({
  11737. ...(item2.location.range
  11738. ? OJBetter_monaco.lspRangeToMonacoRange(item2.location.range)
  11739. : OJBetter_monaco.lspRangeToMonacoRange(item2.location)),
  11740. message: item2.message,
  11741. resource: monaco.Uri.parse(item2.location.uri),
  11742. }))
  11743. : null,
  11744. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(item1.severity),
  11745. source: item1.source,
  11746. }));
  11747.  
  11748. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, markers);
  11749. monaco.editor.setModelMarkers(model, "eslint", markers);
  11750.  
  11751. // 更新状态底栏信息
  11752. const nowMarks = monaco.editor.getModelMarkers();
  11753. warningCount = 0;
  11754. errorCount = 0;
  11755. for (const marker of nowMarks) {
  11756. if (marker.severity === monaco.MarkerSeverity.Warning) {
  11757. warningCount++;
  11758. } else if (marker.severity === monaco.MarkerSeverity.Error) {
  11759. errorCount++;
  11760. }
  11761. }
  11762. $('#OJBetter_statusBar').text(`Warnings: ${warningCount}, Errors: ${errorCount}`);
  11763. };
  11764.  
  11765. // "应用服务器推送的更改"(代码修复)
  11766. OJBetter_monaco.applyEdit = function (message) {
  11767. const params = message.params;
  11768. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11769.  
  11770. if (!params) return;
  11771. const operations = Object.values(params.edit.changes)
  11772. .flat()
  11773. .map((item) => ({
  11774. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11775. text: item.newText,
  11776. }));
  11777.  
  11778. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, operations);
  11779. model.pushEditOperations([], operations, () => null); // 入栈编辑操作
  11780. };
  11781. }
  11782.  
  11783. if (!languageSocketState) await waitForLanguageSocketState();
  11784. OJBetter_monaco.Initialize();
  11785. }
  11786.  
  11787. // 语言更改
  11788. function changeMonacoLanguage(form) {
  11789. let nowSelect = form.selectLang.val();
  11790. // 记忆更改
  11791. GM_setValue('compilerSelection', nowSelect);
  11792. // 销毁旧的编辑器
  11793. try {
  11794. if (OJBetter.monaco.editor) OJBetter.monaco.editor.dispose();
  11795. } catch (error) {
  11796. console.warn("Encountered an error while attempting to delete the old editor, but it should not affect your regular usage.", error)
  11797. }
  11798. // 关闭旧的socket
  11799. OJBetter.monaco.lsp.socket.forEach(socket => {
  11800. socket.close();
  11801. });
  11802. // 移除相关元素
  11803. form.topRightDiv.empty();
  11804. $('#LSPLog').remove();
  11805. $('#OJBetter_statusBar').remove();
  11806. // 创建新的编辑器
  11807. if (nowSelect in value_monacoLanguageMap) {
  11808. let language = value_monacoLanguageMap[nowSelect];
  11809. if (language == "python" || language == "cpp") {
  11810. createMonacoEditor(language, form, true);
  11811. } else {
  11812. createMonacoEditor(language, form, false);
  11813. }
  11814. } else {
  11815. createMonacoEditor(null, form, false);
  11816. }
  11817. // 更新在线编译器参数
  11818. changeCompilerArgs(nowSelect);
  11819. }
  11820.  
  11821. // 收集样例数据
  11822. function getTestData() {
  11823. let testData = {};
  11824.  
  11825. /**
  11826. * 从pre中获取文本信息
  11827. * @param {JQuery<HTMLElement>} node 元素
  11828. * @returns {string} 文本信息
  11829. */
  11830. function getTextFromPre(node) {
  11831. let text;
  11832. if (node.find("br").length > 0) {
  11833. text = node.html().replace(/<br>/g, "\n"); // <br>作换行符的情况
  11834. } else {
  11835. text = node.text();
  11836. }
  11837. return text;
  11838. }
  11839.  
  11840. $('.input').each(function (index) {
  11841. var inputText = '';
  11842. if ($(this).find('pre').find('div').length > 0) {
  11843. $(this).find('pre').find('div').each(function () {
  11844. inputText += getTextFromPre($(this)) + '\n';
  11845. });
  11846. } else {
  11847. inputText = getTextFromPre($(this).find('pre'));
  11848. }
  11849. var outputText = '';
  11850. if ($('.output').eq(index).find('pre').find('div').length > 0) {
  11851. $('.output').eq(index).find('pre').find('div').each(function () {
  11852. inputText += getTextFromPre($(this)) + '\n';
  11853. });
  11854. } else {
  11855. outputText = getTextFromPre($('.output').eq(index).find('pre'));
  11856. }
  11857.  
  11858. testData[index + 1] = {
  11859. input: inputText.trim(),
  11860. output: outputText.trim()
  11861. };
  11862. });
  11863. return testData;
  11864. }
  11865.  
  11866. // 初始化自定义测试数据面板
  11867. function CustomTestInit() {
  11868. const url = window.location.href;
  11869.  
  11870. restoreText();
  11871.  
  11872. // 添加
  11873. $('#addCustomTest').click(function () {
  11874. var sampleDiv = $('<div class="sampleDiv">');
  11875. var inputTextarea = $('<p style="padding: 0px 5px;">input</p><textarea class="dynamicTextarea inputTextarea"></textarea>');
  11876. var outputTextarea = $('<p style="padding: 0px 5px;">output</p><textarea class="dynamicTextarea outputTextarea"></textarea>');
  11877. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  11878. sampleDiv.append(deleteCustomTest);
  11879. sampleDiv.append(inputTextarea);
  11880. sampleDiv.append(outputTextarea);
  11881. $('#customTests').append(sampleDiv);
  11882. });
  11883.  
  11884. // 实时保存文本内容到 IndexedDB 中
  11885. $(document).on('input', '.inputTextarea, .outputTextarea', function () {
  11886. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  11887. var objectStore = OJBetter.common.database.samplesData;
  11888. var samples = {
  11889. url: url,
  11890. samples: []
  11891. };
  11892. var index = 0;
  11893. $('.sampleDiv').each(function () {
  11894. var $sampleDiv = $(this);
  11895. var inputTextarea = $sampleDiv.find('.inputTextarea');
  11896. var outputTextarea = $sampleDiv.find('.outputTextarea');
  11897. $sampleDiv.attr('data-index', index);
  11898. inputTextarea.attr('id', 'input' + index);
  11899. outputTextarea.attr('id', 'output' + index);
  11900. var sample = {
  11901. id: index,
  11902. input: inputTextarea.val(),
  11903. output: outputTextarea.val()
  11904. };
  11905. samples.samples.push(sample);
  11906. index++;
  11907. });
  11908. objectStore.put(samples);
  11909. });
  11910. });
  11911.  
  11912. // 删除
  11913. $(document).on('click', '.deleteCustomTest', function () {
  11914. var $sampleDiv = $(this).closest('.sampleDiv');
  11915. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  11916. var objectStore = OJBetter.common.database.samplesData;
  11917. var index = parseInt($sampleDiv.attr('data-index'));
  11918. if (!isNaN(index)) {
  11919. objectStore.get(url).then(row => {
  11920. let samples = row.samples;
  11921. samples.splice(index, 1); // 移除第index个元素
  11922. objectStore.put({
  11923. url: url,
  11924. samples: samples
  11925. });
  11926. })
  11927. }
  11928. $sampleDiv.remove();
  11929. });
  11930. });
  11931.  
  11932. // 恢复保存的内容
  11933. function restoreText() {
  11934. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  11935. return OJBetter.common.database.samplesData.get(url);
  11936. }).then(function (data) {
  11937. if (data.samples && data.samples.length > 0) {
  11938. data.samples.forEach(function (item, index) {
  11939. var sampleDiv = $('<div class="sampleDiv">');
  11940. var inputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">input</p><textarea id="input${index}" class="dynamicTextarea inputTextarea"></textarea>`);
  11941. var outputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">output</p><textarea id="output${index}" class="dynamicTextarea outputTextarea"></textarea>`);
  11942. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  11943.  
  11944. inputTextarea.val(item.input);
  11945. outputTextarea.val(item.output);
  11946.  
  11947. sampleDiv.append(deleteCustomTest);
  11948. sampleDiv.append(inputTextarea);
  11949. sampleDiv.append(outputTextarea);
  11950. sampleDiv.attr('data-index', index)
  11951. $('#customTests').append(sampleDiv);
  11952. });
  11953. }
  11954. });
  11955. }
  11956. }
  11957.  
  11958. // 获取自定义测试数据
  11959. function getCustomTestData() {
  11960. const url = window.location.href;
  11961.  
  11962. return new Promise(function (resolve) {
  11963. var customTestData = {};
  11964. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  11965. return OJBetter.common.database.samplesData.get(url);
  11966. }).then(function (data) {
  11967. if (!data) resolve(customTestData);
  11968. if (data.samples && data.samples.length > 0) {
  11969. data.samples.forEach(function (item, index) {
  11970. customTestData[index + 1] = {
  11971. input: item.input,
  11972. output: item.output
  11973. };
  11974. });
  11975. }
  11976. resolve(customTestData);
  11977. });
  11978. });
  11979. }
  11980.  
  11981. // codeforces编译器参数列表
  11982. let officialLanguage = "";
  11983. function officialCompilerArgsChange(nowSelect) {
  11984. officialLanguage = nowSelect;
  11985. $('#CompilerArgsInput').prop("disabled", true);
  11986. }
  11987.  
  11988. // codeforces编译器通信
  11989. async function officialCompiler(code, input) {
  11990. const data = new FormData();
  11991. data.append('csrf_token', OJBetter.common.cf_csrf_token);
  11992. data.append('source', code);
  11993. data.append('tabSize', '4');
  11994. data.append('programTypeId', officialLanguage);
  11995. data.append('input', input);
  11996. data.append('output', '');
  11997. data.append('communityCode', '');
  11998. data.append('action', 'submitSourceCode');
  11999. data.append('programTypeId', officialLanguage);
  12000. data.append('sourceCode', code);
  12001.  
  12002. const requestOptions = {
  12003. method: 'POST',
  12004. url: `${OJBetter.common.hostAddress}/data/customtest`,
  12005. data: data,
  12006. headers: {
  12007. 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  12008. }
  12009. };
  12010.  
  12011. const result = {
  12012. Errors: '',
  12013. Result: '',
  12014. Stats: ''
  12015. };
  12016.  
  12017. try {
  12018. const submitResponse = await OJB_GMRequest(requestOptions);
  12019. if (submitResponse.status !== 200 || !submitResponse.response) {
  12020. result.Errors = `${i18next.t('compiler.official.pushError', { ns: 'codeEditor' })}`;
  12021. return result;
  12022. }
  12023.  
  12024. const submitResult = JSON.parse(submitResponse.response);
  12025. const customTestSubmitId = submitResult.customTestSubmitId;
  12026.  
  12027. const verdictResponse = await OJB_promiseRetryWrapper(
  12028. getOfficialCompilerVerdict,
  12029. {
  12030. maxRetries: 10,
  12031. retryInterval: 500
  12032. },
  12033. customTestSubmitId
  12034. );
  12035. return verdictResponse;
  12036. } catch (error) {
  12037. result.Errors = error.message;
  12038. return result;
  12039. }
  12040. }
  12041.  
  12042. // 获取codeforces编译器的执行结果
  12043. async function getOfficialCompilerVerdict(customTestSubmitId) {
  12044. const newdata = new FormData();
  12045. newdata.append('csrf_token', OJBetter.common.cf_csrf_token);
  12046. newdata.append('action', 'getVerdict');
  12047. newdata.append('customTestSubmitId', customTestSubmitId);
  12048.  
  12049. const requestOptions = {
  12050. method: 'POST',
  12051. url: `${OJBetter.common.hostAddress}/data/customtest`,
  12052. data: newdata,
  12053. headers: {
  12054. 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  12055. }
  12056. };
  12057.  
  12058. const responseDetails = await OJB_GMRequest(requestOptions);
  12059. if (responseDetails.status !== 200 || !responseDetails.response) {
  12060. throw new Error(`${i18next.t('compiler.official.getResultError', { ns: 'codeEditor' })}`);
  12061. }
  12062.  
  12063. const response = JSON.parse(responseDetails.response);
  12064. if (!response.stat) {
  12065. throw new Error('Verdict not ready, retrying...');
  12066. }
  12067.  
  12068. return {
  12069. Errors: response.verdict === "OK" ? null : response.verdict + '<br>' + response.output,
  12070. Result: response.output.replace(/\r\n/g, "\n"),
  12071. Stats: `Status: ${response.stat}`
  12072. };
  12073. }
  12074.  
  12075. // rextester编译器参数列表
  12076. let rextesterLanguage = "";
  12077. function rextesterCompilerArgsChange(nowSelect) {
  12078. let LanguageChoiceList = {
  12079. "4": "9", "6": "8", "7": "5", "9": "1", "13": "13", "19": "42", "20": "21", "28": "30", "31": "24", "32": "20",
  12080. "34": "17", "36": "4", "43": "6", "45": "7", "46": "4", "50": "7", "51": "9", "52": "27", "54": "7", "55": "23", "60": "4",
  12081. "61": "7", "65": "1", "67": "12", "70": "5", "73": "7", "74": "4", "75": "46", "77": "43", "79": "1", "80": "27", "83": "43", "87": "4"
  12082. }
  12083. let CompilerArgsList = {
  12084. "6": "-Wall -std=gnu99 -O2 -o a.out source_file.c",
  12085. "7": "-Wall -std=c++14 -O2 -o a.out source_file.cpp",
  12086. "20": "-o a.out source_file.go",
  12087. "27": "-Wall -std=c++14 -stdlib=libc++ -O2 -o a.out source_file.cpp",
  12088. "30": "source_file.d -ofa.out"
  12089. }
  12090. if (nowSelect in LanguageChoiceList) {
  12091. $('#RunTestButton').prop("disabled", false);
  12092. rextesterLanguage = LanguageChoiceList[nowSelect];
  12093. } else {
  12094. $('#RunTestButton').prop("disabled", true);
  12095. }
  12096. if (rextesterLanguage in CompilerArgsList) {
  12097. const rextesterCompilerArgs = CompilerArgsList[rextesterLanguage];
  12098. $('#CompilerArgsInput').val(rextesterCompilerArgs);
  12099. } else {
  12100. $('#CompilerArgsInput').val("");
  12101. }
  12102. }
  12103.  
  12104. // rextester编译器通信
  12105. async function rextesterCompiler(code, input) {
  12106. try {
  12107. const result = await OJB_promiseRetryWrapper(rextesterCompilerRequest, {
  12108. maxRetries: 5,
  12109. retryInterval: 500,
  12110. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  12111. }, code, input);
  12112. return result;
  12113. } catch (error) {
  12114. return { Errors: error.message, Result: '', Stats: '' };
  12115. }
  12116. }
  12117.  
  12118. // rextester编译器请求方法
  12119. async function rextesterCompilerRequest(code, input) {
  12120. const data = new FormData();
  12121. data.append('LanguageChoiceWrapper', rextesterLanguage);
  12122. data.append('EditorChoiceWrapper', '1');
  12123. data.append('LayoutChoiceWrapper', '1');
  12124. data.append('Program', code);
  12125. data.append('CompilerArgs', $('#CompilerArgsInput').val());
  12126. data.append('Input', input);
  12127. data.append('ShowWarnings', 'false');
  12128. data.append('IsInEditMode', 'false');
  12129. data.append('IsLive', 'false');
  12130.  
  12131. const responseDetails = await OJB_GMRequest({
  12132. method: 'POST',
  12133. url: 'https://rextester.com/rundotnet/Run',
  12134. data: data
  12135. });
  12136.  
  12137. if (responseDetails.status !== 200 || !responseDetails.response) {
  12138. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12139. }
  12140.  
  12141. const response = JSON.parse(responseDetails.response);
  12142. return {
  12143. Errors: response.Errors || '',
  12144. Result: response.Result || '',
  12145. Stats: response.Stats || ''
  12146. };
  12147. }
  12148.  
  12149. // wandbox编译器参数列表
  12150. var wandboxlist = JSON.parse(GM_getResourceText("wandboxlist"));
  12151. function wandboxCompilerArgsChange(nowSelect) {
  12152. let LanguageChoiceList = {
  12153. "6": "PHP", "7": "Python", "9": "C#", "12": "Haskell", "13": "Perl", "19": "OCaml",
  12154. "20": "Scala", "28": "D", "31": "Python", "32": "Go", "34": "JavaScript", "36": "Java", "40": "Python", "41": "Python",
  12155. "43": "C++", "50": "C++", "51": "Pascal", "52": "C++", "54": "C++", "60": "Java", "61": "C++", "65": "C#", "67": "Ruby",
  12156. "70": "Python", "73": "C++", "74": "Java", "75": "Rust", "79": "C#", "80": "C++", "87": "Java"
  12157. }
  12158.  
  12159. // 移除旧的
  12160. $('#CompilerChange').remove();
  12161.  
  12162. if (nowSelect in LanguageChoiceList) {
  12163. $('#RunTestButton').prop("disabled", false);
  12164. const Languagefiltered = wandboxlist.filter(obj => obj.language === LanguageChoiceList[nowSelect]);
  12165.  
  12166. // 创建编译器下拉框
  12167. let CompilerChange = $('<select id="CompilerChange" style="width: 100%;"></select>');
  12168.  
  12169. $('#CompilerSetting').show().append(CompilerChange);
  12170. for (let i = 0; i < Languagefiltered.length; i++) {
  12171. let Compiler = Languagefiltered[i];
  12172. let op = $("<option></option>")
  12173. .val(Compiler.name)
  12174. .text(Compiler["display-name"] + " " + Compiler.version);
  12175. $("#CompilerChange").append(op);
  12176. }
  12177.  
  12178. // 编译器参数刷新
  12179. function refreshCompilerArgs() {
  12180. var flags = '';
  12181. $("#CompilerBox").find("*").each(function () {
  12182. if ($(this).is("input[type='checkbox']")) {
  12183. let flag = $(this).prop("checked") ? $(this).val() : '';
  12184. flags += flag + (flag ? ' ' : '');
  12185. } else if ($(this).is("select") || $(this).is("input") || $(this).is("textarea")) {
  12186. let flag = $(this).val();
  12187. flags += flag + (flag ? ' ' : '');
  12188. }
  12189. });
  12190. $("#CompilerArgsInput").val(flags);
  12191. $("#CompilerArgsInput").prop("readonly", true); // 只读
  12192. }
  12193.  
  12194. // 编译器切换监听
  12195. CompilerChange.change(function () {
  12196. let selectedName = CompilerChange.val();
  12197. let Compiler = Languagefiltered.find(
  12198. (obj) => obj.name === selectedName
  12199. );
  12200.  
  12201. $("#CompilerArgsInput").val(); // 初始化编译器输入框
  12202.  
  12203. $("#CompilerBox").remove();
  12204. let div = $("<div id='CompilerBox'></div>");
  12205.  
  12206. let display_compile_command = OJB_safeCreateJQElement(`<input id='${Compiler.name}' value='${Compiler['display-compile-command']}' style="display:none;"}></input>`);
  12207. div.append(display_compile_command);
  12208.  
  12209. let switches = Compiler.switches;
  12210. for (let i = 0; i < switches.length; i++) {
  12211. let switche = switches[i];
  12212.  
  12213. if (switche.type == "single") {
  12214. let single = OJB_safeCreateJQElement(`
  12215. <div>
  12216. <input type='checkbox' id='${switche.name}' value='${switche['display-flags']}' ${switche.default ? 'checked' : ''}></input>
  12217. <label for='${switche.name}'>${switche['display-name']}</label>
  12218. </div>
  12219. `);
  12220. div.append(single);
  12221. single.find("input").change(function () {
  12222. refreshCompilerArgs();
  12223. });
  12224. } else if (switche.type == "select") {
  12225. let select = OJB_safeCreateJQElement(`<select id='${switche.name}'></select>`);
  12226. select.data('previousValue', switche.options[0]['display-flags']);
  12227. div.append(select);
  12228. for (let i = 0; i < switche.options.length; i++) {
  12229. let option = switche.options[i];
  12230. let op = $("<option></option>")
  12231. .val(option['display-flags'])
  12232. .text(option['display-name']);
  12233. select.append(op);
  12234. }
  12235. select.change(function () {
  12236. refreshCompilerArgs();
  12237. });
  12238. }
  12239. }
  12240.  
  12241. if (Compiler['compiler-option-raw'] == true) {
  12242. let textarea = OJB_safeCreateJQElement(`<textarea id="compiler_option_raw" placeholder="Raw compiler options" style="resize: vertical;"></textarea>`);
  12243. div.append(textarea);
  12244. textarea.on('input', function () {
  12245. refreshCompilerArgs();
  12246. });
  12247. }
  12248. if (Compiler['runtime-option-raw'] == true) {
  12249. let textarea = OJB_safeCreateJQElement(`<textarea id="runtime_option_raw" placeholder="Raw runtime options" style="resize: vertical;"></textarea>`);
  12250. div.append(textarea);
  12251. textarea.on('input', function () {
  12252. refreshCompilerArgs();
  12253. });
  12254. }
  12255. $("#CompilerSetting").append(div);
  12256.  
  12257. refreshCompilerArgs(); // 初始化
  12258. });
  12259.  
  12260. CompilerChange.trigger("change"); // 初始化
  12261. } else {
  12262. $('#RunTestButton').prop("disabled", true);
  12263. }
  12264. }
  12265.  
  12266. // wandbox编译器通信
  12267. async function wandboxCompiler(code, input) {
  12268. try {
  12269. const result = await OJB_promiseRetryWrapper(wandboxCompilerRequest, {
  12270. maxRetries: 5,
  12271. retryInterval: 500,
  12272. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  12273. }, code, input);
  12274. return result;
  12275. } catch (error) {
  12276. return { Errors: error.message, Result: '', Stats: '' };
  12277. }
  12278. }
  12279.  
  12280. // wandbox编译器请求方法
  12281. async function wandboxCompilerRequest(code, input) {
  12282. const data = {
  12283. code: code,
  12284. codes: [],
  12285. compiler: $('#CompilerChange').val().replace($('#compiler_option_raw').val(), '').replace($('#runtime_option_raw').val(), ''),
  12286. 'compiler-option-raw': $('#compiler_option_raw').val(),
  12287. 'runtime-option-raw': $('#runtime_option_raw').val(),
  12288. options: $("#CompilerArgsInput").val(),
  12289. description: '',
  12290. stdin: input,
  12291. title: ''
  12292. };
  12293.  
  12294. const responseDetails = await OJB_GMRequest({
  12295. method: 'POST',
  12296. url: 'https://wandbox.org/api/compile.json',
  12297. data: JSON.stringify(data),
  12298. headers: {
  12299. 'Content-Type': 'application/json'
  12300. }
  12301. });
  12302.  
  12303. if (responseDetails.status !== 200 || !responseDetails.response) {
  12304. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12305. }
  12306.  
  12307. const response = JSON.parse(responseDetails.response);
  12308. return {
  12309. Errors: response.compiler_error === "" ? response.signal : response.compiler_error,
  12310. Result: response.program_output || '',
  12311. Stats: response.status === "0" ? "Finish" : "Error"
  12312. };
  12313. }
  12314.  
  12315. // 更改编译器参数
  12316. function changeCompilerArgs(nowSelect) {
  12317. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12318. officialCompilerArgsChange(nowSelect);
  12319. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12320. rextesterCompilerArgsChange(nowSelect);
  12321. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12322. wandboxCompilerArgsChange(nowSelect);
  12323. }
  12324. }
  12325.  
  12326. // 在线编译器通信
  12327. async function onlineCompilerConnect(code, input) {
  12328. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12329. return await officialCompiler(code, input);
  12330. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12331. return await rextesterCompiler(code, input);
  12332. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12333. return await wandboxCompiler(code, input);
  12334. }
  12335. }
  12336.  
  12337. // 差异对比
  12338. function codeDiff(expectedText, actualText) {
  12339. // 将文本按行拆分
  12340. const expectedLines = expectedText ? expectedText.split('\n') : [];
  12341. const actualLines = actualText ? actualText.split('\n') : [];
  12342.  
  12343. const output = document.createElement('div');
  12344.  
  12345. const createLineElement = (lineNo, contentElement) => {
  12346. const lineDiv = document.createElement('div');
  12347. lineDiv.className = 'diffLine';
  12348.  
  12349. const lineNoDiv = document.createElement('div');
  12350. lineNoDiv.className = 'lineNo';
  12351. lineNoDiv.textContent = lineNo;
  12352.  
  12353. lineDiv.appendChild(lineNoDiv);
  12354. lineDiv.appendChild(contentElement);
  12355.  
  12356. return lineDiv;
  12357. };
  12358.  
  12359. const createContentElement = (isEquals = true, expected = null, removed = null) => {
  12360. const contentDiv = document.createElement('div');
  12361. contentDiv.className = 'lineContent';
  12362.  
  12363. if (isEquals) {
  12364. const span = document.createElement('span');
  12365. span.textContent = expected;
  12366. contentDiv.appendChild(span);
  12367. } else {
  12368. if (removed != null) {
  12369. const removedSpan = document.createElement('span');
  12370. removedSpan.className = 'removed';
  12371. removedSpan.textContent = removed;
  12372. contentDiv.appendChild(removedSpan);
  12373. }
  12374. if (expected != null) {
  12375. const addedSpan = document.createElement('span');
  12376. addedSpan.className = 'added';
  12377. addedSpan.textContent = expected;
  12378. contentDiv.appendChild(addedSpan);
  12379. }
  12380. }
  12381.  
  12382. return contentDiv;
  12383. };
  12384.  
  12385. let index = 1;
  12386.  
  12387. expectedLines.forEach((expectedLine, i) => {
  12388. const actualLine = actualLines[i];
  12389. if (actualLine === undefined) {
  12390. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine)));
  12391. } else if (expectedLine === actualLine) {
  12392. output.appendChild(createLineElement(index++, createContentElement(true, expectedLine)));
  12393. } else {
  12394. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine, actualLine)));
  12395. }
  12396. });
  12397.  
  12398. // 处理多余的 actualLines
  12399. for (let i = expectedLines.length; i < actualLines.length; i++) {
  12400. output.appendChild(createLineElement(index++, createContentElement(false, null, actualLines[i])));
  12401. }
  12402.  
  12403. return output.innerHTML;
  12404. }
  12405.  
  12406. // 内容类型常量
  12407. const TestCaseContentType = {
  12408. TERMINAL: 'terminal',
  12409. DIFF: 'diff',
  12410. NO_DIFF: 'no_diff',
  12411. SUCCESS: 'success'
  12412. };
  12413.  
  12414. // 样例测试状态类
  12415. class TestCaseStatus {
  12416. constructor(item, prefix) {
  12417. this.testCase = OJB_safeCreateJQElement(`<div class="test-case"></div>`);
  12418. this.item = item;
  12419. this.prefix = prefix;
  12420. this.titleElement = OJB_safeCreateJQElement(`<div class="test-case-title">${this.prefix} ${this.item}</div>`);
  12421. this.statusElement = OJB_safeCreateJQElement(`<div class="test-case-status"></div>`);
  12422. this.contentElement = OJB_safeCreateJQElement(`<div class="test-case-content"></div>`);
  12423. this.judgeElement = OJB_safeCreateJQElement(`<div class="test-case-judge"></div>`);
  12424. this.testCase.append(this.titleElement, this.statusElement, this.contentElement, this.judgeElement);
  12425. $('#statePanel').append(this.testCase);
  12426. this.setStatus('Queued', 'queued');
  12427. }
  12428.  
  12429. setTitle(title) {
  12430. this.titleElement.text(title);
  12431. }
  12432.  
  12433. setStatus(text, status) {
  12434. this.statusElement.text(text).removeClass('queued running success error').addClass(status);
  12435. }
  12436.  
  12437. setContent(content, type) {
  12438. // 如果内容类型为SUCCESS,隐藏内容元素并提前返回
  12439. if (type === TestCaseContentType.SUCCESS) {
  12440. this.contentElement.hide();
  12441. return;
  12442. }
  12443.  
  12444. // 根据内容类型创建内容元素
  12445. const createContentElementByType = (content, type) => {
  12446. let contentElement;
  12447. switch (type) {
  12448. case TestCaseContentType.TERMINAL:
  12449. // 为TERMINAL类型创建一个新的终端容器
  12450. contentElement = OJB_safeCreateJQElement(`<div class="terminal-container" style="overflow: auto;"></div>`);
  12451. break;
  12452. case TestCaseContentType.DIFF:
  12453. case TestCaseContentType.NO_DIFF:
  12454. // 为DIFF和NO_DIFF类型创建相应的内容元素,并添加差异说明
  12455. const className = type === TestCaseContentType.DIFF ? "output_diff" : "output_no_diff";
  12456. contentElement = OJB_safeCreateJQElement(`<pre class="${className}">${content}</pre>`);
  12457. appendDiffNote();
  12458. break;
  12459. default:
  12460. throw new Error("Unsupported content type.");
  12461. }
  12462. return contentElement;
  12463. };
  12464.  
  12465. // 初始化终端
  12466. const initializeTerminal = (content, contentElement) => {
  12467. const term = new Terminal({ rows: 10, cols: 150 });
  12468. term.setOption('theme', { background: '#2d2e2c' });
  12469. term.setOption('convertEol', true); // 将换行符\n转换为\r\n
  12470. term.write(content);
  12471. term.open(contentElement.get(0));
  12472. };
  12473.  
  12474. // 添加差异说明
  12475. const appendDiffNote = () => {
  12476. const diffNote = OJB_safeCreateJQElement(`<div class="diff_note">${i18next.t('resultBlock.diffNote', { ns: 'codeEditor' })}</div>`);
  12477. this.testCase.append(diffNote);
  12478. };
  12479.  
  12480. // 创建并追加内容元素
  12481. const contentElement = createContentElementByType(content, type);
  12482. this.contentElement.append(contentElement);
  12483.  
  12484. // 如果内容类型为TERMINAL,初始化并打开终端
  12485. if (type === TestCaseContentType.TERMINAL) {
  12486. initializeTerminal(content, contentElement);
  12487. }
  12488. }
  12489.  
  12490. setJudge(judge) {
  12491. this.judgeElement.text(judge);
  12492. }
  12493. }
  12494.  
  12495. // 样例测试函数
  12496. async function runCode(event, runButton, sourceDiv, submitDiv) {
  12497. event.preventDefault();
  12498. const statePanel = $('#statePanel').show().empty();
  12499. const testData = getTestData();
  12500. const customTestData = await getCustomTestData();
  12501. const totalTests = Object.keys(customTestData).length + Object.keys(testData).length;
  12502.  
  12503. let passedTests = 0;
  12504. let failedTests = 0;
  12505. let hasError = false;
  12506.  
  12507. // 定义一个对象队列,包括创建的样例块实例和对应的样例数据
  12508. const queue = [];
  12509.  
  12510. // 先生成各个样例的块,并显示排队中,将创建的各个对象存到队列中,以便后面更新
  12511. for (const [item, data] of Object.entries(customTestData)) {
  12512. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.custom', { ns: 'codeEditor' }));
  12513. queue.push({ testCase, data });
  12514. }
  12515.  
  12516. if (!$('#onlyCustomTest').prop('checked')) {
  12517. for (const [item, data] of Object.entries(testData)) {
  12518. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.sample', { ns: 'codeEditor' }));
  12519. queue.push({ testCase, data });
  12520. }
  12521. }
  12522.  
  12523. // 测试函数
  12524. const runTest = async (testCase, data, index) => {
  12525. runButton.setButtonState('running', `${index}/${totalTests}`);
  12526.  
  12527. testCase.setStatus('Running', 'running');
  12528. const result = await onlineCompilerConnect(sourceDiv.val(), data.input);
  12529.  
  12530. if (result.Errors) {
  12531. testCase.setStatus('Compilation error or Time limit', 'error');
  12532. testCase.setContent(result.Errors, TestCaseContentType.TERMINAL);
  12533. hasError = true;
  12534. } else if (result.Result.trim() === data.output.trim()) {
  12535. testCase.setStatus('Accepted', 'success');
  12536. testCase.setContent('The output is correct.', TestCaseContentType.SUCCESS);
  12537. passedTests++;
  12538. } else {
  12539. testCase.setStatus('Wrong Answer', 'error');
  12540. const diffContent = $('#DontShowDiff').prop('checked') ? result.Result.trim() : codeDiff(data.output.trim(), result.Result.trim());
  12541. const contentType = $('#DontShowDiff').prop('checked') ? TestCaseContentType.NO_DIFF : TestCaseContentType.DIFF;
  12542. testCase.setContent(diffContent, contentType);
  12543. failedTests++;
  12544. }
  12545.  
  12546. const judgeStats = `${i18next.t('resultBlock.state', { ns: 'codeEditor' })}${result.Stats}`;
  12547. testCase.setJudge(judgeStats);
  12548.  
  12549. await OJB_delay(500); // 等待500毫秒
  12550. };
  12551.  
  12552. // 对队列中的对象进行测试
  12553. for (let i = 0; i < queue.length; i++) {
  12554. const { testCase, data } = queue[i];
  12555. await runTest(testCase, data, i + 1);
  12556. }
  12557.  
  12558. // 测试完成后更新按钮状态
  12559. if (hasError) {
  12560. runButton.setButtonState('error', i18next.t('runTestButton.error', { ns: 'codeEditor' }));
  12561. } else if (failedTests > 0) {
  12562. runButton.setButtonState('error', `${passedTests}/${totalTests} ` + i18next.t('runTestButton.partial', { ns: 'codeEditor' }));
  12563. } else {
  12564. runButton.setButtonState('success', i18next.t('runTestButton.success', { ns: 'codeEditor' }));
  12565. }
  12566. }
  12567.  
  12568. /**
  12569. * 添加题目页代码编辑器
  12570. * @returns
  12571. */
  12572. async function addProblemPageCodeEditor() {
  12573. if (typeof ace === 'undefined') {
  12574. const loadingMessage = new LoadingMessage();
  12575. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('error.codeEditor.load', { ns: 'alert' })}`, 'error');
  12576. return; // 因为Codeforces设定的是未登录时不能访问提交页,也不会加载ace库
  12577. }
  12578.  
  12579. // 获取提交页链接
  12580. const href = window.location.href;
  12581. let submitUrl;
  12582. if (/\/problemset\//.test(href)) {
  12583. // problemset
  12584. submitUrl = OJBetter.common.hostAddress + '/problemset/submit';
  12585. } else if (/\/gym\//.test(href)) {
  12586. // gym 题目
  12587. submitUrl = OJBetter.common.hostAddress + '/gym/' + ((href) => {
  12588. const regex = /\/gym\/(?<num>[0-9a-zA-Z]*?)\/problem\//;
  12589. const match = href.match(regex);
  12590. return match && match.groups.num;
  12591. })(href) + '/submit';
  12592. } else if (OJBetter.typeOfPage.is_acmsguru) {
  12593. // acmsguru 题目
  12594. submitUrl = href.replace(/\/problemsets[A-Za-z0-9\/#]*/, "/problemsets/acmsguru/submit");
  12595. } else {
  12596. submitUrl = href.replace(/\/problem[A-Za-z0-9\/#]*/, "/submit");
  12597. }
  12598.  
  12599. // 获取提交页HTML
  12600. let cloneHTML = await getSubmitHTML(submitUrl);
  12601.  
  12602. // 创建
  12603. let form = await createCodeEditorForm(submitUrl, cloneHTML);
  12604. let selectLang = form.selectLang;
  12605. let submitButton = form.submitButton;
  12606. let runButton = form.runButton;
  12607.  
  12608. // 初始化
  12609. CustomTestInit(); // 自定义测试数据面板
  12610. selectLang.val(OJBetter.monaco.compilerSelection);
  12611.  
  12612. // 设置语言选择change事件监听器
  12613. selectLang.on('change', () => {
  12614. changeMonacoLanguage(form); // 编辑器语言切换监听
  12615. });
  12616. changeMonacoLanguage(form);
  12617.  
  12618. // 样例测试
  12619. runButton.on('click', (event) => runCode(event, runButton, form.sourceDiv, form.submitDiv))
  12620. .setHoverRedo();
  12621.  
  12622. // 提交
  12623. submitButton.on('click', async function (event) {
  12624. event.preventDefault();
  12625. if (OJBetter.monaco.setting.isCodeSubmitDoubleConfirm) {
  12626. const submit = await OJB_createDialog(
  12627. i18next.t('submitCode.title', { ns: 'dialog' }),
  12628. i18next.t('submitCode.content', { ns: 'dialog' }),
  12629. [
  12630. i18next.t('submitCode.buttons.0', { ns: 'dialog' }),
  12631. i18next.t('submitCode.buttons.1', { ns: 'dialog' })
  12632. ]
  12633. ); //提交确认
  12634. if (submit) {
  12635. submitButton.after(`<img class="OJBetter_loding" src="//codeforces.org/s/84141/images/ajax-loading-24x24.gif">`);
  12636. $('#OJBetter_SubmitForm').submit();
  12637. } else {
  12638. submitButton.addClass('disabled');
  12639. setTimeout(function () {
  12640. submitButton.removeClass('disabled');
  12641. }, 300);
  12642. }
  12643. } else {
  12644. $('#OJBetter_SubmitForm').submit();
  12645. }
  12646. });
  12647. }
  12648.  
  12649. /**
  12650. * 获取翻译服务目标语言的对应代码
  12651. * @param {string} serverName 服务名称
  12652. * @returns {string} 目标语言,如果没有对应代码则返回中文
  12653. */
  12654. function getTargetLanguage(serverName) {
  12655. let targetLanguage = OJBetter.supportList.translationSupport[serverName][OJBetter.translation.targetLang];
  12656. if (targetLanguage) return targetLanguage;
  12657. else return OJBetter.supportList.translationSupport[serverName]['zh'];
  12658. }
  12659.  
  12660. /**
  12661. * 将文本中Markdown格式的加粗**转换成HTML格式。
  12662. * @param {string} text 文本
  12663. * @returns {string} 替换后的字符串
  12664. */
  12665. function convertBoldMarkdownToHTML(text) {
  12666. return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
  12667. }
  12668.  
  12669. /**
  12670. * 将文本中Markdown格式的链接文本转换成HTML格式。
  12671. * @param {string} text 文本
  12672. * @returns {string} 替换后的字符串
  12673. */
  12674. function convertLinksMarkdownToHTML(text) {
  12675. return text.replace(/(?<!!)\[(.*?)\]\(([^"]*?)("(.*?)")*\)/g, '<a href="$2" title="$4">$1</a>');
  12676. }
  12677.  
  12678. /**
  12679. * 将HTML格式的加粗文本转换回Markdown格式。
  12680. * @param {string} text 文本
  12681. * @returns {string} 替换后的字符串
  12682. */
  12683. function convertBoldHTMLToMarkdown(text) {
  12684. return text.replace(/<strong>(.*?)<\/strong>/g, '**$1**');
  12685. }
  12686.  
  12687. /**
  12688. * 将HTML格式的链接文本转换回Markdown格式。
  12689. * @param {string} html - 包含HTML链接标签<a>的字符串。
  12690. * @returns {string} 转换后的字符串,其中HTML链接标签被替换为Markdown的链接语法。
  12691. */
  12692. function convertLinksHTMLToMarkdown(html) {
  12693. return html.replace(/<a href="([^"]*)"( title="([^"]*)")*>([^<]+)<\/a>/g, '[$4]($1 "$3")');
  12694. }
  12695.  
  12696. /**
  12697. * DeepL翻译
  12698. * @param {string} raw 原文
  12699. * @returns {Promise<TransRawData>} 翻译结果对象
  12700. */
  12701. async function translate_deepl(raw) {
  12702. const id = (Math.floor(Math.random() * 99999) + 100000) * 1000;
  12703. const data = {
  12704. jsonrpc: '2.0',
  12705. method: 'LMT_handle_texts',
  12706. id,
  12707. params: {
  12708. splitting: 'newlines',
  12709. lang: {
  12710. source_lang_user_selected: 'auto',
  12711. target_lang: getTargetLanguage('deepl'),
  12712. },
  12713. texts: [{
  12714. text: raw,
  12715. requestAlternatives: 3
  12716. }],
  12717. timestamp: getTimeStamp(raw.split('i').length - 1)
  12718. }
  12719. }
  12720. let postData = JSON.stringify(data);
  12721. if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
  12722. postData = postData.replace('"method":"', '"method" : "');
  12723. } else {
  12724. postData = postData.replace('"method":"', '"method": "');
  12725. }
  12726. const options = {
  12727. method: 'POST',
  12728. url: 'https://www2.deepl.com/jsonrpc',
  12729. data: postData,
  12730. headers: {
  12731. 'Content-Type': 'application/json',
  12732. 'Host': 'www2.deepl.com',
  12733. 'Origin': 'https://www.deepl.com',
  12734. 'Referer': 'https://www.deepl.com/',
  12735. },
  12736. anonymous: true,
  12737. nocache: true,
  12738. }
  12739.  
  12740. return await BaseTranslate(options, res => JSON.parse(res)?.result?.texts?.[0]?.text || res, res => {
  12741. const resObj = {
  12742. status: true,
  12743. message: 'ok'
  12744. };
  12745. if (res.includes('"message":"Too many requests"')) {
  12746. resObj.status = false;
  12747. resObj.message = i18next.t('error.deepl429', { ns: 'translator' }); // Too many requests 提示
  12748. return resObj;
  12749. };
  12750. return resObj;
  12751. });
  12752. }
  12753.  
  12754. /**
  12755. * 使用 DeepL Free API 进行翻译
  12756. * @param {string} raw 原文
  12757. * @returns {Promise<TransRawData>} 翻译结果对象
  12758. */
  12759. async function translate_deepl_api_free(raw) {
  12760. const data = JSON.stringify({
  12761. text: [raw],
  12762. target_lang: getTargetLanguage('deepl'),
  12763. split_sentences: '1',
  12764. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12765. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12766. });
  12767.  
  12768. const options = {
  12769. method: "POST",
  12770. url: OJBetter.deepl.config.proxy || "https://api-free.deepl.com/v2/translate",
  12771. headers: {
  12772. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12773. "Content-Type": "application/json",
  12774. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12775. },
  12776. data: data,
  12777. onload: response => response.responseText,
  12778. onerror: error => console.error(error)
  12779. };
  12780.  
  12781. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  12782. }
  12783.  
  12784. /**
  12785. * 使用 DeepL Pro API 进行翻译
  12786. * @param {string} raw 原文
  12787. * @returns {Promise<TransRawData>} 翻译结果对象
  12788. */
  12789. async function translate_deepl_api_pro(raw) {
  12790. const data = JSON.stringify({
  12791. text: [raw],
  12792. target_lang: getTargetLanguage('deepl'),
  12793. split_sentences: '1',
  12794. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12795. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12796. });
  12797.  
  12798. const options = {
  12799. method: "POST",
  12800. url: OJBetter.deepl.config.proxy || "https://api.deepl.com/v2/translate",
  12801. headers: {
  12802. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12803. "Content-Type": "application/json",
  12804. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12805. },
  12806. data: data,
  12807. onload: response => response.responseText,
  12808. onerror: error => console.error(error)
  12809. };
  12810.  
  12811. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  12812. }
  12813.  
  12814. /**
  12815. * 使用 DeepLX 进行翻译
  12816. * @param {String} text 原文
  12817. * @returns {Promise<TransRawData>} 翻译结果对象
  12818. */
  12819. async function translate_deeplx(text) {
  12820. const options = {
  12821. method: "POST",
  12822. url: OJBetter.deepl.config.proxy || 'https://api.deeplx.org/translate',
  12823. data: JSON.stringify({
  12824. "text": text,
  12825. "source_lang": "EN",
  12826. "target_lang": getTargetLanguage('deepl'),
  12827. }),
  12828. headers: {
  12829. 'Content-Type': 'application/json',
  12830. ...(OJBetter.deepl.config.key ? { Authorization: `Bearer ${OJBetter.deepl.config.key}` } : {})
  12831. },
  12832. responseType: "json",
  12833. };
  12834.  
  12835. return await BaseTranslate(options, res => {
  12836. const parsedResponse = JSON.parse(res);
  12837. if (parsedResponse.code === 200 && parsedResponse.data) {
  12838. return parsedResponse.data;
  12839. } else {
  12840. throw new Error('Translation failed or invalid response format.');
  12841. }
  12842. });
  12843. }
  12844.  
  12845. function getTimeStamp(iCount) {
  12846. const ts = Date.now();
  12847. if (iCount !== 0) {
  12848. iCount = iCount + 1;
  12849. return ts - (ts % iCount) + iCount;
  12850. } else {
  12851. return ts;
  12852. }
  12853. }
  12854.  
  12855. /**
  12856. * 讯飞听见翻译
  12857. * @param {String} text 要翻译的文本
  12858. * @returns {Promise<TransRawData>} 翻译结果对象
  12859. */
  12860. async function translate_iflyrec(text) {
  12861. const options = {
  12862. method: "POST",
  12863. url: 'https://www.iflyrec.com/TranslationService/v1/textTranslation',
  12864. data: JSON.stringify({
  12865. "from": "2",
  12866. "to": getTargetLanguage('iflyrec'),
  12867. "contents": [{
  12868. "text": text,
  12869. "frontBlankLine": 0
  12870. }]
  12871. }),
  12872. anonymous: true,
  12873. headers: {
  12874. 'Content-Type': 'application/json',
  12875. 'Origin': 'https://www.iflyrec.com',
  12876. },
  12877. responseType: "json",
  12878. };
  12879. return await BaseTranslate(options, res => JSON.parse(res).biz[0].translateResult.replace(/\\n/g, "\n\n"));
  12880. }
  12881.  
  12882. /**
  12883. * 有道翻译
  12884. * @param {string} raw 原文
  12885. * @returns {Promise<TransRawData>} 翻译结果对象
  12886. */
  12887. async function translate_youdao_mobile(raw) {
  12888. const options = {
  12889. method: "POST",
  12890. url: 'http://m.youdao.com/translate',
  12891. data: "inputtext=" + encodeURIComponent(raw) + "&type=" + getTargetLanguage('youdao'),
  12892. anonymous: true,
  12893. headers: {
  12894. "Content-Type": "application/x-www-form-urlencoded",
  12895. 'Host': 'm.youdao.com',
  12896. 'Origin': 'http://m.youdao.com',
  12897. 'Referer': 'http://m.youdao.com/translate',
  12898. }
  12899. }
  12900. return await BaseTranslate(options,
  12901. res => {
  12902. const array = /id="translateResult">\s*?<li>([\s\S]*?)<\/li>\s*?<\/ul/.exec(res);
  12903. if (array && array.length > 1) {
  12904. return array[1];
  12905. } else {
  12906. return res;
  12907. }
  12908. },
  12909. res => {
  12910. const resObj = {
  12911. status: true,
  12912. message: 'ok'
  12913. };
  12914. if (res.includes('<title>413 Request Entity Too Large</title>')) {
  12915. resObj.status = false;
  12916. resObj.message = i18next.t('error.youdao413', { ns: 'translator' }); // Request Entity Too Large 提示
  12917. return resObj;
  12918. };
  12919. return resObj;
  12920. })
  12921. }
  12922.  
  12923. /**
  12924. * google翻译
  12925. * @param {string} raw 原文
  12926. * @returns {Promise<TransRawData>} 翻译结果对象
  12927. */
  12928. async function translate_gg(raw) {
  12929. const params = `tl=${getTargetLanguage('google')}&q=${encodeURIComponent(raw)}`;
  12930. const options = {
  12931. method: "GET",
  12932. url: `https://translate.google.com/m?${params}`,
  12933. }
  12934. return await BaseTranslate(options,
  12935. res => $(res).filter('.result-container').text() || $(res).find('.result-container').text());
  12936. }
  12937.  
  12938. /**
  12939. * 彩云翻译
  12940. * @param {string} raw 原文
  12941. * @returns {Promise<TransRawData>} 翻译结果对象
  12942. */
  12943. async function translate_caiyun(raw) {
  12944. const source = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
  12945. const dic = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"].reduce((dic, current, index) => { dic[current] = source[index]; return dic }, {});
  12946. const browser_id = CryptoJS.MD5(Math.random().toString()).toString();
  12947. const caiyun_jwt = await (async () => {
  12948. const options = {
  12949. method: "POST",
  12950. url: 'https://api.interpreter.caiyunai.com/v1/user/jwt/generate',
  12951. headers: {
  12952. "content-type": "application/json",
  12953. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  12954. "origin": "https://fanyi.caiyunapp.com",
  12955. },
  12956. data: JSON.stringify({ browser_id }),
  12957. }
  12958. const res = await OJB_GMRequest(options);
  12959. return JSON.parse(res.responseText).jwt;
  12960. })();
  12961.  
  12962. // 解码
  12963. const decodeUnicode = str => {
  12964. const decoder = new TextDecoder();
  12965. const data = Uint8Array.from(atob(str), c => c.charCodeAt(0));
  12966. return decoder.decode(data);
  12967. };
  12968. const decoder = line => decodeUnicode([...line].map(i => dic[i] || i).join(""));
  12969.  
  12970. const options = {
  12971. method: "POST",
  12972. url: 'https://api.interpreter.caiyunai.com/v1/translator',
  12973. data: JSON.stringify({
  12974. "source": raw.split('\n'),
  12975. "browser_id": browser_id,
  12976. "trans_type": getTargetLanguage('caiyun'),
  12977. "request_id": "web_fanyi",
  12978. "media": "text",
  12979. "os_type": "web",
  12980. "dict": true,
  12981. "cached": true,
  12982. "replaced": true,
  12983. "style": "formal",
  12984. "model": "",
  12985. "detect": true,
  12986. }),
  12987. headers: {
  12988. "content-type": "application/json;charset=UTF-8",
  12989. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  12990. "t-authorization": caiyun_jwt
  12991. }
  12992. }
  12993. return await BaseTranslate(options, res => JSON.parse(res).target.map(decoder).join('\n'))
  12994. }
  12995.  
  12996. /**
  12997. * ChatGPT
  12998. * @param {string} raw 原文
  12999. * @returns {Promise<TransRawData>} 翻译结果对象
  13000. */
  13001. async function translate_openai(raw) {
  13002. const modelDefault = 'gpt-3.5-turbo';
  13003. const lang = getTargetLanguage('openai');
  13004. const prompt = `
  13005. As a professional English translator, your task is to accurately translate a segment of an algorithm programming competition question into ${lang}.
  13006. The translation should use professional terms and maintain the text format, including ${OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru
  13007. ? "keeping the LaTeX equations unchanged."
  13008. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13009. }
  13010. After translation, please ensure that the ${lang} version conforms to normal expression habits.
  13011. What I need is a carefully polished ${lang} translation of my question segment, The segment to be translated is as follows: "
  13012. ${raw}
  13013. "`;
  13014. const data = {
  13015. model: OJBetter.chatgpt.config.model || modelDefault,
  13016. messages: [{
  13017. role: "assistant",
  13018. content: prompt
  13019. }],
  13020. temperature: 0.7,
  13021. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13022. }
  13023. const options = {
  13024. method: "POST",
  13025. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13026. data: JSON.stringify(data),
  13027. responseType: 'json',
  13028. headers: {
  13029. 'Content-Type': 'application/json',
  13030. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13031. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13032. }
  13033. }
  13034. return await BaseTranslate(options,
  13035. res => res,
  13036. undefined,
  13037. response => response.response.choices[0].message.content);
  13038. }
  13039.  
  13040. /**
  13041. * ChatGPT 流式传输
  13042. * @param {string} raw 原文
  13043. * @param {TranslateDiv} translateDiv 翻译结果面板
  13044. * @returns {Promise<TransRawData>} 翻译结果对象
  13045. */
  13046. async function translate_openai_stream(raw, translateDiv) {
  13047. const result = {
  13048. done: true,
  13049. checkPassed: null,
  13050. response: null,
  13051. responseText: null,
  13052. text: "",
  13053. error: null,
  13054. message: null
  13055. };
  13056. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13057. try {
  13058. for await (const delta of openai_stream(raw)) {
  13059. result.text += delta;
  13060. // 翻译结果面板更新
  13061. translateDiv.updateTranslateDiv(result.text, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru), false);
  13062. }
  13063. return result;
  13064. } catch (err) {
  13065. console.warn(err);
  13066. result.error = {
  13067. message: err.message || null,
  13068. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13069. enumerable: err,
  13070. source: 'openai_stream'
  13071. };
  13072. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13073. }
  13074.  
  13075. return result;
  13076. }
  13077.  
  13078. /**
  13079. * 流式传输
  13080. * @param {string} raw 原文
  13081. * @returns {AsyncGenerator<string>} 返回 AsyncGenerator
  13082. */
  13083. async function* openai_stream(raw) {
  13084. const modelDefault = 'gpt-3.5-turbo';
  13085. const lang = getTargetLanguage('openai');
  13086. const prompt = `
  13087. I hope you can act as a professional English translator to help me translate a segment of an algorithm programming competition question into ${lang}.
  13088. 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
  13089. ? "keeping the LaTeX equations unchanged."
  13090. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13091. }
  13092. After completing the translation, please polish the ${lang} version to ensure it conforms to normal expression habits.
  13093. What I need is a carefully polished ${lang} translation of my question segment, which is as follows:
  13094. "
  13095. ${raw}
  13096. "`;
  13097. const data = {
  13098. model: OJBetter.chatgpt.config.model || modelDefault,
  13099. messages: [{
  13100. role: "assistant",
  13101. content: prompt
  13102. }],
  13103. temperature: 0.7,
  13104. stream: true,
  13105. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13106. }
  13107. const options = {
  13108. method: "POST",
  13109. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13110. data: JSON.stringify(data),
  13111. responseType: 'stream',
  13112. headers: {
  13113. 'Content-Type': 'application/json',
  13114. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13115. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13116. }
  13117. }
  13118. const response = await OJB_GMRequest(options, true);
  13119. const reader = response.response.getReader();
  13120. const decoder = new TextDecoder();
  13121. let buffer = ''; // 用于累积数据片段的缓冲区
  13122.  
  13123. while (true) {
  13124. const { done, value } = await reader.read();
  13125. if (done) break;
  13126. buffer += decoder.decode(value, { stream: true }); // 将新的数据片段追加到缓冲区
  13127. let lines = buffer.split("\n\n"); // 处理累积的数据
  13128.  
  13129. // 缓冲区的最后一行可能还未完整接收,保留在缓冲区中,-1
  13130. for (let i = 0; i < lines.length - 1; i++) {
  13131. let line = lines[i];
  13132. line = line.substring(5); // 移除 'data:' 前缀
  13133. if (line.includes('[DONE]')) {
  13134. return; // End
  13135. }
  13136. try {
  13137. let data = JSON.parse(line);
  13138. let delta = data['choices'][0]['delta'];
  13139. let content = delta['content'] ? delta['content'] : "";
  13140. yield content; // 传递数据给调用者
  13141. } catch (error) {
  13142. console.warn(`Error parsing JSON: ${error}\n\nError data: ${line}`);
  13143. }
  13144. }
  13145.  
  13146. // 保留最后一行在缓冲区中
  13147. buffer = lines.slice(-1);
  13148. }
  13149.  
  13150. return buffer;
  13151. }
  13152.  
  13153. /**
  13154. * @typedef {Object} CheckResponseResult
  13155. * @property {boolean} status 检查是否通过
  13156. * @property {string} message 检查失败时的消息
  13157. */
  13158.  
  13159. /**
  13160. * @typedef {Object} ErrorResponse
  13161. * @property {Object} message 错误消息
  13162. * @property {Object} stack 错误堆栈
  13163. * @property {Object} enumerable 可枚举的错误属性
  13164. * @property {string} source 错误来源
  13165. */
  13166.  
  13167. /**
  13168. * @typedef {Object} TransRawData
  13169. * @property {boolean} done 操作是否完成
  13170. * @property {CheckResponseResult|null} checkPassed 检查是否通过的结果
  13171. * @property {Object|null} response 响应对象
  13172. * @property {string|null} text 处理后的文本
  13173. * @property {ErrorResponse} error 错误列表
  13174. * @property {string|null} message 可能的消息
  13175. */
  13176.  
  13177. /**
  13178. * 通用翻译函数
  13179. * @param {Object} options GM_xmlhttpRequest 的参数
  13180. * @param {Function} processer 响应再处理函数,它接收响应文本,并应返回处理后的文本。
  13181. * @param {Function} checkResponse 检查文本是否符合预期的函数,它接收文本,并返回一个Object,包含状态和信息。默认为返回 { status: true, message: 'ok' }
  13182. * @param {Function} getResponseText 重写响应文本获取函数,它接收response,并返回响应文本。 默认为 response.responseText
  13183. * @returns {Promise<TransRawData>} 返回 Promise,其解析值为翻译结果对象
  13184. */
  13185. async function BaseTranslate(options, processer, checkResponse = () => { return { status: true, message: 'ok' } }, getResponseText = (response) => response.responseText) {
  13186. const result = {
  13187. done: false,
  13188. checkPassed: null,
  13189. response: null,
  13190. responseText: null,
  13191. text: "",
  13192. error: null,
  13193. message: null
  13194. };
  13195. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13196. const toDo = async () => {
  13197. try {
  13198. result.response = await OJB_GMRequest(options);
  13199. result.responseText = result.response.responseText;
  13200. result.text = getResponseText(result.response);
  13201. } catch (err) {
  13202. console.warn(err);
  13203. result.error = {
  13204. message: err.message || null,
  13205. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13206. enumerable: err,
  13207. source: 'GMRequest'
  13208. };
  13209. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13210. throw result;
  13211. }
  13212. try {
  13213. result.text = processer(result.text);
  13214. } catch (err) {
  13215. console.warn(err);
  13216. result.error = {
  13217. message: err.message || null,
  13218. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13219. enumerable: err,
  13220. source: 'processer'
  13221. };
  13222. result.message = `${i18next.t('error.processer', { ns: 'translator' })}${helpText}`;
  13223. throw result;
  13224. }
  13225. try {
  13226. result.checkPassed = checkResponse(result.text);
  13227. if (result.checkPassed.status) result.done = true;
  13228. else result.message = result.checkPassed.message;
  13229. return result;
  13230. } catch (err) {
  13231. console.warn(err);
  13232. result.error = {
  13233. message: err.message || null,
  13234. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13235. enumerable: err,
  13236. source: 'checkResponse'
  13237. };
  13238. result.message = `${i18next.t('error.checkResponse', { ns: 'translator' })}${helpText}`;
  13239. throw result;
  13240. }
  13241. };
  13242.  
  13243. return await OJB_promiseRetryWrapper(toDo, {
  13244. maxRetries: 3,
  13245. errorHandler: (err, maxRetries, attemptsLeft) => {
  13246. const detailedError = {
  13247. maxRetries: maxRetries,
  13248. attemptsLeft: attemptsLeft,
  13249. ...err
  13250. };
  13251. return detailedError;
  13252. }
  13253. });
  13254. }
  13255.  
  13256. /**
  13257. * 查询服务余额
  13258. * @param {Object} quotaConfig - 配额配置对象
  13259. * @returns {Promise} 返回包含余额信息的 Promise
  13260. */
  13261. async function queryServerBalance(quotaConfig) {
  13262. // 确保传入了有效的配置对象
  13263. if (!quotaConfig || !quotaConfig.url) {
  13264. return Promise.reject(new Error('Quota configuration is missing.'));
  13265. }
  13266.  
  13267. // 准备请求选项
  13268. const requestOptions = {
  13269. method: quotaConfig.method || 'GET',
  13270. url: quotaConfig.url,
  13271. headers: {
  13272. ...Object.assign({}, ...quotaConfig.header)
  13273. },
  13274. data: JSON.stringify({ ...Object.assign({}, ...quotaConfig.data) })
  13275. };
  13276.  
  13277. // 发送请求并返回 Promise
  13278. return OJB_GMRequest(requestOptions).then(response => {
  13279. try {
  13280. const responseData = JSON.parse(response.responseText);
  13281. // 从响应数据中提取余额
  13282. const surplusPath = quotaConfig.surplus;
  13283. const surplusValue = OJB_evaluatePathOrExpression(responseData, surplusPath);
  13284. return surplusValue;
  13285. } catch (error) {
  13286. return Promise.reject(new Error('Failed to parse balance response.'));
  13287. }
  13288. }).catch(error => {
  13289. console.warn('Error querying balance:', error);
  13290. return Promise.reject(error);
  13291. });
  13292. }
  13293.  
  13294. /**
  13295. * 确认 jQuery 已加载
  13296. * @param {number} retryDelay 重试延迟(毫秒)
  13297. * @returns {Promise<void>}
  13298. */
  13299. async function ensureJQueryIsLoaded(retryDelay = 50) {
  13300. while (typeof jQuery === 'undefined') {
  13301. console.warn(`JQuery is not loaded. Retry after ${retryDelay} ms.`);
  13302. await OJB_delay(retryDelay);
  13303. retryDelay = Math.min(retryDelay * 2, 2000);
  13304. }
  13305. }
  13306.  
  13307. /**
  13308. * 加载必须的函数
  13309. * @returns {Promise} 加载提示信息
  13310. */
  13311. async function loadRequiredFunctions() {
  13312. await initVar();// 初始化全局变量
  13313. return Promise.allSettled([
  13314. initDB(), // 连接数据库
  13315. initI18next(), // i18next初始化
  13316. initButtonFunc(), // 加载按钮相关函数
  13317. initHTML2MarkDown(), // 初始化html2markdown转换器
  13318. checkScriptVersion(), // 更新检查
  13319. ...(OJBetter.typeOfPage.is_acmsguru ? [acmsguruReblock()] : []) // 为acmsguru题面重新划分div
  13320. ]);
  13321. }
  13322.  
  13323. /**
  13324. * DOM加载后即可执行
  13325. */
  13326. function initOnDOMReady() {
  13327. showAnnounce(); // 显示公告
  13328. showWarnMessage(); // 显示警告消息
  13329. initSettingsPanel(); // 加载设置按钮面板
  13330. initMonacoEditor(); // 初始化monaco编辑器资源
  13331. localizeWebsite(); // 网站本地化替换
  13332. addDependencyStyles(); // 添加一些依赖库的样式
  13333. addI18nStyles(); // 添加包含i18n内容的样式
  13334. if (OJBetter.basic.expandFoldingblocks) ExpandFoldingblocks(); // 折叠块展开
  13335. if (OJBetter.basic.renderPerfOpt) RenderPerfOpt(); // 折叠块渲染优化
  13336. if (OJBetter.basic.selectElementPerfOpt) SelectElementPerfOpt(); // 下拉选择框性能优化
  13337. if (OJBetter.typeOfPage.is_problem) {
  13338. const problemPageLinkbar = new ProblemPageLinkbar(); // 创建题目页相关链接栏
  13339. if (OJBetter.basic.showCF2vjudge) CF2vjudge(problemPageLinkbar); // 跳转到Vjudge按钮
  13340. if (OJBetter.basic.showJumpToLuogu) CF2luogu(problemPageLinkbar); // 跳转到洛谷按钮
  13341. if (OJBetter.clist.enabled.problem) showRatingByClist_problem(problemPageLinkbar); // problem页显示Rating
  13342. }
  13343. if (OJBetter.typeOfPage.is_contest) {
  13344. if (OJBetter.clist.enabled.contest) showRatingByClist_contest(); // contest页显示Rating
  13345. }
  13346. if (OJBetter.typeOfPage.is_problemset) {
  13347. if (OJBetter.clist.enabled.problemset) showRatingByClist_problemset(); // problemset页显示Rating
  13348. }
  13349. if (OJBetter.typeOfPage.is_problem && OJBetter.monaco.enableOnProblemPage) {
  13350. addProblemPageCodeEditor(); // 添加题目页代码编辑器
  13351. }
  13352. }
  13353.  
  13354. /**
  13355. * 需要在页面资源完全加载后执行的函数
  13356. */
  13357. function onResourcesReady(loadingMessage) {
  13358. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadFunc', { ns: 'alert' })}`);
  13359. initializeInParallel(loadingMessage);
  13360. initializeSequentially(loadingMessage);
  13361. }
  13362.  
  13363. /**
  13364. * 可以异步并行的函数
  13365. */
  13366. function initializeInParallel(loadingMessage) {
  13367. if (OJBetter.basic.darkMode == "dark") darkModeStyleAdjustment(); // 黑暗模式额外的处理事件
  13368. if (OJBetter.basic.commentPaging) CommentPagination(); // 评论区分页
  13369. if (OJBetter.translation.comment.transMode == "2") multiChoiceTranslation(); // 选段翻译支持
  13370. if (OJBetter.monaco.beautifyPreBlocks) beautifyPreBlocksWithMonaco(); // 美化Pre代码块
  13371. }
  13372.  
  13373. /**
  13374. * 必须按序执行的函数
  13375. */
  13376. async function initializeSequentially(loadingMessage) {
  13377. await addConversionButton(); // 添加MD/复制/翻译按钮
  13378. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  13379. await initTransResultsRecover(); // 翻译结果恢复功能初始化
  13380. }
  13381. if (OJBetter.translation.auto.enabled) {
  13382. await initTransWhenViewable(); // 自动翻译
  13383. }
  13384. if (OJBetter.basic.standingsRecolor && OJBetter.typeOfPage.is_cfStandings) {
  13385. await recolorStandings(); // cf赛制榜单重新着色
  13386. }
  13387. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadSuccess', { ns: 'alert' })}`, 'success', 3000);
  13388. }
  13389.  
  13390. /**
  13391. * 主方法
  13392. */
  13393. async function main() {
  13394. await ensureJQueryIsLoaded(); // 等待jQuery加载
  13395. const loadingMessage = new LoadingMessage();
  13396. await loadRequiredFunctions(); // 加载必须的函数
  13397. initOnDOMReady(); // DOM加载后即可执行的函数
  13398. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('onload', { ns: 'alert' })}`);
  13399.  
  13400. // 检查页面资源是否已经完全加载
  13401. if (OJBetter.state.notWaiteLoaded) {
  13402. onResourcesReady(loadingMessage);
  13403. } else {
  13404. if (document.readyState === 'complete') {
  13405. onResourcesReady(loadingMessage);
  13406. } else {
  13407. window.addEventListener('load', () => onResourcesReady(loadingMessage));
  13408. }
  13409. }
  13410. };
  13411.  
  13412. // ------------------------------
  13413. // 脚本加载入口
  13414. if (document.readyState === 'loading') {
  13415. document.addEventListener("DOMContentLoaded", main);
  13416. } else {
  13417. main(); // 如果DOMContentLoaded已经触发,立即执行
  13418. }
  13419. // ------------------------------
  13420.  
  13421. // ------------------------------
  13422. // 配置自动迁移代码(将在10个小版本后移除-1.83)
  13423. // ------------------------------
  13424.  
  13425. {
  13426. let bottomZh_CN = GM_getValue("bottomZh_CN");
  13427. if (bottomZh_CN !== undefined) {
  13428. if (bottomZh_CN == true) {
  13429. GM_setValue("localizationLanguage", "zh");
  13430. } else {
  13431. GM_setValue("localizationLanguage", "initial");
  13432. }
  13433. GM_deleteValue("bottomZh_CN");
  13434. location.reload();
  13435. }
  13436. }
  13437. {
  13438. let config = GM_getValue("chatgpt-config");
  13439. if (config && config !== undefined) {
  13440. let index = parseInt(config.choice, 10);
  13441. if (index == -1) config.choice = "";
  13442. else config.choice = config.configurations[index].note;
  13443. config.configurations.forEach(function (item) {
  13444. item.name = item.note;
  13445. delete item.note;
  13446. });
  13447. GM_deleteValue("chatgpt-config");
  13448. GM_setValue("chatgpt_config", config);
  13449. location.reload();
  13450. }
  13451. }
  13452. {
  13453. let config = GM_getValue("Complet_config");
  13454. if (config && config.changed === undefined) {
  13455. config.changed = true; // 设置一个迁移标志
  13456. config.configurations.forEach(function (item) {
  13457. if (item.note !== undefined) {
  13458. item.name = item.note;
  13459. delete item.note;
  13460. }
  13461. });
  13462. GM_setValue("Complet_config", config);
  13463. location.reload();
  13464. }
  13465. }