Atcoder Better!

A Tampermonkey script for AtCoder that enhances functionality and interface.

As of 2024-07-04. See the latest version.

  1. // ==UserScript==
  2. // @name Atcoder Better!
  3. // @namespace https://greatest.deepsurf.us/users/747162
  4. // @version 1.17.0
  5. // @description 一个适用于 AtCoder 的 Tampermonkey 脚本,增强功能与界面。
  6. // @author 北极小狐
  7. // @match *://atcoder.jp/*
  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 dict.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 greatest.deepsurf.us
  23. // @connect sustech.edu.cn
  24. // @connect aowuucdn.oss-cn-beijing.aliyuncs.com
  25. // @connect aowuucdn.oss-accelerate.aliyuncs.com
  26. // @connect 127.0.0.1
  27. // @connect *
  28. // @grant GM_xmlhttpRequest
  29. // @grant GM_info
  30. // @grant GM_setValue
  31. // @grant GM_getValue
  32. // @grant GM_listValues
  33. // @grant GM_deleteValue
  34. // @grant GM_addStyle
  35. // @grant GM_setClipboard
  36. // @grant GM_getResourceText
  37. // @icon https://aowuucdn.oss-accelerate.aliyuncs.com/atcoder.png
  38. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/turndown/7.2.0/turndown.min.js#sha512-sJzEecN5Nk8cq81zKtGq6/z9Z/r3q38zV9enY75IVxiG7ybtlNUt864sL4L1Kf36bYIwxTMVKQOtU4VhD7hGrw==
  39. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/markdown-it/13.0.2/markdown-it.js#sha512-2LtYcLGnCbAWz9nDIrfG2pHFiFu9n+3oGecQlzLuYsLgen/oxiYscGWnDST9J9EZanlsQkDD0ZP2n/6peDuALQ==
  40. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha512-a+SUDuwNzXDvz4XrIcXHuCf089/iJAoN4lmrXJg18XnduKK6YlDHNRalv4yd1N40OKI80tFidF+rqTFKGPoWFQ==
  41. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/chroma-js/2.4.2/chroma.min.js#sha512-zInFF17qBFVvvvFpIfeBzo7Tj7+rQxLeTJDmbxjBz5/zIr89YVbTNelNhdTT+/DCrxoVzBeUPVFJsczKbB7sew==
  42. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/xterm/5.5.0/xterm.js#sha512-Gujw5GajF5is3nMoGv9X+tCMqePLL/60qvAv1LofUZTV9jK8ENbM9L+maGmOsNzuZaiuyc/fpph1KT9uR5w3CQ==
  43. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dexie/4.0.7/dexie.min.js#sha512-882VotT07mOQRzqIxsyxHzJX0XUaoeee3qXp4THg1A0KI0XFnWFAaLFQm0x6OW3pHSIipVZW+gzQ1w9b6uvkVw==
  44. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/i18next/23.11.5/i18next.min.js#sha512-3RSGkmT48HnO+hlmzGYDx5/w2LIBX0O5hSuYX6KWAxmvVlSjFgoxIaWa2tlMExheGvt3lLyxeTsXfpC47yb8CQ==
  45. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/i18next-http-backend/2.5.2/i18nextHttpBackend.min.js#sha512-bBb+wrGRTx4MvHpksYb1Iv5oJ1o8ineCqpc0cnTgdJQhuAFJJ93SEVXxUOCptvt0vAqYdjzWO5emorYUBt6Ceg==
  46. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/jquery-i18next/1.2.1/jquery-i18next.min.js#sha512-79RgNpOyaf8AvNEUdanuk1x6g53UPoB6Fh2uogMkOMGADBG6B0DCzxc+dDktXkVPg2rlxGvPeAFKoZxTycVooQ==
  47. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==
  48. // @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dialog-polyfill/0.5.6/dialog-polyfill.min.js#sha512-qUIG93zKzcLBVD5RGRbx2PBmbVRu+tJIl+EPLTus0z8I1AMru9sQYdlf6cBacSzYmZVncB9rcc8rYBnazqgrxA==
  49. // @require https://update.greatest.deepsurf.us/scripts/484742/1311040/i18nextChainedBackendjs.js#sha512-JYm2AqU8EvoEOnCucDItAsNtmGcjbxccOXjnwNFp87zdlyclpEephXrgR2sMlWj/gL4DCJUN3X0JhI1omaRO0A==
  50. // @require https://update.greatest.deepsurf.us/scripts/484743/1311041/i18next-localstorage-backendjs.js#sha512-kY1lU3DCvgzkWkOl47sIlmLKdgDcO4T3NYN6p/ET4oi3fnKO74sHUt1xYGtksIHXciKF8Jt+N4RDqG3CRoeYww==
  51. // @resource acwing_cpp_code_completer https://aowuucdn.oss-accelerate.aliyuncs.com/acwing_cpp_code_completer-0.0.11.json#sha512-DQVpao4qMMExToRdid0g/S0nbO/C9hwCECjI5aW8A0g7nvi8hEcD2Lw3QIqdJBV7haP15oJOocfwuiw7ryTO9w==
  52. // @resource wandboxlist https://wandbox.org/api/list.json
  53. // @resource xtermcss https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/xterm/5.5.0/xterm.min.css#sha512-XpXUuzg5afNt1bsgnrOesXP70TLH8tXYYK5sK+Y0UV+YBvJn9EfRFYWy4HT3TVDfH0nl1CO0lwOxIrt2gk9qjg==
  54. // @resource selectpagecss https://aowuucdn.oss-accelerate.aliyuncs.com/css/selectpage.css#sha512-cRXJfA2tEcAxHEKylJfxteY17N7j9fia3waahHOVnvl63uVZT9OQ7jjjpofZMVZ4JSX3BRET+mI8UvKnsXd3NA==
  55. // @resource dialogpolyfillcss https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dialog-polyfill/0.5.6/dialog-polyfill.min.css#sha512-J2+1q+RsZuJXabBfH1q/fgRr6jMy9By5SwVLk7bScEW7NFJkMUXxfeOyyxtDe6fsaJ4jsciexSlGrPYn9YbBIg==
  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 Atcoder Better!
  63. // @name:en Atcoder Better!
  64. // @name:de Atcoder Better!
  65. // @name:fr Atcoder Better!
  66. // @name:ko Atcoder Better!
  67. // @name:pt Atcoder Better!
  68. // @name:ja Atcoder Better!
  69. // @name:es Atcoder Better!
  70. // @name:it Atcoder Better!
  71. // @name:hi Atcoder Better!
  72. // @description 一个适用于 AtCoder 的 Tampermonkey 脚本,增强功能与界面。
  73. // @description:zh-TW 一個適用於 AtCoder 的 Tampermonkey 腳本,增強功能與界面。
  74. // @description:en A Tampermonkey script for AtCoder that enhances functionality and interface.
  75. // @description:de Ein Tampermonkey-Skript für AtCoder, das Funktionalität und Benutzeroberfläche verbessert.
  76. // @description:fr Un script Tampermonkey pour AtCoder qui améliore les fonctionnalités et l'interface.
  77. // @description:ko AtCoder를 위한 Tampermonkey 스크립트로 기능과 인터페이스를 개선합니다.
  78. // @description:pt Um script Tampermonkey para AtCoder que aprimora a funcionalidade e a interface.
  79. // @description:ja AtCoder用のTampermonkeyスクリプトで機能とインターフェースを強化します。
  80. // @description:es Un script Tampermonkey para AtCoder que mejora la funcionalidad y la interfaz.
  81. // @description:it Uno script Tampermonkey per AtCoder che migliora la funzionalità e l'interfaccia.
  82. // @description:hi AtCoder के लिए एक 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?} Atcoder的CSRF令牌 */
  122. at_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: false,
  163. /** @type {boolean?} 是否是acmsguru页面 */
  164. is_acmsguru: false,
  165. /** @type {boolean?} 是否是旧版LaTeX页面 */
  166. is_oldLatex: false,
  167. /** @type {boolean?} 是否是题目集页面 */
  168. is_contest: undefined,
  169. /** @type {boolean?} 是否是题目页面 */
  170. is_problem: undefined,
  171. /** @type {boolean?} 是否是完整的问题集页面 */
  172. is_completeProblemset: false,
  173. /** @type {boolean?} 是否是问题集中的问题页面 */
  174. is_problemset_problem: false,
  175. /** @type {boolean?} 是否是问题集页面 */
  176. is_problemset: false,
  177. /** @type {boolean?} 是否是Codeforces排名页面 */
  178. is_cfStandings: false,
  179. /** @type {boolean?} 是否是提交页面 */
  180. is_submitPage: false,
  181. /** @type {boolean?} 是否是代码状态页面 */
  182. is_statePage: false,
  183. /** @type {boolean?} 是否是提交记录页面 */
  184. is_submissions: false,
  185. /** @type {boolean?} 是否是主页 */
  186. is_homepage: undefined,
  187. /** @type {boolean?} 是否选择的是英语页面 */
  188. isEnglishLanguage: undefined,
  189. /** @type {boolean?} 是否是题解页面 */
  190. isEditorial: undefined,
  191. };
  192.  
  193. /**
  194. * @namespace localization
  195. * @desc 本地化设置。
  196. * @memberof OJBetter
  197. */
  198. OJBetter.localization = {
  199. /** @type {string?} 网站语言 */
  200. websiteLang: undefined,
  201. /** @type {string?} 脚本语言 */
  202. scriptLang: undefined
  203. };
  204.  
  205. /**
  206. * @namespace translation
  207. * @desc 翻译设置。
  208. * @memberof OJBetter
  209. */
  210. OJBetter.translation = {
  211. /** @type {string?} 翻译服务选择 */
  212. choice: undefined,
  213. /** @type {string?} 目标语言 */
  214. targetLang: undefined,
  215. comment: {
  216. /** @type {string?} 评论翻译服务选择 */
  217. choice: undefined,
  218. /** @type {string?} 评论翻译模式 */
  219. transMode: undefined
  220. },
  221. auto: {
  222. /** @type {boolean?} 自动翻译开关 */
  223. enabled: undefined,
  224. /** @type {number?} 短文本长度限制 */
  225. shortTextLength: undefined,
  226. mixTrans: {
  227. /** @type {boolean?} 混合翻译开关 */
  228. enabled: undefined,
  229. /** @type {Array?} 混合翻译服务列表 */
  230. servers: undefined
  231. }
  232. },
  233. memory: {
  234. /** @type {boolean?} 翻译记忆开关 */
  235. enabled: undefined,
  236. /** @type {Object?} 翻译记忆树 */
  237. ttTree: undefined
  238. },
  239. /** @type {string?} 重翻译时的行为 */
  240. retransAction: undefined,
  241. /** @type {number?} 等待时间 */
  242. waitTime: undefined,
  243. /** @type {boolean?} 替换符 */
  244. replaceSymbol: undefined,
  245. /** @type {boolean?} 过滤文本中的*号 */
  246. filterTextWithoutEmphasis: undefined
  247. };
  248.  
  249. /**
  250. * @namespace clist
  251. * @desc Clist相关设置。
  252. * @memberof OJBetter
  253. */
  254. OJBetter.clist = {
  255. enabled: {
  256. /** @type {boolean?} 比赛页面开关 */
  257. contest: undefined,
  258. /** @type {boolean?} 问题页面开关 */
  259. problem: undefined,
  260. /** @type {boolean?} 问题集页面开关 */
  261. problemset: undefined
  262. },
  263. /** @type {boolean?} Rating数据防剧透 */
  264. ratingHidden: undefined,
  265. /** @type {string?} Clist key */
  266. authorization: undefined
  267. };
  268.  
  269. /**
  270. * @namespace monaco
  271. * @desc Monaco编辑器配置。
  272. * @memberof OJBetter
  273. */
  274. OJBetter.monaco = {
  275. /** @type {boolean?} 在问题页面上启用Monaco编辑器 */
  276. enableOnProblemPage: undefined,
  277. /** @type {boolean?} 美化pre代码块 */
  278. beautifyPreBlocks: undefined,
  279. /** @type {boolean} Monaco编辑器加载完成标志 */
  280. loaderOnload: false,
  281. lsp: {
  282. /** @type {Array?} LSP套接字数组 */
  283. socket: [],
  284. /** @type {boolean?} 是否启用LSP */
  285. enabled: undefined,
  286. /** @type {string?} 工作路径 */
  287. workUri: undefined,
  288. /** @type {string?} 套接字URL */
  289. socketUrl: undefined
  290. },
  291. complet: {
  292. /** @type {boolean?} 是否启用C++代码补全模板 */
  293. cppCodeTemplate: undefined,
  294. /** @type {Object?} 自定义配置 */
  295. customConfig: undefined
  296. },
  297. /** @type {Object?} Monaco编辑器实例 */
  298. editor: null,
  299. /** @type {string?} 在线编译器选择 */
  300. onlineCompilerChoice: undefined,
  301. /** @type {string?} 记忆编译器语言选择 */
  302. compilerSelection: undefined,
  303. /** @type {string?} 当前选择的语言 */
  304. nowLangSelect: undefined,
  305. setting: {
  306. /** @type {Array?} 语言设置数组 */
  307. language: [],
  308. /** @type {string?} 位置 */
  309. position: undefined,
  310. /** @type {boolean} 位置初始化标志 */
  311. position_initialized: false,
  312. /** @type {number?} 字体大小 */
  313. fontsize: undefined,
  314. /** @type {boolean?} 鼠标滚动锁定 */
  315. alwaysConsumeMouseWheel: undefined,
  316. /** @type {boolean?} 提交代码二次确认 */
  317. isCodeSubmitDoubleConfirm: undefined,
  318. /** @type {boolean?} 测试通过后自动提交 */
  319. autoSubmitAfterPass: undefined,
  320. /** @type {string?} 提交按钮位置 */
  321. submitButtonPosition: undefined,
  322. /** @type {boolean?} 自动保存代码 */
  323. autoMemoryCode: undefined
  324. }
  325. };
  326.  
  327. /**
  328. * @namespace deepl
  329. * @desc DeepL翻译服务配置。
  330. * @memberof OJBetter
  331. */
  332. OJBetter.deepl = {
  333. /** @type {Object?} DeepL配置对象 */
  334. configs: undefined,
  335. config: {
  336. /** @type {string?} 类型 */
  337. type: undefined,
  338. /** @type {string?} 名称 */
  339. name: undefined,
  340. /** @type {string?} API类型 */
  341. apiGenre: undefined,
  342. /** @type {string?} API密钥 */
  343. key: undefined,
  344. /** @type {string?} 代理 */
  345. proxy: undefined,
  346. /** @type {Object?} 额外请求头 */
  347. header: undefined,
  348. /** @type {Object?} 额外请求数据 */
  349. data: undefined,
  350. quota: {
  351. /** @type {string?} 余额URL */
  352. url: undefined,
  353. /** @type {string?} 余额请求方法 */
  354. method: undefined,
  355. /** @type {Object?} 余额请求头 */
  356. header: undefined,
  357. /** @type {Object?} 余额请求数据 */
  358. data: undefined,
  359. /** @type {number?} 剩余配额 */
  360. surplus: undefined
  361. }
  362. },
  363. /** @type {boolean?} 启用重点保护 */
  364. enableEmphasisProtection: undefined,
  365. /** @type {boolean?} 启用链接保护 */
  366. enableLinkProtection: undefined
  367. };
  368.  
  369. /**
  370. * @namespace chatgpt
  371. * @desc ChatGPT服务配置。
  372. * @memberof OJBetter
  373. */
  374. OJBetter.chatgpt = {
  375. /** @type {Object?} ChatGPT配置对象 */
  376. configs: undefined,
  377. config: {
  378. /** @type {string?} 名称 */
  379. name: undefined,
  380. /** @type {string?} 模型 */
  381. model: undefined,
  382. /** @type {string?} API密钥 */
  383. key: undefined,
  384. /** @type {string?} 代理 */
  385. proxy: undefined,
  386. /** @type {Object?} 额外请求头 */
  387. header: undefined,
  388. /** @type {Object?} 额外请求数据 */
  389. data: undefined,
  390. quota: {
  391. /** @type {string?} 余额URL */
  392. url: undefined,
  393. /** @type {string?} 余额请求方法 */
  394. method: undefined,
  395. /** @type {Object?} 余额请求头 */
  396. header: undefined,
  397. /** @type {Object?} 余额请求数据 */
  398. data: undefined,
  399. /** @type {number?} 剩余配额 */
  400. surplus: undefined
  401. }
  402. },
  403. /** @type {boolean?} 是否为流式传输 */
  404. isStream: undefined,
  405. /** @type {string?} 是否使用自定义Prompt */
  406. customPrompt: undefined,
  407. /** @type {boolean?} 是否作为系统Prompt */
  408. asSystemPrompt: undefined
  409. };
  410.  
  411. /**
  412. * @namespace preference
  413. * @desc 偏好设置
  414. * @memberof OJBetter
  415. */
  416. OJBetter.preference = {
  417. /** @type {boolean?} 是否显示加载动画 */
  418. showLoading: undefined,
  419. /** @type {boolean?} 是否显示悬停目标区域 */
  420. hoverTargetAreaDisplay: undefined,
  421. /** @type {string?} 按钮图标大小 */
  422. iconButtonSize: undefined,
  423. };
  424.  
  425. /**
  426. * @namespace dev
  427. * @desc 维护
  428. * @memberof OJBetter
  429. */
  430. OJBetter.dev = {
  431. /** @type {boolean?} 是否显示规则标记 */
  432. isRuleMarkingEnabled: undefined,
  433. };
  434.  
  435. /**
  436. * @namespace about
  437. * @desc 关于页信息
  438. * @memberof OJBetter
  439. */
  440. OJBetter.about = {
  441. /** @type {string?} 更新通道 */
  442. updateChannel: undefined,
  443. /** @type {string?} 更新源 */
  444. updateSource: undefined
  445. };
  446.  
  447. /**
  448. * @namespace supportList
  449. * @desc 支持列表
  450. * @memberof OJBetter
  451. */
  452. OJBetter.supportList = {
  453. /** @type {object} 翻译支持列表和对应语言代码*/
  454. translationSupport: {
  455. 'deepl': { 'zh': 'ZH', 'de': 'DE', 'fr': 'FR', 'ko': 'KO', 'pt': 'PT', 'ja': 'JA', 'es': 'ES', 'it': 'IT' },
  456. 'iflyrec': { 'zh': '1' },
  457. 'youdao': { 'zh': 'zh-CHS', 'zh-Hant': 'zh-CHT', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
  458. 'google': { 'zh': 'zh-CN', 'zh-Hant': 'zh-TW', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
  459. 'caiyun': { 'zh': 'auto2zh', 'ja': 'auto2ja', 'ko': 'auto2ko', 'es': 'auto2es', 'fr': 'auto2fr' },
  460. 'openai': { 'zh': 'Chinese', 'zh-Hant': 'Traditional Chinese', 'de': 'German', 'fr': 'French', 'ko': 'Korean', 'pt': 'Portuguese', 'ja': 'Japanese', 'es': 'Spanish', 'it': 'Italian', 'hi': 'Hindi' }
  461. },
  462. /** @type {object} 更新源支持列表*/
  463. updateSourceSupportList: {
  464. 'greasyfork': {
  465. 'release': true,
  466. 'dev': false
  467. },
  468. 'github': {
  469. 'release': true,
  470. 'dev': true
  471. },
  472. 'aliyunoss': {
  473. 'release': true,
  474. 'dev': true
  475. }
  476. }
  477. }
  478.  
  479. // ------------------------------
  480. // 一些工具函数
  481. // ------------------------------
  482.  
  483. /**
  484. * 延迟函数
  485. * @param {number} ms 延迟时间(毫秒)
  486. * @returns {Promise<void>}
  487. */
  488. function OJB_delay(ms) {
  489. return new Promise(resolve => setTimeout(resolve, ms));
  490. }
  491.  
  492. /**
  493. * 等待直到指定的条件函数返回true。
  494. *
  495. * @param {() => boolean} conditionCheck 一个无参数的函数,用于检查条件是否满足。当函数返回true时,表示条件已满足。
  496. * @param {number} [interval=100] 检查条件的间隔时间,单位为毫秒。默认为100毫秒。
  497. * @returns {Promise<void>} 返回一个Promise,在条件满足时解决。
  498. */
  499. async function OJB_waitUntilTrue(conditionCheck, interval = 100) {
  500. return new Promise((resolve) => {
  501. const checkCondition = async () => {
  502. if (conditionCheck()) {
  503. resolve();
  504. } else {
  505. await OJB_delay(interval);
  506. checkCondition();
  507. }
  508. };
  509. checkCondition();
  510. });
  511. }
  512.  
  513. /**
  514. * 动态加载JavaScript库并返回一个Promise,该Promise在脚本加载完成后解决。
  515. *
  516. * @param {string} url - 要加载的JavaScript库的URL地址。
  517. * @param {string} [expectedHash] - 可选的Base64编码的SHA-512哈希值,用于校验脚本内容。格式为 "sha512-<Base64编码的哈希值>"。
  518. * @returns {Promise<void>} 一个Promise,它在脚本加载并执行完成后解决。
  519. */
  520. async function OJB_LoadJS(url, expectedHash) {
  521. /**
  522. * 计算给定数据的SHA-512哈希值,并将其转换为十六进制字符串。
  523. *
  524. * @param {string} data - 要计算哈希值的数据。
  525. * @returns {Promise<string>} 一个Promise,它解析为数据的SHA-512哈希值的十六进制字符串。
  526. */
  527. const calculateHash = async (data) => {
  528. const encoder = new TextEncoder();
  529. const dataBuffer = encoder.encode(data);
  530. const hashBuffer = await crypto.subtle.digest('SHA-512', dataBuffer);
  531. const hashArray = Array.from(new Uint8Array(hashBuffer));
  532. return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  533. };
  534.  
  535. /**
  536. * 将Base64编码的字符串转换为十六进制字符串。
  537. *
  538. * @param {string} base64 - Base64编码的字符串。
  539. * @returns {string} 转换后的十六进制字符串。
  540. */
  541. const base64ToHex = (base64) => {
  542. const binaryString = atob(base64);
  543. const byteArray = new Uint8Array(binaryString.length);
  544. for (let i = 0; i < binaryString.length; i++) {
  545. byteArray[i] = binaryString.charCodeAt(i);
  546. }
  547. return Array.from(byteArray).map(b => b.toString(16).padStart(2, '0')).join('');
  548. };
  549.  
  550. try {
  551. const response = await fetch(url);
  552. if (!response.ok) throw new Error(`Failed to fetch script: ${response.statusText}`);
  553. const scriptContent = await response.text();
  554.  
  555. if (expectedHash) {
  556. // 去掉前缀 "sha512-"
  557. const base64Hash = expectedHash.replace(/^sha512-/, '');
  558. const actualHash = await calculateHash(scriptContent);
  559. const expectedHashHex = base64ToHex(base64Hash);
  560. if (actualHash !== expectedHashHex) throw new Error('SHA-512 hash mismatch');
  561. }
  562.  
  563. const scriptElement = document.createElement("script");
  564. scriptElement.textContent = scriptContent;
  565. document.head.prepend(scriptElement);
  566.  
  567. return Promise.resolve();
  568. } catch (error) {
  569. return Promise.reject(error);
  570. }
  571. }
  572.  
  573. /**
  574. * 安全地创建JQuery对象
  575. * @description 通过字符串创建JQuery对象时,如果字符串以空格开头,在某些Jquery版本中会发生错误,过滤空格以安全的创建元素。
  576. * @param {string} string - 字符串。
  577. * @returns {JQuery} JQuery对象
  578. */
  579. const OJB_safeCreateJQElement = function (string) {
  580. return $(string.replace(/^\s+/, ""));
  581. }
  582.  
  583.  
  584. /**
  585. * 将数字或者字符串解析为数字。
  586. * @memberof OJBetter.common
  587. * @param {string} val 要解析的字符串
  588. * @param {boolean} [strict=false] 是否进行严格类型检查
  589. * @returns {number} 解析结果
  590. * @throws {Error} 如果解析失败,则抛出错误
  591. */
  592. const OJB_parseNumber = (val, strict = false) => {
  593. const num = Number(val);
  594. if (isNaN(num) || (strict && val.toString() !== num.toString())) {
  595. throw new Error('Invalid number');
  596. }
  597. return num;
  598. };
  599.  
  600. /**
  601. * 将字符串解析为布尔值
  602. * @param {string} val - 要解析的字符串
  603. * @param {boolean} strict - 是否进行严格类型检查
  604. * @returns {boolean} - 解析结果
  605. * @throws {Error} - 如果解析失败,则抛出错误
  606. */
  607. const OJB_parseBoolean = (val, strict) => {
  608. if (strict) {
  609. if (val === true || val === false) return val;
  610. throw new Error('Invalid boolean');
  611. }
  612. return val === 'true' ? true : val === 'false' ? false : val;
  613. };
  614.  
  615. /**
  616. * 将字符串解析为对象
  617. * @param {string} val - 要解析的字符串
  618. * @returns {Object} - 解析结果
  619. * @throws {Error} - 如果解析失败,则抛出错误
  620. */
  621. const OJB_parseObject = val => {
  622. try {
  623. return JSON.parse(val);
  624. } catch {
  625. throw new Error('Invalid JSON');
  626. }
  627. };
  628.  
  629. /**
  630. * 将字符串解析为键值对数组
  631. * @param {string} val - 要解析的字符串
  632. * @returns {Object[]} - 解析结果
  633. * @throws {Error} - 如果解析失败,则抛出错误
  634. */
  635. const OJB_parseLinePairArray = val => {
  636. if (typeof val !== 'string' || val.trim() === '') return [];
  637. return val.split("\n").filter(line => line.trim() !== '').map(line => {
  638. const indexOfFirstColon = line.indexOf(":");
  639. if (indexOfFirstColon === -1) throw new Error('Invalid LinePairArray format: ":" is missing');
  640. const key = line.substring(0, indexOfFirstColon).trim();
  641. const value = line.substring(indexOfFirstColon + 1).trim();
  642. return { [key]: value };
  643. });
  644. };
  645.  
  646. /**
  647. * 移除文本中的HTML标签
  648. * @param {string} text - 包含HTML标签的文本
  649. * @returns {string} - 移除HTML标签后的文本
  650. */
  651. const OJB_removeHTMLTags = function (text) {
  652. return text.replace(/<\/?[a-zA-Z]+("[^"]*"|'[^']*'|[^'">])*>/g, '');
  653. }
  654.  
  655. /**
  656. * 获取对象中指定路径表达式的值
  657. * @param {Object} obj - 要计算的对象
  658. * @param {string} pathOrExpression - 要计算的路径表达式
  659. * @returns {any} - 计算结果
  660. * @example
  661. * const obj = {
  662. * "a": {
  663. * "b": 1
  664. * },
  665. * "c": 2
  666. * };
  667. * OJB_evaluatePathOrExpression(obj, "a.b"); // 1
  668. * OJB_evaluatePathOrExpression(obj, "a.b + c"); // 3
  669. * OJB_evaluatePathOrExpression(obj, "a.b + a.c"); // 1
  670. */
  671. function OJB_evaluatePathOrExpression(obj, pathOrExpression) {
  672. const hasOperator = /[\+\-\*\/]/.test(pathOrExpression);
  673. const getPathValue = (obj, path) => {
  674. return path.split('.').reduce((acc, part) => {
  675. return acc !== undefined && acc !== null && acc.hasOwnProperty(part) ? acc[part] : undefined;
  676. }, obj);
  677. };
  678. const evaluateExpression = (obj, expression) => {
  679. const tokens = expression.split(/([\+\-\*\/])/).map(token => token.trim());
  680. const values = tokens.map(token => {
  681. if (/[\+\-\*\/]/.test(token)) {
  682. return token;
  683. } else {
  684. const value = getPathValue(obj, token);
  685. return value !== undefined ? value : 0;
  686. }
  687. });
  688. const evaluatedExpression = values.join(' ');
  689. try {
  690. return Function(`'use strict'; return (${evaluatedExpression});`)();
  691. } catch (e) {
  692. console.error('Expression evaluation error:', e);
  693. return undefined;
  694. }
  695. };
  696. return hasOperator ? evaluateExpression(obj, pathOrExpression) : getPathValue(obj, pathOrExpression);
  697. }
  698.  
  699. /**
  700. * 获取 GM 存储的值并根据类型进行处理
  701. * @param {string} key - 要检索的值的键。
  702. * @param {any} defaultValue - 如果值未找到,则返回的默认值。
  703. * @param {Object} [options={}] - 配置选项对象。
  704. * @param {string} [options.type='string'] - 期望的值的类型。可选值:'string', 'number', 'boolean', 'object', 'array', 'linePairArray'。
  705. * @param {boolean} [options.strict=false] - 用于数字和布尔类型,表示是否进行严格类型检查。
  706. * @param {string} [options.pathOrExpression=''] - 用于对象或数组类型,表示路径表达式或获取元素的索引。
  707. * @returns {any} - 检索到的值。
  708. */
  709. const OJB_getGMValue = (key, defaultValue, { type = 'string', strict = false, pathOrExpression = '' } = {}) => {
  710. let value = GM_getValue(key);
  711. if (value === undefined || value === null || value === "") {
  712. GM_setValue?.(key, defaultValue);
  713. return defaultValue;
  714. }
  715.  
  716. const parsers = {
  717. string: val => val,
  718. number: (val) => OJB_parseNumber(val, strict),
  719. boolean: (val) => OJB_parseBoolean(val, strict),
  720. object: OJB_parseObject,
  721. array: OJB_parseObject,
  722. linePairArray: OJB_parseLinePairArray
  723. };
  724.  
  725. if (!(type in parsers)) {
  726. console.error(`Unsupported type: ${type}`);
  727. return defaultValue;
  728. }
  729.  
  730. try {
  731. value = parsers[type](value);
  732. } catch (e) {
  733. console.error('Error:', e.message);
  734. return defaultValue;
  735. }
  736.  
  737. // The pathOrExpression processing is not applicable to linePairArray type
  738. if ((type === 'object' || type === 'array') && pathOrExpression) {
  739. const evaluated = OJB_evaluatePathOrExpression(value, pathOrExpression);
  740. if (evaluated === undefined) {
  741. console.error('Path or expression evaluation returned undefined');
  742. return defaultValue;
  743. }
  744. value = evaluated;
  745. }
  746.  
  747. return value;
  748. };
  749.  
  750. /**
  751. * 版本号比较方法
  752. * @param {string} version1 版本号1
  753. * @param {string} version2 版本号2
  754. * @returns {number} -1: version1 < version2, 0: version1 = version2, 1: version1 > version2
  755. */
  756. const OJB_compareVersions = function (version1 = "0", version2 = "0") {
  757. const v1Array = version1.split(".").map(Number);
  758. const v2Array = version2.split(".").map(Number);
  759. const length = Math.max(v1Array.length, v2Array.length);
  760. for (let i = 0; i < length; i++) {
  761. const diff = (v1Array[i] || 0) - (v2Array[i] || 0);
  762. if (diff) return Math.sign(diff);
  763. }
  764. return 0;
  765. }
  766.  
  767. /**
  768. * 获取上一个主版本号
  769. * @param {string} currentVersion 当前版本号
  770. * @returns {string} 上一个主版本号
  771. */
  772. const OJB_getPreviousVersion = function (currentVersion) {
  773. const versionArray = currentVersion.split(".").map(Number);
  774. let lastNonZeroIndex = versionArray.length - 1;
  775. while (lastNonZeroIndex >= 0 && versionArray[lastNonZeroIndex] === 0) {
  776. lastNonZeroIndex--;
  777. }
  778. if (lastNonZeroIndex >= 0) {
  779. versionArray[lastNonZeroIndex]--;
  780. for (let i = lastNonZeroIndex + 1; i < versionArray.length; i++) {
  781. versionArray[i] = 0;
  782. }
  783. }
  784. return versionArray.join(".");
  785. };
  786.  
  787. /**
  788. * 在指定根节点下观察指定选择器的元素,当元素存在时,执行回调函数
  789. * @param {Object} options - 配置对象
  790. * @param {string} options.selector - CSS选择器文本
  791. * @param {Function} options.callback - 回调函数,接收变动的节点作为参数
  792. * @param {Boolean} [options.triggerOnExist=true] - 如果为true,元素已存在时立即触发一次回调
  793. * @param {Element} [options.root=document.body] - 在哪个根节点下监听变化
  794. * @param {Boolean} [options.subtree=false] - 是否监听子树变化(即非直接子元素)
  795. */
  796. function OJB_observeElement({
  797. selector,
  798. callback,
  799. triggerOnExist = true,
  800. root = document.body,
  801. subtree = false
  802. }) {
  803. // 尝试获取选择器指定的元素
  804. const targetNode = root.querySelector(selector);
  805.  
  806. if (targetNode) {
  807. // 如果元素已存在,直接开始观察
  808. observeAndReport(targetNode, callback);
  809. // 如果triggerOnExist为true,则立即触发一次回调
  810. if (triggerOnExist) {
  811. callback(targetNode);
  812. }
  813. } else {
  814. // 如果元素不存在,监听DOM变化直到该元素被添加
  815. const observer = new MutationObserver((mutations) => {
  816. mutations.forEach((mutation) => {
  817. mutation.addedNodes.forEach((node) => {
  818. if (node.nodeType === Node.ELEMENT_NODE && node.matches(selector)) {
  819. observeAndReport(node, callback);
  820. if (triggerOnExist) {
  821. callback(node);
  822. }
  823. observer.disconnect(); // 停止监听
  824. }
  825. });
  826. });
  827. });
  828.  
  829. observer.observe(root, { childList: true, subtree, attributes: false });
  830. }
  831.  
  832. function observeAndReport(node, callback) {
  833. const childObserver = new MutationObserver((mutations) => {
  834. mutations.forEach((mutation) => {
  835. mutation.addedNodes.forEach((addedNode) => {
  836. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  837. callback(addedNode); // 执行回调函数
  838. }
  839. });
  840. });
  841. });
  842.  
  843. childObserver.observe(node, { childList: true, subtree: true, attributes: false });
  844. }
  845. }
  846.  
  847. /**
  848. * 初始化全局变量
  849. */
  850. async function initVar() {
  851. const { hostname, href } = window.location;
  852. OJBetter.state.formatName = (() => OJBetter.state.name
  853. .toLowerCase()
  854. .replace(/\s+/g, '-')
  855. .replace(/[^a-z0-9-]/g, ''))();
  856. OJBetter.state.lastAnnounceVer = OJB_getGMValue("lastAnnounceVer", "0");
  857. OJBetter.state.lastReadAnnounceVer = OJB_getGMValue("lastReadAnnounceVer", "0");
  858. OJBetter.typeOfPage.is_contest = /\/contests\/[^\/]+\/tasks\/?$/.test(href);
  859. OJBetter.typeOfPage.is_problem = href.includes('/tasks/');
  860. OJBetter.typeOfPage.is_homepage = (href === 'https://atcoder.jp/' || href === 'https://atcoder.jp/?lang=ja');
  861. OJBetter.typeOfPage.isEnglishLanguage = $('meta[http-equiv="Content-Language"]').attr('content') === 'en';
  862. OJBetter.typeOfPage.isEditorial = href.includes("editorial");
  863. OJBetter.localization.websiteLang = OJB_getGMValue("localizationLanguage", "zh");
  864. OJBetter.localization.scriptLang = OJB_getGMValue("scriptL10nLanguage", "zh");
  865. OJBetter.basic.renderPerfOpt = OJB_getGMValue("renderPerfOpt", false);
  866. OJBetter.basic.selectElementPerfOpt = OJB_getGMValue("selectElementPerfOpt", true);
  867. OJBetter.basic.commentPaging = OJB_getGMValue("commentPaging", true);
  868. OJBetter.basic.showJumpToLuogu = OJB_getGMValue("showJumpToLuogu", true);
  869. OJBetter.basic.showCF2vjudge = OJB_getGMValue("showCF2vjudge", true);
  870. OJBetter.basic.standingsRecolor = OJB_getGMValue("standingsRecolor", true);
  871. OJBetter.state.notWaiteLoaded = OJB_getGMValue("notWaiteLoaded", false);
  872. OJBetter.translation.targetLang = OJB_getGMValue("transTargetLang", "zh");
  873. OJBetter.translation.choice = OJB_getGMValue("translation", "deepl");
  874. OJBetter.translation.comment.transMode = OJB_getGMValue("commentTranslationMode", "0");
  875. OJBetter.translation.comment.choice = OJB_getGMValue("commentTranslationChoice", "0");
  876. OJBetter.translation.memory.enabled = OJB_getGMValue("memoryTranslateHistory", true);
  877. OJBetter.translation.auto.enabled = OJB_getGMValue("autoTranslation", false);
  878. OJBetter.translation.auto.shortTextLength = OJB_getGMValue("shortTextLength", "2000");
  879. OJBetter.translation.retransAction = OJB_getGMValue("retransAction", "0");
  880. OJBetter.translation.waitTime = OJB_getGMValue("transWaitTime", "200");
  881. OJBetter.translation.auto.mixTrans.enabled = OJB_getGMValue("allowMixTrans", true);
  882. OJBetter.translation.auto.mixTrans.servers = OJB_getGMValue("mixedTranslation", ['deepl', 'iflyrec', 'youdao', 'caiyun']);
  883. OJBetter.common.taskQueue = new TaskQueue();
  884. OJBetter.translation.replaceSymbol = OJB_getGMValue("replaceSymbol", "2");
  885. OJBetter.translation.filterTextWithoutEmphasis = OJB_getGMValue("filterTextWithoutEmphasis", false);
  886. OJBetter.clist.enabled.contest = OJB_getGMValue("showClistRating_contest", false);
  887. OJBetter.clist.enabled.problem = OJB_getGMValue("showClistRating_problem", false);
  888. OJBetter.clist.enabled.problemset = OJB_getGMValue("showClistRating_problemset", false);
  889. OJBetter.clist.ratingHidden = OJB_getGMValue("RatingHidden", false);
  890. OJBetter.clist.authorization = OJB_getGMValue("clist_Authorization", "");
  891. //deepl
  892. OJBetter.deepl.config.type = OJB_getGMValue("deepl_type", "free");
  893. OJBetter.deepl.configs = OJB_getGMValue("deepl_config", {
  894. "choice": "",
  895. "configurations": []
  896. });
  897. if (OJBetter.deepl.configs.choice !== "" && OJBetter.deepl.configs.configurations.length !== 0) {
  898. const choice = OJBetter.deepl.configs.choice;
  899. const configuration = OJBetter.deepl.configs.configurations.find(obj => obj.name === choice);;
  900. if (configuration == undefined) {
  901. let existingConfig = GM_getValue('deepl_config');
  902. existingConfig.choice = "";
  903. GM_setValue('deepl_config', existingConfig);
  904. location.reload();
  905. }
  906. OJBetter.deepl.config.name = configuration.name;
  907. OJBetter.deepl.config.apiGenre = configuration.apiGenre;
  908. OJBetter.deepl.config.key = configuration.key;
  909. OJBetter.deepl.config.proxy = configuration.proxy;
  910. OJBetter.deepl.config.header = OJB_parseLinePairArray(configuration._header);
  911. OJBetter.deepl.config.data = OJB_parseLinePairArray(configuration._data);
  912. OJBetter.deepl.config.quota.url = configuration.quota_url;
  913. OJBetter.deepl.config.quota.method = configuration.quota_method;
  914. OJBetter.deepl.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
  915. OJBetter.deepl.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
  916. OJBetter.deepl.config.quota.surplus = configuration.quota_surplus;
  917. }
  918. OJBetter.deepl.enableEmphasisProtection = OJB_getGMValue("enableEmphasisProtection", true);
  919. OJBetter.deepl.enableLinkProtection = OJB_getGMValue("enableLinkProtection", true);
  920. //openai
  921. OJBetter.chatgpt.isStream = OJB_getGMValue("openai_isStream", true);
  922. OJBetter.chatgpt.customPrompt = OJB_getGMValue("openai_customPrompt", '');
  923. OJBetter.chatgpt.asSystemPrompt = OJB_getGMValue("openai_asSystemPrompt", false);
  924. OJBetter.chatgpt.configs = OJB_getGMValue("chatgpt_config", {
  925. "choice": "",
  926. "configurations": []
  927. });
  928. if (OJBetter.chatgpt.configs.choice !== "" && OJBetter.chatgpt.configs.configurations.length !== 0) {
  929. const choice = OJBetter.chatgpt.configs.choice;
  930. const configuration = OJBetter.chatgpt.configs.configurations.find(obj => obj.name === choice);;
  931. if (configuration == undefined) {
  932. let existingConfig = GM_getValue('chatgpt_config');
  933. existingConfig.choice = "";
  934. GM_setValue('chatgpt_config', existingConfig);
  935. location.reload();
  936. }
  937. OJBetter.chatgpt.config.name = configuration.name;
  938. OJBetter.chatgpt.config.model = configuration.model;
  939. OJBetter.chatgpt.config.key = configuration.key;
  940. OJBetter.chatgpt.config.proxy = configuration.proxy;
  941. OJBetter.chatgpt.config.header = OJB_parseLinePairArray(configuration._header);
  942. OJBetter.chatgpt.config.data = OJB_parseLinePairArray(configuration._data);
  943. OJBetter.chatgpt.config.quota.url = configuration.quota_url;
  944. OJBetter.chatgpt.config.quota.method = configuration.quota_method;
  945. OJBetter.chatgpt.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
  946. OJBetter.chatgpt.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
  947. OJBetter.chatgpt.config.quota.surplus = configuration.quota_surplus;
  948. }
  949. // 编辑器
  950. // if (!OJBetter.typeOfPage.is_mSite) OJBetter.common.cf_csrf_token = Codeforces.getCsrfToken();
  951. // else OJBetter.common.cf_csrf_token = "";
  952. OJBetter.common.at_csrf_token = csrfToken;
  953. // OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "61");
  954. OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "5001");
  955. OJBetter.monaco.setting.fontsize = OJB_getGMValue("editorFontSize", "15");
  956. OJBetter.monaco.enableOnProblemPage = OJB_getGMValue("problemPageCodeEditor", true);
  957. OJBetter.monaco.beautifyPreBlocks = OJB_getGMValue("beautifyPreBlocks", true);
  958. OJBetter.monaco.complet.cppCodeTemplate = OJB_getGMValue("cppCodeTemplateComplete", true);
  959. OJBetter.monaco.onlineCompilerChoice = OJB_getGMValue("onlineCompilerChoice", "official");
  960. OJBetter.monaco.setting.isCodeSubmitDoubleConfirm = OJB_getGMValue("isCodeSubmitConfirm", true);
  961. OJBetter.monaco.setting.autoSubmitAfterPass = OJB_getGMValue("autoSubmitAfterPass", false);
  962. OJBetter.monaco.setting.alwaysConsumeMouseWheel = OJB_getGMValue("alwaysConsumeMouseWheel", true);
  963. OJBetter.monaco.setting.submitButtonPosition = OJB_getGMValue("submitButtonPosition", "bottom");
  964. OJBetter.monaco.setting.autoMemoryCode = OJB_getGMValue("autoMemoryCode", true);
  965. //自定义补全
  966. OJBetter.monaco.complet.customConfig = OJB_getGMValue("Complet_config", {
  967. "choice": -1,
  968. "configurations": []
  969. });
  970. // monaco
  971. OJBetter.monaco.lsp.enabled = OJB_getGMValue("useLSP", false);
  972. OJBetter.monaco.setting.position = OJB_getGMValue("monacoEditor_position", "initial");
  973. OJBetter.monaco.lsp.workUri = OJB_getGMValue("OJBetter_Bridge_WorkUri", "C:/OJBetter_Bridge");
  974. OJBetter.monaco.lsp.socketUrl = OJB_getGMValue("OJBetter_Bridge_SocketUrl", "ws://127.0.0.1:2323/");
  975. OJBetter.preference.showLoading = OJB_getGMValue("showLoading", true);
  976. OJBetter.preference.hoverTargetAreaDisplay = OJB_getGMValue("hoverTargetAreaDisplay", false);
  977. OJBetter.basic.expandFoldingblocks = OJB_getGMValue("expandFoldingblocks", true);
  978. OJBetter.preference.iconButtonSize = OJB_getGMValue("iconButtonSize", "16");
  979. OJBetter.dev.isRuleMarkingEnabled = OJB_getGMValue("isRuleMarkingEnabled", false);
  980. OJBetter.about.updateChannel = OJB_getGMValue("updateChannel", "release");
  981. OJBetter.about.updateSource = OJB_getGMValue("updateSource", "greasyfork");
  982. }
  983.  
  984. /**
  985. * 显示警告消息
  986. */
  987. function showWarnMessage() {
  988. if (OJBetter.typeOfPage.is_oldLatex) {
  989. const loadingMessage = new LoadingMessage();
  990. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_oldLatex', { ns: 'alert' })}`, 'warning');
  991. }
  992. if (OJBetter.typeOfPage.is_acmsguru) {
  993. const loadingMessage = new LoadingMessage();
  994. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_acmsguru', { ns: 'alert' })}`, 'warning');
  995. }
  996. if (OJBetter.translation.comment.transMode == "1") {
  997. const loadingMessage = new LoadingMessage();
  998. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_segment', { ns: 'alert' })}`, 'warning');
  999. }
  1000. if (OJBetter.translation.comment.transMode == "2") {
  1001. const loadingMessage = new LoadingMessage();
  1002. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_select', { ns: 'alert' })}`, 'warning');
  1003. }
  1004. if (OJBetter.typeOfPage.is_submitPage && OJBetter.monaco.enableOnProblemPage) {
  1005. const loadingMessage = new LoadingMessage();
  1006. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_submitPage', { ns: 'alert' })}`, 'warning');
  1007. }
  1008. }
  1009.  
  1010. // 常量
  1011. 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>';
  1012. 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>`;
  1013. 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
  1014. 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>`;
  1015. 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" />
  1016. <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" />
  1017. <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>`;
  1018.  
  1019. /**
  1020. * 连接数据库
  1021. */
  1022. async function initDB() {
  1023. OJBetter.common.database = new Dexie('OJBetterDB');
  1024. OJBetter.common.database.version(3).stores({
  1025. samplesData: '&url',
  1026. editorCode: '&url',
  1027. translateData: '&url',
  1028. localizeSubsData: '&lang'
  1029. });
  1030.  
  1031. // 等待数据库打开
  1032. await OJBetter.common.database.open();
  1033. }
  1034.  
  1035. /**
  1036. * 清空数据库
  1037. */
  1038. async function clearDatabase() {
  1039. const isConfirmed = await OJB_createDialog(
  1040. i18next.t('isClearDatabase.title', { ns: 'dialog' }),
  1041. i18next.t('isClearDatabase.content', { ns: 'dialog' }),
  1042. [
  1043. i18next.t('isClearDatabase.buttons.0', { ns: 'dialog' }),
  1044. i18next.t('isClearDatabase.buttons.1', { ns: 'dialog' })
  1045. ]
  1046. );
  1047. if (!isConfirmed) {
  1048. try {
  1049. // 开启一个读写事务,包含数据库中的所有表
  1050. await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
  1051. // 遍历所有表
  1052. for (const table of OJBetter.common.database.tables) {
  1053. // 清空当前表
  1054. await table.clear();
  1055. }
  1056. });
  1057. console.log("All tables in the database have been cleared.");
  1058. alert("All tables in the database have been cleared.");
  1059. } catch (error) {
  1060. console.error("Error clearing the database:", error);
  1061. }
  1062. }
  1063. }
  1064.  
  1065. /**
  1066. * 导出数据库
  1067. * @returns {Promise<string>} 数据库的JSON字符串
  1068. */
  1069. async function exportDatabase() {
  1070. try {
  1071. // 创建一个存储数据的对象
  1072. const exportData = {};
  1073. // 获取数据库中所有表的名称
  1074. const tableNames = OJBetter.common.database.tables.map(table => table.name);
  1075.  
  1076. // 遍历每一个表,获取数据
  1077. for (const tableName of tableNames) {
  1078. const tableData = await OJBetter.common.database.table(tableName).toArray();
  1079. exportData[tableName] = tableData;
  1080. }
  1081.  
  1082. // 将数据对象转换为JSON字符串
  1083. const jsonData = JSON.stringify(exportData, null, 4);
  1084. return jsonData;
  1085. } catch (error) {
  1086. console.error("Error exporting database:", error);
  1087. }
  1088. }
  1089.  
  1090. /**
  1091. * 导入数据库
  1092. * @param {string} jsonData 数据库的JSON字符串
  1093. */
  1094. async function importDatabase(jsonData) {
  1095. const isConfirmed = await OJB_createDialog(
  1096. i18next.t('isImportDatabase.title', { ns: 'dialog' }),
  1097. i18next.t('isImportDatabase.content', { ns: 'dialog' }),
  1098. [
  1099. i18next.t('isImportDatabase.buttons.0', { ns: 'dialog' }),
  1100. i18next.t('isImportDatabase.buttons.1', { ns: 'dialog' })
  1101. ]
  1102. );
  1103. if (!isConfirmed) {
  1104. try {
  1105. // 将JSON字符串解析为对象
  1106. const importData = JSON.parse(jsonData);
  1107.  
  1108. // 开启一个事务,并清空现有数据
  1109. await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
  1110. // 清空所有表的数据
  1111. for (const tableName of OJBetter.common.database.tables.map(table => table.name)) {
  1112. await OJBetter.common.database.table(tableName).clear();
  1113. }
  1114.  
  1115. // 插入新数据
  1116. for (const [tableName, rows] of Object.entries(importData)) {
  1117. await OJBetter.common.database.table(tableName).bulkAdd(rows);
  1118. }
  1119. });
  1120. alert("Data imported successfully");
  1121. } catch (error) {
  1122. console.error("Error importing database:", error);
  1123. }
  1124. }
  1125. }
  1126.  
  1127. /**
  1128. * 将数据下载为文件
  1129. * @param {string} data 数据
  1130. * @param {string} filename 文件名,默认为'export.json'
  1131. * @param {string} fileType 文件MIME类型,默认为'application/json'
  1132. * @returns {void}
  1133. */
  1134. function downloadDataAsFile(data, filename = 'export.json', fileType = 'application/json') {
  1135. // 创建一个blob对象,指定文件类型
  1136. const blob = new Blob([data], { type: fileType });
  1137. const url = URL.createObjectURL(blob);
  1138.  
  1139. // 创建一个隐藏的a标签,模拟点击进行下载
  1140. const a = document.createElement('a');
  1141. a.href = url;
  1142. a.download = filename;
  1143. document.body.appendChild(a);
  1144. a.click();
  1145.  
  1146. // 清理
  1147. document.body.removeChild(a);
  1148. URL.revokeObjectURL(url);
  1149. }
  1150.  
  1151.  
  1152. /**
  1153. * 从文件中读取数据
  1154. * @param {Function} callback 回调函数
  1155. * @returns {void}
  1156. */
  1157. function readFileInput(callback) {
  1158. const fileInput = document.createElement('input');
  1159. fileInput.type = 'file';
  1160. fileInput.accept = '.json';
  1161. fileInput.style.display = 'none'; // 隐藏input元素
  1162.  
  1163. fileInput.onchange = (e) => {
  1164. const file = e.target.files[0];
  1165. if (file) {
  1166. const reader = new FileReader();
  1167. reader.onload = (e) => {
  1168. const fileContent = e.target.result;
  1169. if (callback && typeof callback === 'function') {
  1170. callback(fileContent); // 调用回调函数,传入文件内容
  1171. }
  1172. };
  1173. reader.readAsText(file);
  1174. }
  1175. };
  1176.  
  1177. document.body.appendChild(fileInput);
  1178. fileInput.click();
  1179. document.body.removeChild(fileInput);
  1180. }
  1181.  
  1182. /**
  1183. * 清除所有设置
  1184. */
  1185. async function deleteAllConfigSettings() {
  1186. const isConfirmed = await OJB_createDialog(
  1187. i18next.t('isDeleteAllConfigSettings.title', { ns: 'dialog' }),
  1188. i18next.t('isDeleteAllConfigSettings.content', { ns: 'dialog' }),
  1189. [
  1190. i18next.t('isDeleteAllConfigSettings.buttons.0', { ns: 'dialog' }),
  1191. i18next.t('isDeleteAllConfigSettings.buttons.1', { ns: 'dialog' })
  1192. ]
  1193. );
  1194. if (!isConfirmed) {
  1195. const keys = GM_listValues();
  1196.  
  1197. keys.forEach(key => {
  1198. GM_deleteValue(key);
  1199. });
  1200.  
  1201. alert('All settings have been deleted.');
  1202. window.location.reload();
  1203. }
  1204. }
  1205.  
  1206. /**
  1207. * 导出设置到JSON
  1208. * @returns {string} JSON字符串
  1209. */
  1210. function exportSettingsToJSON() {
  1211. const keys = GM_listValues();
  1212. let settings = {};
  1213.  
  1214. keys.forEach(key => {
  1215. settings[key] = GM_getValue(key);
  1216. });
  1217.  
  1218. return JSON.stringify(settings, null, 4);
  1219. }
  1220.  
  1221. /**
  1222. * 从JSON导入设置
  1223. * @param {string} jsonData JSON字符串
  1224. * @returns {void}
  1225. */
  1226. async function importSettingsFromJSON(jsonData) {
  1227. const isConfirmed = await OJB_createDialog(
  1228. i18next.t('isImportSettings.title', { ns: 'dialog' }),
  1229. i18next.t('isImportSettings.content', { ns: 'dialog' }),
  1230. [
  1231. i18next.t('isImportSettings.buttons.0', { ns: 'dialog' }),
  1232. i18next.t('isImportSettings.buttons.1', { ns: 'dialog' })
  1233. ]
  1234. );
  1235. if (!isConfirmed) {
  1236. let settings;
  1237. try {
  1238. settings = JSON.parse(jsonData);
  1239. } catch (e) {
  1240. console.error('JSON parsing error:', e);
  1241. return;
  1242. }
  1243.  
  1244. Object.keys(settings).forEach(key => {
  1245. GM_setValue(key, settings[key]);
  1246. });
  1247.  
  1248. alert('Settings imported successfully!');
  1249. window.location.reload();
  1250. }
  1251. }
  1252.  
  1253. /**
  1254. * 加载元素本地化语言数据
  1255. * @param {JQuery} element jQuery元素
  1256. * @param {number} [retries=10] 重试次数
  1257. * @param {number} [interval=50] 重试间隔
  1258. */
  1259. function elementLocalize(element, retries = 10, interval = 50) {
  1260. if ($.isFunction(element.localize)) {
  1261. element.localize();
  1262. } else if (retries > 0) {
  1263. setTimeout(elementLocalize, interval, element, retries - 1, interval);
  1264. } else {
  1265. console.error('Unable to localize', element);
  1266. }
  1267. }
  1268.  
  1269. // 切换系统黑暗监听
  1270. const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  1271. const changeEventListeners = [];
  1272. function handleColorSchemeChange(event) {
  1273. event.matches ? $('html').attr('data-theme', 'dark') : $('html').attr('data-theme', 'light');
  1274. if (!event.matches) {
  1275. var originalColor = $(this).data("original-color");
  1276. $(this).css("background-color", originalColor);
  1277. const intervalId = setinterval(() => {
  1278. if (OJBetter.monaco && OJBetter.monaco.editor) {
  1279. monaco.editor.setTheme('vs');
  1280. clearInterval(intervalId);
  1281. }
  1282. }, 100);
  1283. } else {
  1284. const intervalId = setInterval(() => {
  1285. if (OJBetter.monaco && OJBetter.monaco.editor) {
  1286. monaco.editor.setTheme('vs-dark');
  1287. clearInterval(intervalId);
  1288. }
  1289. }, 100);
  1290. }
  1291. }
  1292.  
  1293. // 黑暗模式
  1294. (function setDark() {
  1295. // 初始化
  1296. function setDarkTheme() {
  1297. const htmlElement = document.querySelector('html');
  1298. if (htmlElement) {
  1299. htmlElement.setAttribute('data-theme', 'dark');
  1300. const intervalId = setInterval(() => {
  1301. if (OJBetter.monaco && OJBetter.monaco.editor) {
  1302. monaco.editor.setTheme('vs-dark');
  1303. clearInterval(intervalId);
  1304. }
  1305. }, 100);
  1306. } else {
  1307. setTimeout(setDarkTheme, 100);
  1308. }
  1309. }
  1310. OJBetter.basic.darkMode = OJB_getGMValue("darkMode", "follow")
  1311. if (OJBetter.basic.darkMode == "dark") {
  1312. setDarkTheme();
  1313. } else if (OJBetter.basic.darkMode == "follow") {
  1314. // 添加事件监听器
  1315. changeEventListeners.push(handleColorSchemeChange);
  1316. mediaQueryList.addEventListener('change', handleColorSchemeChange);
  1317.  
  1318. if (window.matchMedia('(prefers-color-scheme: dark)').matches) setDarkTheme();
  1319. }
  1320.  
  1321. // 定义全局变量
  1322. GM_addStyle(`
  1323. /* 黑暗支持 */
  1324. html[data-theme=dark]:root {
  1325. color-scheme: light dark;
  1326. }
  1327. /* 颜色 */
  1328. :root {
  1329. /* 文字颜色 */
  1330. --ojb-color-text-primary: #a0adb9; /* 主要文字颜色 */
  1331. --ojb-color-text-secondary: #9AA4B1; /* 次要文字颜色 */
  1332. --ojb-color-text-tertiary: #9BA5B2; /* 第三级文字颜色 */
  1333. --ojb-color-text-success: #43A047; /* 成功状态文字颜色 */
  1334. --ojb-color-text-highlight: #cbd6e2; /* 高亮文字颜色 */
  1335. --ojb-color-text-disabled: #506778; /* 禁用状态文字颜色 */
  1336. --ojb-color-text-icon-success: #2e7d32; /* 成功状态图标颜色 */
  1337. --ojb-color-text-link: #4b8eda; /* 链接颜色 */
  1338.  
  1339. /* 背景颜色 */
  1340. --ojb-color-bg-primary: #22272e; /* 主背景颜色 */
  1341. --ojb-color-bg-secondary: #2d333b; /* 次级背景颜色 */
  1342. --ojb-color-bg-disabled: #24292e; /* 禁用元素背景颜色 */
  1343.  
  1344. /* 边框颜色 */
  1345. --ojb-color-border-primary: #48535F; /* 主要边框颜色 */
  1346. --ojb-color-border-disabled: #404950; /* 禁用状态边框颜色 */
  1347. --ojb-color-border-dashed-hover: #03A9F4; /* 虚线边框悬浮颜色 */
  1348. --ojb-color-border-radio-checked: #326154; /* 选中的单选框边框颜色 */
  1349.  
  1350. /* 阴影颜色 */
  1351. --ojb-shadow-standard: 0px 0px 0.5px 0.5px #3A4048; /* 标准阴影 */
  1352. --ojb-shadow-menu-modal: 0px 0px 0px 4px #2d333b; /* 菜单和模态框阴影 */
  1353.  
  1354. /* 区域遮罩颜色 */
  1355. --ojb-overlay-background: repeating-linear-gradient(135deg, #49525f6e, #49525f6e 30px, #49525f29 0px, #49525f29 55px); /* 区域遮罩背景 */
  1356.  
  1357. /* 文字阴影 */
  1358. --ojb-text-shadow-icon: 1px 1px 0px #2d333b, 1px -1px 0px #2d333b, -1px -1px 0px #2d333b, -1px 1px 0px #2d333b; /* 图标文字阴影 */
  1359. }
  1360. /* 边框样式 */
  1361. :root {
  1362. /* 边框样式 */
  1363. --ojb-border-width: 1px; /* 边框宽度 */
  1364. --ojb-border-style-solid: solid; /* 实线样式 */
  1365. --ojb-border-style-dashed: dashed; /* 虚线样式 */
  1366. --ojb-border-radius-small: 4px; /* 小圆角 */
  1367. --ojb-border-radius-medium: 8px; /* 中圆角 */
  1368. --ojb-border-radius-large: 12px; /* 大圆角 */
  1369. /* 组合边框样式 */
  1370. --ojb-border-solid-primary: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-primary); /* 主要实线边框 */
  1371. --ojb-border-dashed: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-primary); /* 主要虚线边框 */
  1372. --ojb-border-dashed-hover: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-dashed-hover); /* 悬浮虚线边框 */
  1373. --ojb-border-solid-disabled: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-disabled); /* 禁用状态实线边框 */
  1374. }
  1375. `);
  1376.  
  1377. // OJBetter界面样式
  1378. GM_addStyle(`
  1379. /* 主要文字颜色 */
  1380. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1381. html[data-theme=dark] .alert-warning, html[data-theme=dark] .markItUpEditor,
  1382. html[data-theme=dark] .translate-problem-statement, html[data-theme=dark] .OJBetter_setting_menu,
  1383. html[data-theme=dark] .help_tip .tip_text,
  1384. html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
  1385. html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
  1386. html[data-theme=dark] #items-per-page, html[data-theme=dark] #pagBar,
  1387. html[data-theme=dark] .OJBetter_setting_sidebar li a:link,
  1388. html[data-theme=dark] .popup .content{
  1389. color: var(--ojb-color-text-primary);
  1390. }
  1391. /* 次要文字颜色 */
  1392. html[data-theme=dark] .ojb_btn:hover, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_statusBar,
  1393. html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #addCustomTest,
  1394. html[data-theme=dark] #customTestBlock, html[data-theme=dark] .OJBetter_setting_list.alert_info{
  1395. color: var(--ojb-color-text-secondary);
  1396. }
  1397. /* 文字颜色3 */
  1398. html[data-theme=dark] .ojb_btn{
  1399. color: var(--ojb-color-text-tertiary);
  1400. }
  1401. /* 文字颜色 浅绿 */
  1402. html[data-theme=dark] #SubmitButton{
  1403. color: var(--ojb-color-text-success);
  1404. }
  1405. /* 禁止文字颜色 */
  1406. html[data-theme=dark] .ojb_btn[disabled]{
  1407. color: var(--ojb-color-text-disabled);
  1408. }
  1409. /* 主要背景层次 */
  1410. html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] li#add_button:hover,
  1411. html[data-theme=dark] .ojb_btn:hover,
  1412. html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
  1413. html[data-theme=dark] #OJBetter_SubmitForm input,
  1414. html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
  1415. html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
  1416. html[data-theme=dark] .OJBetter_setting_sidebar li a.active, html[data-theme=dark] .OJBetter_setting_sidebar li,
  1417. html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-track, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-track,
  1418. html[data-theme=dark] .OJBetter_modal, html[data-theme=dark] .OJBetter_modal button:hover,
  1419. html[data-theme=dark] .popup .content,
  1420. html[data-theme=dark] .config_bar_list, html[data-theme=dark] #LSPLog,
  1421. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
  1422. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before,
  1423. html[data-theme=dark] .OJBetter_setting_menu a, html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button:hover,
  1424. html[data-theme=dark] .OJBetter_setting_menu select{
  1425. background-color: var(--ojb-color-bg-primary);
  1426. background-image: none;
  1427. }
  1428. /* 次要背景层次 */
  1429. html[data-theme=dark] .ojb_btn,
  1430. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1431. html[data-theme=dark] .alert-warning, html[data-theme=dark] .SumoSelect>.optWrapper>.options li.opt:hover,
  1432. html[data-theme=dark] .translate-problem-statement-panel,
  1433. html[data-theme=dark] .translate-problem-statement,
  1434. html[data-theme=dark] .OJBetter_setting_list,
  1435. html[data-theme=dark] .OJBetter_setting_menu hr,
  1436. html[data-theme=dark] .OJBetter_setting_sidebar li a,
  1437. html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-thumb, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-thumb,
  1438. html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] .test-for-popup pre,
  1439. html[data-theme=dark] .popup .content pre, html[data-theme=dark] .popup .content pre code,
  1440. html[data-theme=dark] ul.config_bar_ul::-webkit-scrollbar-thumb, html[data-theme=dark] #OJBetter_statusBar,
  1441. html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] .sampleDiv,
  1442. html[data-theme=dark] #addCustomTest, html[data-theme=dark] #LSPLog li:nth-child(odd),
  1443. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::before,
  1444. html[data-theme=dark] .config::before, html[data-theme=dark] .config li.tempConfig_add_button:hover,
  1445. html[data-theme=dark] .OJBetter_setting_menu details, html[data-theme=dark] #config_bar_menu,
  1446. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button,
  1447. html[data-theme=dark] .OJBetter_setting_menu .badge, html[data-theme=dark] #OJBetter_SubmitForm #SubmitButton{
  1448. background-color: var(--ojb-color-bg-secondary);
  1449. }
  1450. /* 禁止背景层次 */
  1451. html[data-theme=dark] .ojb_btn[disabled]{
  1452. background-color: var(--ojb-color-bg-disabled);
  1453. }
  1454. /* 实线边框颜色-圆角 */
  1455. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1456. html[data-theme=dark] .alert-warning, html[data-theme=dark] .translate-problem-statement{
  1457. border: var(--ojb-border-solid-primary);
  1458. border-radius: 2px;
  1459. }
  1460. /* 实线边框颜色-无圆角 */
  1461. html[data-theme=dark] .ojb_btn,
  1462. html[data-theme=dark] .OJBetter_setting_list, html[data-theme=dark] .config_bar_list,
  1463. html[data-theme=dark] label.config_bar_ul_li_text,
  1464. html[data-theme=dark] .OJBetter_setting_sidebar li, html[data-theme=dark] .OJBetter_setting_menu select,
  1465. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_SubmitForm select,
  1466. html[data-theme=dark] #OJBetter_editor, html[data-theme=dark] #OJBetter_statusBar,
  1467. html[data-theme=dark] #OJBetter_SubmitForm #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #customTestBlock,
  1468. html[data-theme=dark] #OJBetter_SubmitForm #addCustomTest, html[data-theme=dark] #OJBetter_SubmitForm #SubmitButton,
  1469. html[data-theme=dark] .OJBetter_setting_menu input,
  1470. html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
  1471. html[data-theme=dark] .OJBetter_setting_menu textarea,
  1472. html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea,
  1473. html[data-theme=dark] #CompilerSetting select, html[data-theme=dark] #CompilerSetting textarea, html[data-theme=dark] #CompilerBox,
  1474. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
  1475. html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] .config::before,
  1476. html[data-theme=dark] #statePanel, html[data-theme=dark] .test-case, html[data-theme=dark] .OJBetter_setting_menu .badge{
  1477. border: var(--ojb-border-solid-primary);
  1478. }
  1479. html[data-theme=dark] #customTestBlock #customTests{
  1480. border-top: var(--ojb-border-solid-primary);
  1481. }
  1482. html[data-theme=dark] .OJBetter_setting_sidebar {
  1483. border-right: var(--ojb-border-solid-primary);
  1484. }
  1485. /* 实线边框-禁止 */
  1486. html[data-theme=dark] .ojb_btn[disabled]{
  1487. border: var(--ojb-border-solid-disabled);
  1488. }
  1489. /* 虚线边框 */
  1490. html[data-theme=dark] li#add_button,
  1491. html[data-theme=dark] .OJBetter_setting_menu_label_text{
  1492. border: var(--ojb-border-dashed);
  1493. }
  1494. /* 虚线边框-悬浮 */
  1495. html[data-theme=dark] li#add_button:hover{
  1496. border: var(--ojb-border-dashed-hover);
  1497. background-color: var(--ojb-color-bg-secondary);
  1498. color: var(--ojb-color-border-dashed-hover);
  1499. }
  1500. /* 无边框 */
  1501. html[data-theme=dark] .translate-problem-statement-panel .ojb_btn{
  1502. border: none;
  1503. }
  1504. /* 区域遮罩 */
  1505. html[data-theme=dark] .overlay::before {
  1506. background: var(--ojb-overlay-background);
  1507. color: var(--ojb-color-text-secondary);
  1508. text-shadow: 0px 0px 2px #000000;
  1509. }
  1510. /* 阴影 */
  1511. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .translate-problem-statement{
  1512. box-shadow: var(--ojb-shadow-standard);
  1513. }
  1514. /* 图标按钮状态样式 */
  1515. html[data-theme=dark] .ojb_btn_popover.success i:before, html[data-theme=dark] .ojb_btn_popover.success i {
  1516. color: var(--ojb-color-text-icon-success);
  1517. }
  1518. html[data-theme=dark] .ojb_btn_popover i:before {
  1519. text-shadow: var(--ojb-text-shadow-icon);
  1520. }
  1521. /* 其他样式 */
  1522. html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .OJBetter_modal{
  1523. box-shadow: var(--ojb-shadow-menu-modal);
  1524. border: 1px solid var(--ojb-color-bg-secondary);
  1525. }
  1526. html[data-theme=dark] input[type="radio"]:checked+.OJBetter_setting_menu_label_text {
  1527. color: var(--ojb-color-text-primary);
  1528. border: 1px solid var(--ojb-color-border-radio-checked);
  1529. }
  1530. html[data-theme=dark] .alert{
  1531. text-shadow: none;
  1532. }
  1533. `);
  1534.  
  1535. // 网站界面样式
  1536. GM_addStyle(`
  1537. /* 文字颜色1 */
  1538. html[data-theme=dark] body, html[data-theme=dark] .float-container>#main-container,
  1539. html[data-theme=dark] .panel-default>.panel-heading, html[data-theme=dark] #header a,
  1540. html[data-theme=dark] .pagination>li>a, html[data-theme=dark] .pagination>li>span, html[data-theme=dark] .dropdown-menu,
  1541. html[data-theme=dark] .select2-container--bootstrap .select2-selection--single .select2-selection__rendered,
  1542. html[data-theme=dark] .ace-tm .ace_gutter, html[data-theme=dark] .translate-problem-statement-panel,
  1543. html[data-theme=dark] .select2-container--bootstrap .select2-results__option--highlighted[aria-selected],
  1544. html[data-theme=dark] .nav-pills>li.active>a, html[data-theme=dark] .user-unrated, html[data-theme=dark] #header .header-page li.is-active a,
  1545. html[data-theme=dark] .m-box_inner, html[data-theme=dark] .m-list-job_item, html[data-theme=dark] .a-btn_arrow,
  1546. html[data-theme=dark] #header, html[data-theme=dark] #header .header-sub_page li a,
  1547. html[data-theme=dark] .select2-container--default .select2-selection--single .select2-selection__rendered, html[data-theme=dark] .select2-results{
  1548. color: var(--ojb-color-text-primary) !important;
  1549. }
  1550. /* 文字颜色2 */
  1551. html[data-theme=dark] pre, html[data-theme=dark] .html2mdButton, html[data-theme=dark] .btn-default, html[data-theme=dark] .btn-pre,
  1552. html[data-theme=dark] small.contest-duration, html[data-theme=dark] .select2-container--bootstrap .select2-results__option,
  1553. html[data-theme=dark] #ace_settingsmenu, #kbshortcutmenu, html[data-theme=dark] code{
  1554. color: var(--ojb-color-text-secondary) !important;
  1555. }
  1556. /* 文字颜色3 */
  1557. html[data-theme=dark] input, html[data-theme=dark] #header .header-page li a:hover{
  1558. color: var(--ojb-color-text-secondary);
  1559. }
  1560. /* 文字颜色4 */
  1561. html[data-theme=dark] .katex{
  1562. color: var(--ojb-color-text-highlight) !important;
  1563. }
  1564. /* 链接颜色 */
  1565. html[data-theme=dark] a:link {
  1566. color: var(--ojb-color-text-link);
  1567. }
  1568. html[data-theme=dark] a:visited {
  1569. color: var(--ojb-color-text-secondary);
  1570. }
  1571. /* 按钮 */
  1572. html[data-theme=dark] input:hover, html[data-theme=dark] .btn-default:hover{
  1573. background-color: var(--ojb-color-bg-primary) !important;
  1574. }
  1575. /* 背景层次1 */
  1576. html[data-theme=dark] body, html[data-theme=dark] #main-div.float-container, html[data-theme=dark] pre,
  1577. html[data-theme=dark] .html2mdButton:hover, html[data-theme=dark] .pagination>.active>a, html[data-theme=dark] .ace-tm,
  1578. html[data-theme=dark] .dropdown-menu>li>a:hover, html[data-theme=dark] .dropdown-menu>li>a:focus,
  1579. html[data-theme=dark] .dropdown-menu .divider, html[data-theme=dark] .select2-container--bootstrap .select2-selection,
  1580. html[data-theme=dark] .ace-tm .ace_gutter-active-line, html[data-theme=dark] .select2-dropdown,
  1581. html[data-theme=dark] input, html[data-theme=dark] button, html[data-theme=dark] select, html[data-theme=dark] textarea,
  1582. html[data-theme=dark] code, html[data-theme=dark] #keyvisual .keyvisual-inner:before, html[data-theme=dark] .m-box_inner,
  1583. html[data-theme=dark] .m-list-job_item, html[data-theme=dark] .select2-container--default .select2-selection--single,
  1584. html[data-theme=dark] ol.linenums, html[data-theme=dark] li.L0, html[data-theme=dark] li.L1, html[data-theme=dark] li.L2,
  1585. html[data-theme=dark] li.L3, html[data-theme=dark] li.L4, html[data-theme=dark] li.L5, html[data-theme=dark] li.L6,
  1586. html[data-theme=dark] li.L7, html[data-theme=dark] li.L8, html[data-theme=dark] li.L9{
  1587. background-color: var(--ojb-color-bg-primary) !important;
  1588. }
  1589. /* 背景层次2 */
  1590. html[data-theme=dark] .float-container>#main-container, html[data-theme=dark] #contest-nav-tabs,
  1591. html[data-theme=dark] .btn-default, html[data-theme=dark] .html2mdButton,
  1592. html[data-theme=dark] .nav-tabs>li.active>a, html[data-theme=dark] .nav-tabs>li.active>a:hover, html[data-theme=dark] .nav-tabs>li.active>a:focus,
  1593. html[data-theme=dark] .nav>li>a:hover, html[data-theme=dark] .nav>li>a:focus, html[data-theme=dark] .panel,
  1594. html[data-theme=dark] .table-striped>tbody>tr:nth-of-type(odd), html[data-theme=dark] .insert-participant-box,
  1595. html[data-theme=dark] .btn-pre, html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-danger,
  1596. html[data-theme=dark] .alert-warning, html[data-theme=dark] .panel-default>.panel-heading,
  1597. html[data-theme=dark] .pagination>li>a, html[data-theme=dark] .pagination>li>span, html[data-theme=dark] .dropdown-menu,
  1598. html[data-theme=dark] .ace-tm .ace_gutter, html[data-theme=dark] .select2-container--bootstrap .select2-results__option[aria-selected=true],
  1599. html[data-theme=dark] #ace_settingsmenu, #kbshortcutmenu, html[data-theme=dark] #header .header-inner,
  1600. html[data-theme=dark] ul#config_bar_ul::-webkit-scrollbar-thumb, html[data-theme=dark] .panel-info>.panel-heading,
  1601. html[data-theme=dark] .post-footer, html[data-theme=dark] .a-btn_arrow:before,
  1602. html[data-theme=dark] .table-hover>tbody>tr:hover,
  1603. html[data-theme=dark] li.L1, html[data-theme=dark] li.L3, html[data-theme=dark] li.L5, html[data-theme=dark] li.L7,
  1604. html[data-theme=dark] li.L9{
  1605. background-color: var(--ojb-color-bg-secondary) !important;
  1606. }
  1607. /* 实线边框颜色-圆角 */
  1608. html[data-theme=dark] input{
  1609. border: var(--ojb-border-solid-primary) !important;
  1610. border-radius: 2px;
  1611. }
  1612. /* 实线边框颜色-无圆角 */
  1613. html[data-theme=dark] .btn-default, html[data-theme=dark] .html2mdButton, html[data-theme=dark] .nav-tabs>li>a:hover,
  1614. html[data-theme=dark] .nav-tabs>li.active>a, html[data-theme=dark] .nav-tabs>li.active>a:hover,
  1615. html[data-theme=dark] .nav-tabs>li.active>a:focus, html[data-theme=dark] .btn-pre, html[data-theme=dark] .btn-pre:hover,
  1616. html[data-theme=dark] pre, html[data-theme=dark] .pagination>li>a, html[data-theme=dark] .pagination>li>span,
  1617. html[data-theme=dark] .table-bordered>thead>tr>th, html[data-theme=dark] .table-bordered>tbody>tr>th, html[data-theme=dark] .table-bordered>tfoot>tr>th,
  1618. html[data-theme=dark] .table-bordered>thead>tr>td, html[data-theme=dark] .table-bordered>tbody>tr>td, html[data-theme=dark] .table-bordered>tfoot>tr>td,
  1619. html[data-theme=dark] .panel, html[data-theme=dark] #editor, html[data-theme=dark] div#config_bar_list, html[data-theme=dark] label.config_bar_ul_li_text,
  1620. html[data-theme=dark] .select2-container--bootstrap .select2-selection, html[data-theme=dark] .select2-container--default .select2-selection--single{
  1621. border: var(--ojb-border-solid-primary) !important;
  1622. }
  1623. html[data-theme=dark] hr, html[data-theme=dark] .panel-footer,
  1624. html[data-theme=dark] .table>thead>tr>th, html[data-theme=dark] .table>tbody>tr>th, html[data-theme=dark] .table>tfoot>tr>th,
  1625. html[data-theme=dark] .table>thead>tr>td, html[data-theme=dark] .table>tbody>tr>td, html[data-theme=dark] .table>tfoot>tr>td{
  1626. border-top: var(--ojb-border-solid-primary) !important;
  1627. }
  1628. html[data-theme=dark] .nav-tabs, html[data-theme=dark] .panel-info>.panel-heading, html[data-theme=dark] .panel-default>.panel-heading,
  1629. html[data-theme=dark] .a-btn_arrow{
  1630. border-bottom: var(--ojb-border-solid-primary) !important;
  1631. }
  1632. html[data-theme=dark] .table>thead>tr>th{
  1633. border-bottom: 2px solid var(--ojb-color-border-primary) !important;
  1634. }
  1635. /* 双实线边框 */
  1636. html[data-theme=dark] #header .header-inner{
  1637. border-bottom: 5px double var(--ojb-color-border-primary) !important;
  1638. }
  1639. /* 阴影 */
  1640. html[data-theme=dark] .float-container>#main-container{
  1641. box-shadow: 0px 0px 10px 5px #fff0;
  1642. }
  1643. /* 图片-亮度 */
  1644. html[data-theme=dark] img{
  1645. opacity: .75;
  1646. }
  1647. /* 反转 */
  1648. html[data-theme=dark] .ace_content, html[data-theme=dark] #header .header-logo img, html[data-theme=dark] pre code{
  1649. filter: invert(1) hue-rotate(.5turn);
  1650. }
  1651. /* 区域遮罩 */
  1652. html[data-theme=dark] .overlay {
  1653. background: repeating-linear-gradient(135deg, #49525f6e, #49525f6e 30px, #49525f29 0px, #49525f29 55px);
  1654. color: #9099a3;
  1655. text-shadow: 0px 0px 2px #000000;
  1656. }
  1657. /* 其他样式 */
  1658. html[data-theme=dark] .nav-tabs>li.active>a, html[data-theme=dark] .nav-tabs>li.active>a:hover, html[data-theme=dark] .nav-tabs>li.active>a:focus{
  1659. border-bottom-color: transparent !important;
  1660. }
  1661. html[data-theme=dark] .collapsible-topic.collapsed .content .collapsible-topic-options:before{
  1662. background-image: linear-gradient(#22272e00, #22272e);
  1663. }
  1664. html[data-theme=dark] .alert{
  1665. text-shadow: none;
  1666. }
  1667. html[data-theme=dark] .m-box-news_post:before{
  1668. background: linear-gradient(0deg, #22272e 50%, rgba(255,255,255,0) 100%);
  1669. }
  1670. html[data-theme=dark] #header .header-sub_page li a:before, html[data-theme=dark] #header .header-page li a:before{
  1671. background-color: #9e9e9e !important;
  1672. }
  1673. html[data-theme=dark] .standings-score{
  1674. color: #2196f3;
  1675. }
  1676. html[data-theme=dark] pre code{
  1677. background-color: transparent !important;
  1678. }
  1679. html[data-theme=dark] #fixed-server-timer {
  1680. color: #000;
  1681. }
  1682. `);
  1683. })()
  1684.  
  1685. /**
  1686. * 黑暗模式额外的处理事件
  1687. */
  1688. function darkModeStyleAdjustment() {
  1689.  
  1690. }
  1691.  
  1692. /**
  1693. * 初始化monaco编辑器资源
  1694. */
  1695. async function initMonacoEditor() {
  1696. if (OJBetter.monaco.enableOnProblemPage || OJBetter.monaco.beautifyPreBlocks) {
  1697. try {
  1698. // 等待Monaco Editor加载器脚本加载完成
  1699. await OJB_LoadJS("https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/monaco-editor/0.49.0/min/vs/loader.min.js", "sha512-ZG31AN9z/CQD1YDDAK4RUAvogwbJHv6bHrumrnMLzdCrVu4HeAqrUX7Jsal/cbUwXGfaMUNmQU04tQ8XXl5Znw==");
  1700.  
  1701. // 配置Monaco Editor
  1702. require.config({
  1703. paths: { vs: "https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/monaco-editor/0.49.0/min/vs" },
  1704. "vs/nls": { availableLanguages: { "*": "zh-cn" } },
  1705. });
  1706.  
  1707. // 加载Monaco Editor主脚本
  1708. require(["vs/editor/editor.main"], () => {
  1709. OJBetter.monaco.loaderOnload = true;
  1710. });
  1711. } catch (error) {
  1712. console.error("Failed to load Monaco Editor: ", error);
  1713. }
  1714. }
  1715. }
  1716.  
  1717. /**
  1718. * 美化代码块
  1719. */
  1720. async function beautifyPreBlocksWithMonaco() {
  1721. // 等待 MonacoLoader 加载完毕
  1722. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  1723.  
  1724. const LINE_HEIGHT = 20; // 每行代码的高度
  1725. const MIN_HEIGHT = 100; // 容器的最小高度
  1726.  
  1727. /**
  1728. * 计算容器的高度
  1729. * @param {number} lineCount 代码的行数
  1730. * @param {string} [maxHeightStr='1000px'] 最大高度,为 'none' 时表示不限制
  1731. * @returns {string} 容器的高度
  1732. */
  1733. const calculateContainerHeight = (lineCount, maxHeightStr = '1000px') => {
  1734. const maxHeight = parseInt(maxHeightStr, 10); // 最大高度
  1735. const calculatedHeight = Math.max(lineCount * LINE_HEIGHT, MIN_HEIGHT);
  1736. return `${isNaN(maxHeight) ? calculatedHeight : Math.min(calculatedHeight, maxHeight)}px`;
  1737. };
  1738.  
  1739. /**
  1740. * 用于替换 <pre> 标签为 Monaco 编辑器的函数
  1741. * @param {HTMLElement} pre <pre> 元素
  1742. */
  1743. const replacePreWithMonaco = (pre) => {
  1744. if (pre.classList.contains('source-code-for-copy')) return; // 跳过复制块
  1745.  
  1746. const code = OJB_getCodeFromPre(pre); // 获取 <pre> 标签中的代码
  1747. if (!code) return;
  1748.  
  1749. const language = OJB_codeLangDetect(code); // 检测代码语言
  1750. const container = document.createElement('div'); // 创建一个用于 Monaco 编辑器的容器
  1751. const lineCount = code.split('\n').length; // 计算代码的行数
  1752.  
  1753. // 设置容器的样式
  1754. container.style.height = calculateContainerHeight(lineCount);
  1755. container.style.width = '100%';
  1756. pre.style.display = 'none';
  1757. pre.insertAdjacentElement('afterend', container);
  1758.  
  1759. // 折叠/展开按钮点击事件
  1760. const toggleExpandCollapse = (() => {
  1761. let isExpanded = false;
  1762. return () => {
  1763. console.log(calculateContainerHeight(lineCount, isExpanded ? '1000px' : 'none'));
  1764. container.style.height = calculateContainerHeight(lineCount, isExpanded ? '1000px' : 'none');
  1765. isExpanded = !isExpanded;
  1766. };
  1767. })();
  1768.  
  1769. // 重新注册 submission-code-expand-btn 按钮的点击事件
  1770. document.querySelectorAll('.submission-code-expand-btn').forEach(button => {
  1771. button.addEventListener('click', toggleExpandCollapse);
  1772. });
  1773.  
  1774. // 初始化 Monaco 编辑器
  1775. monaco.editor.create(container, {
  1776. value: code,
  1777. language,
  1778. readOnly: true,
  1779. tabSize: 4,
  1780. theme: OJBetter.basic.darkMode === "dark" ? "vs-dark" : "vs",
  1781. scrollbar: {
  1782. verticalScrollbarSize: 10,
  1783. horizontalScrollbarSize: 10,
  1784. alwaysConsumeMouseWheel: false
  1785. },
  1786. automaticLayout: true,
  1787. scrollBeyondLastLine: false
  1788. });
  1789. };
  1790.  
  1791. // 全局替换页面上所有的 <pre> 元素
  1792. document.querySelectorAll('pre').forEach(replacePreWithMonaco);
  1793.  
  1794. // 监听页面上的提交状态页面窗口的 <pre> 元素
  1795. if (OJBetter.typeOfPage.is_statePage || OJBetter.typeOfPage.is_submissions) {
  1796. OJB_observeElement({
  1797. selector: '#facebox',
  1798. callback: (node) => {
  1799. // 如果 facebox 中存在 pre 元素,则替换它们
  1800. node.querySelectorAll('pre').forEach(replacePreWithMonaco);
  1801. }
  1802. });
  1803. }
  1804. }
  1805.  
  1806. // 样式
  1807. GM_addStyle(`
  1808. /*动画*/
  1809. @keyframes shake {
  1810. 0% { transform: translateX(-5px); }
  1811. 100% { transform: translateX(5px); }
  1812. }
  1813. @keyframes rotate {
  1814. from {
  1815. transform: rotate(0deg);
  1816. }
  1817.  
  1818. to {
  1819. transform: rotate(360deg);
  1820. }
  1821. }
  1822. @keyframes rippleout {
  1823. 0% {
  1824. box-shadow: 0 0 0 0 rgba(96, 98, 102, 0.2);
  1825. }
  1826.  
  1827. 100% {
  1828. box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
  1829. }
  1830. }
  1831. @keyframes bounce-in {
  1832. 20%,40%,60%,80%,from,to {
  1833. animation-timing-function: cubic-bezier(.215,.61,.355,1);
  1834. }
  1835.  
  1836. 0% {
  1837. opacity: 0;
  1838. transform: scale3d(.995,.995,.995);
  1839. }
  1840.  
  1841. 20% {
  1842. opacity: 1;
  1843. transform: scale3d(1.005,1.005,1.005);
  1844. }
  1845.  
  1846. 40% {
  1847. transform: scale3d(.998,.998,.998);
  1848. }
  1849.  
  1850. 60% {
  1851. transform: scale3d(1.002,1.002,1.002);
  1852. }
  1853.  
  1854. 80% {
  1855. transform: scale3d(.995,.995,.995);
  1856. }
  1857.  
  1858. to {
  1859. opacity: 1;
  1860. transform: scale3d(1,1,1);
  1861. }
  1862. }
  1863. /*iconfont图标*/
  1864. .iconfont {
  1865. font-family: "iconfont" !important;
  1866. font-size: 16px;
  1867. font-style: normal !important;
  1868. -webkit-font-smoothing: antialiased;
  1869. -moz-osx-font-smoothing: grayscale;
  1870. }
  1871. @font-face {
  1872. font-family: 'iconfont'; /* Project id 4284341 */
  1873. src: url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2') format('woff2'),
  1874. url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2.ttf') format('truetype');
  1875. }
  1876. html {
  1877. scroll-behavior: smooth;
  1878. }
  1879. :root {
  1880. --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";
  1881. }
  1882. span.mdViewContent {
  1883. white-space: pre-wrap;
  1884. }
  1885.  
  1886. /* dialog */
  1887. dialog {
  1888. margin: 0px !important;
  1889. }
  1890. dialog::backdrop {
  1891. background-color: rgba(0, 0, 0, 0.4);
  1892. }
  1893.  
  1894. /*题目页链接栏样式*/
  1895. #problemToolbar {
  1896. display: flex;
  1897. flex-wrap: wrap;
  1898. justify-content: flex-end;
  1899. overflow: auto;
  1900. height: 100%;
  1901. margin: 0.5em;
  1902. }
  1903.  
  1904. /*html2md面板*/
  1905. .html2md-panel {
  1906. display: flex;
  1907. justify-content: flex-end;
  1908. align-items: center;
  1909. }
  1910. .html2md-panel a {
  1911. text-decoration: none;
  1912. }
  1913. .html2md-panel > button {
  1914. margin: 5px;
  1915. }
  1916. .html2md-panel.is_simple {
  1917. position: absolute;
  1918. right: 2%;
  1919. }
  1920.  
  1921. /*通用按钮*/
  1922. .ojb_btn {
  1923. display: flex;
  1924. align-items: center;
  1925. justify-content: center;
  1926. cursor: pointer;
  1927. background-color: #ffffff;
  1928. color: #606266;
  1929. width: auto;
  1930. font-size: 13px;
  1931. border-radius: 0.3rem;
  1932. padding: 2px 5px;
  1933. margin: 0px 5px;
  1934. border: 1px solid #dcdfe6;
  1935. }
  1936. .ojb_btn[disabled] {
  1937. cursor: not-allowed !important;
  1938. color: rgb(168, 171, 178) !important;
  1939. border: 1px solid #e4e7ed;
  1940. background-color: #ffffff;
  1941. }
  1942. .ojb_btn:hover {
  1943. color: #409eff;
  1944. border-color: #409eff;
  1945. background-color: #f1f8ff;
  1946. z-index: 150;
  1947. }
  1948. .ojb_btn.primary {
  1949. color: #ffffff;
  1950. border: 1px solid #409eff;
  1951. background-color: #409eff;
  1952. }
  1953. .ojb_btn.primary:hover {
  1954. color: #ffffff;
  1955. border: 1px solid #79bbff;
  1956. background-color: #79bbff;
  1957. }
  1958. .ojb_btn.success {
  1959. color: #4caf50;
  1960. border: 1px solid #C8E6C9;
  1961. background-color: #f0f9eb;
  1962. }
  1963. .ojb_btn.warning {
  1964. color: #e6a23c;
  1965. border: 1px solid #f3d19e;
  1966. background-color: #fdf6ec;
  1967. }
  1968. .ojb_btn.error {
  1969. color: #f56c6c;
  1970. border: 1px solid #fab6b6;
  1971. background-color: #fef0f0;
  1972. }
  1973. .ojb_btn.enabled {
  1974. color: #42A5F5;
  1975. border: 1px solid #90CAF9;
  1976. background-color: #fafbff;
  1977. }
  1978. .ojb_btn.active {
  1979. animation: rippleout 0.5s ease-in-out;
  1980. }
  1981. a.ojb_btn {
  1982. text-decoration: none;
  1983. }
  1984. a.ojb_btn:link {
  1985. color: #606266;
  1986. }
  1987. a.ojb_btn span {
  1988. margin-left: 2px;
  1989. }
  1990. /*按钮图标和popover*/
  1991. .ojb_btn_popover {
  1992. display: flex;
  1993. justify-content: center;
  1994. position: relative;
  1995. outline: none;
  1996. appearance: none;
  1997. }
  1998. .ojb_btn_popover:hover span {
  1999. opacity: 1;
  2000. visibility: visible;
  2001. }
  2002. .ojb_btn_popover i:before {
  2003. position: absolute;
  2004. text-shadow: 1px 1px 0px #ffffff, 1px -1px 0px #ffffff, -1px -1px 0px #ffffff, -1px 1px 0px #ffffff;
  2005. }
  2006. .ojb_btn_popover span {
  2007. cursor: initial;
  2008. position: absolute;
  2009. left: 50%;
  2010. opacity: 0;
  2011. visibility: hidden;
  2012. padding: 4px 8px;
  2013. background-color: rgba(33, 33, 33, 0.8);
  2014. color: rgba(255, 255, 255, 0.9019607843);
  2015. font-size: 12px;
  2016. border-radius: 6px;
  2017. line-height: 1.6;
  2018. text-align: left;
  2019. white-space: nowrap;
  2020. transition: all 0.15s ease-in-out;
  2021. z-index: 999;
  2022. }
  2023. .ojb_btn_popover span:hover {
  2024. opacity: 0;
  2025. visibility: hidden;
  2026. }
  2027. .ojb_btn_popover.top:hover span {
  2028. transform: translate(-50%, 0);
  2029. }
  2030. .ojb_btn_popover.top span {
  2031. bottom: 100%;
  2032. transform: translate(-50%, -20%);
  2033. margin-bottom: 4px;
  2034. }
  2035. .ojb_btn_popover.top span:hover {
  2036. transform: translate(-50%, -20%);
  2037. }
  2038. .ojb_btn_popover.bottom:hover span {
  2039. transform: translate(-50%, 105%);
  2040. }
  2041. .ojb_btn_popover.bottom span {
  2042. bottom: -2%;
  2043. transform: translate(-50%, 100%);
  2044. margin-top: 4px;
  2045. }
  2046. .ojb_btn_popover.bottom span:hover {
  2047. transform: translate(-50%, 50%);
  2048. }
  2049. .ojb_btn_popover.loading i {
  2050. color: rgba(33, 33, 33, 0.1);
  2051. }
  2052. .ojb_btn_popover.loading i:before {
  2053. content: "\\e640";
  2054. color: rgb(168, 171, 178);
  2055. animation: rotate 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
  2056. }
  2057. .ojb_btn_popover.running i {
  2058. color: rgba(33, 33, 33, 0.1);
  2059. }
  2060. .ojb_btn_popover.running i:before {
  2061. content: "\\e600";
  2062. color: rgb(168, 171, 178);
  2063. animation: rotate 1s linear infinite;
  2064. }
  2065. .ojb_btn_popover.warning i {
  2066. color: rgba(230, 162, 61, 0.8);
  2067. }
  2068. .ojb_btn_popover.warning i:before {
  2069. content: "\\e68b";
  2070. font-size: 15px;
  2071. left: 10px;
  2072. bottom: 0%;
  2073. color: #ff9800;
  2074. }
  2075. .ojb_btn_popover.error i {
  2076. color: rgba(245, 108, 108, 0.8);
  2077. }
  2078. .ojb_btn_popover.error i:before {
  2079. content: "\\e651";
  2080. font-size: 15px;
  2081. left: 10px;
  2082. bottom: 0%;
  2083. color: #F44336;
  2084. }
  2085. .ojb_btn_popover.success i {
  2086. color: rgba(76, 175, 80, 0.9);
  2087. }
  2088. .ojb_btn_popover.success i:before {
  2089. content: "\\e61e";
  2090. font-size: 15px;
  2091. left: 10px;
  2092. bottom: 0%;
  2093. color: #4caf50;
  2094. }
  2095. .ojb_btn_popover.enabled i {
  2096. color: rgba(33, 150, 243, 0.6);
  2097. }
  2098. .ojb_btn_popover.enabled i:before {
  2099. content: "\\e6f4";
  2100. font-size: 15px;
  2101. left: 10px;
  2102. bottom: 0%;
  2103. color: #2196F3;
  2104. }
  2105. .ojb_btn_popover.redo i {
  2106. color: rgba(33, 33, 33, 0.1);
  2107. }
  2108. .ojb_btn_popover.redo i:before {
  2109. content: "\\e831";
  2110. color: #616161;
  2111. }
  2112. .ojb_btn_popover.reverse i {
  2113. transform: rotate(180deg);
  2114. }
  2115.  
  2116. /*translateDiv样式*/
  2117. .translateDiv .topText {
  2118. display: flex;
  2119. margin-left: 5px;
  2120. color: #9e9e9e;
  2121. font-size: 13px;
  2122. align-items: center;
  2123. }
  2124. .translateDiv .borderlessButton{
  2125. display: flex;
  2126. align-items: center;
  2127. margin: 2.5px 7px;
  2128. fill: #9E9E9E;
  2129. }
  2130. .translateDiv .borderlessButton:hover{
  2131. cursor: pointer;
  2132. fill: #059669;
  2133. }
  2134. .translateDiv.bounce-in {
  2135. animation: bounce-in 1s forwards;
  2136. }
  2137. html:not([data-theme='dark']) .translateDiv {
  2138. box-shadow: 0px 0px 0.5px 0.5px #defdf378;
  2139. }
  2140. .translate-problem-statement {
  2141. justify-items: start;
  2142. letter-spacing: 1.8px;
  2143. color: #059669;
  2144. background-color: #f9f9fa;
  2145. border: 1px solid #c5ebdf;
  2146. border-radius: 0rem 0rem 0.3rem 0.3rem;
  2147. padding: 5px;
  2148. margin: -5px 0px 6px 0px;
  2149. width: 100%;
  2150. box-sizing: border-box;
  2151. font-size: 13px;
  2152. }
  2153. .translate-problem-statement h2 {
  2154. font-size: 1.6em;
  2155. font-weight: 700;
  2156. }
  2157. .translate-problem-statement h3 {
  2158. font-size: 1.3em;
  2159. font-weight: 700;
  2160. }
  2161. .translate-problem-statement-panel{
  2162. display: flex;
  2163. justify-content: space-between;
  2164. background-color: #f9f9fa;
  2165. border: 1px solid #c5ebdf;
  2166. border-radius: 0.3rem;
  2167. margin: 4px 0px;
  2168. }
  2169. .translate-problem-statement-panel .ojb_btn {
  2170. background: none;
  2171. border: none;
  2172. color: #9e9e9e;
  2173. }
  2174. .translate-problem-statement-panel.error, .translate-problem-statement.error {
  2175. color: red;
  2176. border-color: red;
  2177. }
  2178. .translate-problem-statement a, .translate-problem-statement a:link {
  2179. color: #10b981;
  2180. font-weight: 600;
  2181. background: 0 0;
  2182. text-decoration: none;
  2183. }
  2184. .translate-problem-statement ol, .translate-problem-statement ul {
  2185. display: grid;
  2186. margin-inline-start: 0.8em;
  2187. margin-block-start: 0em;
  2188. margin: 0.5em 0 0 3em;
  2189. padding-inline-start: 0px;
  2190. }
  2191. .translate-problem-statement li {
  2192. display: list-item;
  2193. height: auto;
  2194. word-wrap: break-word;
  2195. }
  2196. .translate-problem-statement ol li {
  2197. list-style-type: auto;
  2198. }
  2199. .translate-problem-statement ul li {
  2200. list-style-type: disc;
  2201. }
  2202. .translate-problem-statement img {
  2203. max-width: 100.0%;
  2204. max-height: 100.0%;
  2205. }
  2206. #task-statement .translate-problem-statement .MathJax {
  2207. color: #059669!important;
  2208. }
  2209. .translate-problem-statement span.math {
  2210. margin: 0px 2.5px !important;
  2211. }
  2212. .translate-problem-statement a:hover {
  2213. background-color: #800;
  2214. color: #fff;
  2215. text-decoration: none;
  2216. }
  2217. .translate-problem-statement table {
  2218. border: 1px #ccc solid !important;
  2219. margin: 1.5em 0 !important;
  2220. color: #059669 !important;
  2221. }
  2222. .translate-problem-statement table thead th {
  2223. border: 1px #ccc solid !important;
  2224. color: #059669 !important;
  2225. }
  2226. .translate-problem-statement table td {
  2227. border-right: 1px solid #ccc;
  2228. border-top: 1px solid #ccc;
  2229. padding: 0.7143em 0.5em;
  2230. }
  2231. .translate-problem-statement table th {
  2232. padding: 0.7143em 0.5em;
  2233. }
  2234. .translate-problem-statement p:not(:first-child) {
  2235. margin: 1.5em 0 0;
  2236. }
  2237. .translate-problem-statement p {
  2238. line-height: 20px !important;
  2239. word-wrap: break-word;
  2240. font-size: 13px !important
  2241. }
  2242. .problem-statement p:last-child {
  2243. margin-bottom: 0px !important;
  2244. }
  2245.  
  2246. /*设置按钮*/
  2247. header .enter-or-register-box, header .languages {
  2248. position: absolute;
  2249. right: 170px;
  2250. }
  2251. .ojb_btn.OJBetter_setting {
  2252. float: right;
  2253. height: 30px;
  2254. background: #60a5fa;
  2255. color: white;
  2256. margin: 10px;
  2257. border: 1px solid #60a5fa;
  2258. }
  2259. .ojb_btn.OJBetter_setting.open {
  2260. background-color: #e6e6e6;
  2261. color: #727378;
  2262. cursor: not-allowed;
  2263. }
  2264.  
  2265. /*设置面板*/
  2266. .OJBetter_setting_menu {
  2267. box-shadow: 0px 0px 0px 4px #ffffff;
  2268. position: fixed;
  2269. top: 50%;
  2270. left: 50%;
  2271. width: 600px;
  2272. min-height: 600px;
  2273. transform: translate(-50%, -50%);
  2274. border-radius: 6px;
  2275. background-color: #f0f4f9;
  2276. border-collapse: collapse;
  2277. border: 1px solid #ffffff;
  2278. color: #697e91;
  2279. font-family: var(--vp-font-family-base);
  2280. padding: 10px 20px 20px 10px;
  2281. box-sizing: content-box;
  2282. }
  2283. .OJBetter_setting_menu h3 {
  2284. margin-top: 10px;
  2285. font-size: 1.4em;
  2286. font-weight: 700;
  2287. }
  2288. .OJBetter_setting_menu h4 {
  2289. margin: 15px 0px 10px 0px;
  2290. }
  2291. .OJBetter_setting_menu h4,.OJBetter_setting_menu h5 {
  2292. font-weight: 600;
  2293. }
  2294. .OJBetter_setting_menu hr {
  2295. border: none;
  2296. height: 1px;
  2297. background-color: #ccc;
  2298. margin: 10px 0;
  2299. }
  2300. .OJBetter_setting_menu details {
  2301. padding: 10px;
  2302. margin-bottom: 5px;
  2303. background-color: #ffffff;
  2304. border-bottom: 1px solid #c9c6c696;
  2305. border-radius: 8px;
  2306. }
  2307. .OJBetter_setting_menu .badge {
  2308. border-radius: 4px;
  2309. border: 1px solid #009688;
  2310. color: #009688;
  2311. background-color: #fff;
  2312. padding: 0.5px 4px;
  2313. margin-left: 5px;
  2314. margin-right: auto;
  2315. line-height: initial;
  2316. font-weight: initial;
  2317. }
  2318. .OJBetter_setting_menu .missing {
  2319. box-shadow: inset 0px 0px 1px 1px red;
  2320. }
  2321. /* 页面切换 */
  2322. .OJBetter_setting_menu .settings-page {
  2323. display: none;
  2324. }
  2325. .OJBetter_setting_menu .settings-page.active {
  2326. display: block;
  2327. }
  2328. .OJBetter_setting_container {
  2329. display: flex;
  2330. }
  2331. .OJBetter_setting_sidebar {
  2332. flex: 0 0 auto;
  2333. min-width: 110px;
  2334. padding: 6px 10px 6px 6px;
  2335. margin: 20px 0px;
  2336. border-right: 1px solid #d4d8e9;
  2337. }
  2338. .OJBetter_setting_content {
  2339. flex-grow: 1;
  2340. margin: 20px 0px 0px 12px;
  2341. padding-right: 10px;
  2342. max-height: 580px;
  2343. overflow-y: auto;
  2344. box-sizing: border-box;
  2345. }
  2346. .OJBetter_setting_sidebar h3 {
  2347. margin-top: 0;
  2348. }
  2349. .OJBetter_setting_sidebar hr {
  2350. margin-top: 10px;
  2351. margin-bottom: 10px;
  2352. border: none;
  2353. border-top: 1px solid #DADCE0;
  2354. }
  2355. .OJBetter_setting_sidebar ul {
  2356. list-style-type: none;
  2357. margin: 0;
  2358. padding: 0;
  2359. }
  2360. .OJBetter_setting_sidebar li {
  2361. margin: 5px 0px;
  2362. background-color: #ffffff;
  2363. border: 1px solid #d4d8e9;
  2364. border-radius: 4px;
  2365. font-size: 16px;
  2366. }
  2367. .OJBetter_setting_sidebar li a {
  2368. text-decoration: none;
  2369. display: flex;
  2370. width: 100%;
  2371. font-size: 16px;
  2372. color: gray;
  2373. background-color: #ffffff;
  2374. border: none;
  2375. letter-spacing: 2px;
  2376. padding: 7px;
  2377. margin: 0px;
  2378. border-radius: 4px;
  2379. align-items: center;
  2380. -webkit-box-sizing: border-box;
  2381. -moz-box-sizing: border-box;
  2382. box-sizing: border-box;
  2383. }
  2384. .OJBetter_setting_sidebar li a.active {
  2385. background-color: #eceff1c7;
  2386. }
  2387. /* 链接样式 */
  2388. .OJBetter_setting_menu a {
  2389. font-size: 13px;
  2390. color: #009688 !important;
  2391. background-color: #E0F2F1;
  2392. border: 1px solid #009688;
  2393. border-radius: 4px;
  2394. padding: 0px 5px;
  2395. margin: 0px 5px;
  2396. text-decoration: none;
  2397. }
  2398. /* 下拉选择框 */
  2399. .OJBetter_setting_menu select {
  2400. appearance: none;
  2401. padding: 5px 10px;
  2402. margin: -5px 0px;
  2403. border-radius: 6px;
  2404. border-style: solid;
  2405. border: 1px solid #ced4da;
  2406. color: #009688;
  2407. background: #ffffff;
  2408. font-size: 15px;
  2409. }
  2410. .OJBetter_setting_menu select:focus-visible {
  2411. outline: none;
  2412. }
  2413. .OJBetter_setting_menu select option:disabled {
  2414. color: #EEEEEE;
  2415. }
  2416. /* 数值输入框 */
  2417. .OJBetter_setting_menu input[type="number"] {
  2418. width: 70px;
  2419. color: #009688;
  2420. font-size: 15px;
  2421. appearance: none;
  2422. padding: 5px 10px;
  2423. margin: -5px 3px;
  2424. border-radius: 6px;
  2425. border-style: solid;
  2426. border: 1px solid #ced4da;
  2427. }
  2428. .OJBetter_setting_menu input[type="number"]:focus-visible {
  2429. outline: none;
  2430. }
  2431. .OJBetter_setting_menu input[type="number"]::-webkit-inner-spin-button,
  2432. .OJBetter_setting_menu input[type="number"]::-webkit-outer-spin-button {
  2433. -webkit-appearance: none;
  2434. margin: 0;
  2435. }
  2436. /*设置面板-滚动条*/
  2437. .OJBetter_setting_menu::-webkit-scrollbar, .OJBetter_setting_content::-webkit-scrollbar,
  2438. .OJBetter_modal .content::-webkit-scrollbar {
  2439. width: 5px;
  2440. height: 7px;
  2441. background-color: #aaa;
  2442. }
  2443. .OJBetter_setting_menu::-webkit-scrollbar-thumb, .OJBetter_setting_content::-webkit-scrollbar-thumb,
  2444. .OJBetter_modal .content::-webkit-scrollbar-thumb {
  2445. background-clip: padding-box;
  2446. background-color: #d7d9e4;
  2447. }
  2448. .OJBetter_setting_menu::-webkit-scrollbar-track, .OJBetter_setting_content::-webkit-scrollbar-track,
  2449. .OJBetter_modal .content::-webkit-scrollbar-track {
  2450. background-color: #f1f1f1;
  2451. }
  2452. /*设置面板-关闭按钮*/
  2453. .OJBetter_setting_menu .tool-box {
  2454. position: absolute;
  2455. width: 20px;
  2456. height: 20px;
  2457. top: 3px;
  2458. right: 3px;
  2459. }
  2460. .OJBetter_setting_menu .btn-close {
  2461. width: 20px;
  2462. height: 20px;
  2463. border-radius: 50%;
  2464. border: none;
  2465. margin: 0px;
  2466. padding: 0px;
  2467. background-color: #ff000080;
  2468. transition: .15s ease all;
  2469. box-sizing: border-box;
  2470. text-align: center;
  2471. color: transparent;
  2472. }
  2473. .OJBetter_setting_menu .iconfont {
  2474. font-size: 10px;
  2475. font-weight: bolder;
  2476. }
  2477. .OJBetter_setting_menu .btn-close:hover {
  2478. color: #ffffff;
  2479. background-color: #ff0000cc;
  2480. box-shadow: 0 5px 5px 0 #00000026;
  2481. }
  2482. .OJBetter_setting_menu .btn-close:active {
  2483. color: #ffffffde;
  2484. background-color: #ff000080;
  2485. }
  2486. /*设置面板-checkbox*/
  2487. .OJBetter_setting_menu input[type=checkbox]:focus {
  2488. outline: 0px;
  2489. }
  2490. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"] {
  2491. margin: 0px;
  2492. appearance: none;
  2493. -webkit-appearance: none;
  2494. width: 40px;
  2495. height: 20px;
  2496. border: 1.5px solid #D7CCC8;
  2497. padding: 0px !important;
  2498. border-radius: 20px;
  2499. background: #efebe978;
  2500. position: relative;
  2501. box-sizing: border-box;
  2502. }
  2503. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::before {
  2504. content: "";
  2505. width: 17px;
  2506. height: 17px;
  2507. background: #D7CCC8;
  2508. border: 1.5px solid #BCAAA4;
  2509. border-radius: 50%;
  2510. position: absolute;
  2511. top: 0;
  2512. left: 0;
  2513. transform: translate(2%, 2%);
  2514. transition: all 0.3s ease-in-out;
  2515. box-sizing: border-box;
  2516. }
  2517. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::after {
  2518. 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");
  2519. position: absolute;
  2520. top: 0;
  2521. left: 24px;
  2522. }
  2523. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked {
  2524. border: 1.5px solid #C5CAE9;
  2525. background: #E8EAF6;
  2526. }
  2527. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::before {
  2528. background: #C5CAE9;
  2529. border: 1.5px solid #7986CB;
  2530. transform: translate(122%, 2%);
  2531. transition: all 0.3s ease-in-out;
  2532. }
  2533. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::after {
  2534. 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");
  2535. position: absolute;
  2536. top: 1.5px;
  2537. left: 4.5px;
  2538. }
  2539. .OJBetter_setting_menu .OJBetter_setting_list button {
  2540. cursor: pointer;
  2541. color: #7986cb;
  2542. background-color: #e8eaf6;
  2543. border: 1px solid #7986cb;
  2544. border-radius: 6px;
  2545. width: 100px;
  2546. margin: -5px 2px;
  2547. padding: 5px 10px;
  2548. }
  2549. .OJBetter_setting_menu .OJBetter_setting_list button:hover {
  2550. color: #e8eaf6;
  2551. background-color: #7986cb;
  2552. border: 1px solid #7986cb;
  2553. }
  2554. .OJBetter_setting_menu label, #darkMode_span, #loaded_span {
  2555. font-size: 16px;
  2556. }
  2557. .OJBetter_setting_list {
  2558. display: flex;
  2559. flex-wrap: wrap;
  2560. align-items: center;
  2561. padding: 10px;
  2562. margin: 5px 0px;
  2563. background-color: #ffffff;
  2564. border: 1px solid #c9c6c642;
  2565. border-bottom-color: #c9c6c696;
  2566. border-radius: 8px;
  2567. justify-content: space-between;
  2568. }
  2569. .OJBetter_setting_list.alert_danger {
  2570. color: #F44336;
  2571. background-color: #FFEBEE;
  2572. border: 1px solid #F44336;
  2573. margin: 10px 0px;
  2574. }
  2575. .OJBetter_setting_list.alert_warn {
  2576. color: #E65100;
  2577. background-color: #FFF3E0;
  2578. border: 1px solid #FF9800;
  2579. margin: 10px 0px;
  2580. }
  2581. .OJBetter_setting_list.alert_tip {
  2582. color: #009688;
  2583. background-color: #E0F2F1;
  2584. border: 1px solid #009688;
  2585. margin: 10px 0px;
  2586. }
  2587. .OJBetter_setting_list.alert_info {
  2588. color: #ffffff;
  2589. background-color: #009688;
  2590. margin: 10px 0px;
  2591. box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset;
  2592. }
  2593. .OJBetter_setting_list p:not(:last-child) {
  2594. margin-bottom: 10px;
  2595. }
  2596. .OJBetter_setting_list p:not(:first-child) {
  2597. margin-top: 10px;
  2598. }
  2599. /*设置面板-checkboxs*/
  2600. .OJBetter_setting_menu .OJBetter_checkboxs {
  2601. flex-basis: 100%;
  2602. display: flex;
  2603. padding: 8px;
  2604. margin: 10px 0px 0px 0px;
  2605. border-bottom: 1px solid #c9c6c696;
  2606. border-radius: 8px;
  2607. border: 1px solid #c5cae9;
  2608. background-color: #f0f8ff;
  2609. }
  2610. .OJBetter_setting_menu .OJBetter_checkboxs label {
  2611. font-size: 13px;
  2612. margin: 0px 6px 0px 3px;
  2613. }
  2614. .OJBetter_setting_menu .OJBetter_checkboxs input[type=checkbox]:checked+label{
  2615. color: #7986cb;
  2616. }
  2617. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"] {
  2618. border: none;
  2619. width: 16px;
  2620. height: 16px;
  2621. }
  2622. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before{
  2623. background: #ffffff;
  2624. transform: none;
  2625. width: 16px;
  2626. height: 16px;
  2627. }
  2628. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked {
  2629. background: none;
  2630. border: none;
  2631. }
  2632. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::before {
  2633. border: 1.5px solid #95a2de;
  2634. background: #e8eaf6;
  2635. transform: none;
  2636. }
  2637. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::after {
  2638. 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");
  2639. top: 0px;
  2640. left: 3.5px;
  2641. }
  2642. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:disabled+label {
  2643. color: #BDBDBD;
  2644. }
  2645. /*设置面板-radio*/
  2646. .OJBetter_setting_menu label {
  2647. display: block;
  2648. font-weight: initial;
  2649. list-style-type: none;
  2650. padding-inline-start: 0px;
  2651. overflow-x: auto;
  2652. max-width: 100%;
  2653. margin: 3px 0px;
  2654. overflow-x: visible;
  2655. }
  2656. .OJBetter_setting_menu_label_text {
  2657. display: flex;
  2658. border: 1px dashed #00aeeccc;
  2659. height: 35px;
  2660. width: 100%;
  2661. color: #6e6e6e;
  2662. font-weight: 300;
  2663. font-size: 14px;
  2664. letter-spacing: 2px;
  2665. padding: 7px;
  2666. margin-bottom: 4px;
  2667. align-items: center;
  2668. -webkit-box-sizing: border-box;
  2669. -moz-box-sizing: border-box;
  2670. box-sizing: border-box;
  2671. }
  2672. input[type="radio"]:checked+.OJBetter_setting_menu_label_text {
  2673. background: #41e49930;
  2674. border: 1px solid green;
  2675. color: green;
  2676. text-shadow: 0px 0px 0.5px green;
  2677. }
  2678. input[type="radio"]:disabled+.OJBetter_setting_menu_label_text {
  2679. background: #fafafa00;
  2680. border: 1px solid #e0e0e07a;
  2681. color: #e0e0e0;
  2682. }
  2683. .OJBetter_setting_menu label input[type="radio"], .OJBetter_contextmenu label input[type="radio"]{
  2684. appearance: none;
  2685. list-style: none;
  2686. padding: 0px !important;
  2687. margin: 0px;
  2688. clip: rect(0 0 0 0);
  2689. -webkit-clip-path: inset(100%);
  2690. clip-path: inset(100%);
  2691. height: 1px;
  2692. overflow: hidden;
  2693. position: absolute;
  2694. white-space: nowrap;
  2695. width: 1px;
  2696. }
  2697. /*设置面板-文本输入框*/
  2698. .OJBetter_setting_menu input[type="text"] {
  2699. display: block;
  2700. height: 25px !important;
  2701. width: 100%;
  2702. background-color: #ffffff;
  2703. color: #727378;
  2704. font-size: 12px;
  2705. border-radius: 0.3rem;
  2706. padding: 1px 5px !important;
  2707. box-sizing: border-box;
  2708. margin: 5px 0px 5px 0px;
  2709. border: 1px solid #00aeeccc;
  2710. box-shadow: 0 0 1px #0000004d;
  2711. }
  2712. .OJBetter_setting_menu .OJBetter_setting_list input[type="text"] {
  2713. margin-left: 5px;
  2714. }
  2715. .OJBetter_setting_menu input[type="text"]:focus-visible{
  2716. border-style: solid;
  2717. border-color: #3f51b5;
  2718. outline: none;
  2719. }
  2720. .OJBetter_setting_menu_config_box {
  2721. width: 100%;
  2722. display: grid;
  2723. margin-top: 5px;
  2724. -webkit-box-sizing: border-box;
  2725. -moz-box-sizing: border-box;
  2726. box-sizing: border-box;
  2727. }
  2728. .OJBetter_setting_menu input::placeholder {
  2729. color: #727378;
  2730. }
  2731. .OJBetter_setting_menu input.no_default::placeholder{
  2732. color: #BDBDBD;
  2733. }
  2734. .OJBetter_setting_menu input.is_null::placeholder{
  2735. color: red;
  2736. border-width: 1.5px;
  2737. }
  2738. .OJBetter_setting_menu input.is_null{
  2739. border-color: red;
  2740. }
  2741. .OJBetter_setting_menu textarea {
  2742. resize: vertical;
  2743. display: block;
  2744. width: 100%;
  2745. height: 60px;
  2746. background-color: #ffffff;
  2747. color: #727378;
  2748. font-size: 12px;
  2749. padding: 1px 5px !important;
  2750. box-sizing: border-box;
  2751. margin: 5px 0px 5px 0px;
  2752. border: 1px solid #00aeeccc;
  2753. box-shadow: 0 0 1px #0000004d;
  2754. }
  2755. .OJBetter_setting_menu textarea:focus-visible{
  2756. border-style: solid;
  2757. border-color: #3f51b5;
  2758. outline: none;
  2759. }
  2760. .OJBetter_setting_menu textarea::placeholder{
  2761. color: #BDBDBD;
  2762. font-size: 14px;
  2763. }
  2764. .OJBetter_setting_menu #tempConfig_save {
  2765. cursor: pointer;
  2766. display: inline-flex;
  2767. padding: 5px;
  2768. background-color: #1aa06d;
  2769. color: #ffffff;
  2770. font-size: 14px;
  2771. line-height: 1.5rem;
  2772. font-weight: 500;
  2773. justify-content: center;
  2774. width: 100%;
  2775. border-radius: 0.375rem;
  2776. border: none;
  2777. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  2778. margin-top: 20px
  2779. }
  2780. .OJBetter_setting_menu button#debug_button.debug_button {
  2781. width: 18%;
  2782. }
  2783. .OJBetter_setting_menu span.tip {
  2784. color: #999;
  2785. font-size: 12px;
  2786. font-weight: 500;
  2787. padding: 5px 0px;
  2788. }
  2789. /*设置面板-tip*/
  2790. .help_tip {
  2791. margin-right: auto;
  2792. }
  2793. span.input_label {
  2794. font-size: 14px;
  2795. }
  2796. .help_tip .tip_text {
  2797. display: none;
  2798. position: absolute;
  2799. color: #697e91;
  2800. font-weight: 400;
  2801. font-size: 14px;
  2802. letter-spacing: 0px;
  2803. background-color: #ffffff;
  2804. padding: 10px;
  2805. margin: 5px 0px;
  2806. border-radius: 4px;
  2807. border: 1px solid #e4e7ed;
  2808. box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);
  2809. z-index: 100;
  2810. }
  2811. .help_tip .tip_text p {
  2812. margin-bottom: 5px;
  2813. }
  2814. .help_tip .tip_text:before {
  2815. content: "";
  2816. position: absolute;
  2817. top: -20px;
  2818. right: -10px;
  2819. bottom: -10px;
  2820. left: -10px;
  2821. z-index: -1;
  2822. }
  2823. .help-icon {
  2824. cursor: help;
  2825. width: 15px;
  2826. color: #b4b9d4;
  2827. margin-left: 5px;
  2828. margin-top: 3px;
  2829. }
  2830. .OJBetter_setting_menu .OJBetter_setting_menu_label_text .help_tip .help-icon {
  2831. color: #7fbeb2;
  2832. }
  2833. .help_tip .help-icon:hover + .tip_text, .help_tip .tip_text:hover {
  2834. display: block;
  2835. cursor: help;
  2836. width: 250px;
  2837. }
  2838. /* 版本信息 */
  2839. .OJBetter_setting_menu .versionInfo{
  2840. display: grid;
  2841. justify-items: center;
  2842. font-size: 16px;
  2843. padding: 10px;
  2844. }
  2845. .OJBetter_setting_menu .versionInfo>* {
  2846. margin: 10px 0px;
  2847. }
  2848.  
  2849. /* 配置管理面板 */
  2850. .config{
  2851. width: 100%;
  2852. margin: 10px 0px;
  2853. }
  2854. .config li.tempConfig_add_button {
  2855. cursor: pointer;
  2856. height: 40px;
  2857. border: 1px dashed #BDBDBD;
  2858. border-radius: 8px;
  2859. background-color: #fcfbfb36;
  2860. color: #bdbdbd;
  2861. font-size: 14px;
  2862. align-items: center;
  2863. justify-content: center;
  2864. }
  2865. .config li.tempConfig_add_button:hover {
  2866. border: 1px dashed #03A9F4;
  2867. background-color: #d7f0fb8c;
  2868. color: #03A9F4;
  2869. }
  2870. .config .config_bar_list {
  2871. display: flex;
  2872. width: 100%;
  2873. padding-bottom: 2px;
  2874. border: 1px solid #c5cae9;
  2875. background-color: #f0f8ff;
  2876. box-sizing: border-box;
  2877. border-radius: 0px 0px 8px 8px;
  2878. }
  2879. .config .config_bar_list input[type="radio"] {
  2880. appearance: none;
  2881. width: 0;
  2882. height: 0;
  2883. overflow: hidden;
  2884. }
  2885. .config .config_bar_list input[type="radio"] {
  2886. margin: 0px;
  2887. }
  2888. .config .config_bar_list input[type=radio]:focus {
  2889. outline: 0px;
  2890. }
  2891. .config .config_bar_ul_li_text {
  2892. display: flex;
  2893. align-items: center;
  2894. justify-content: center;
  2895. max-width: 100%;
  2896. height: 40px;
  2897. overflow-x: auto;
  2898. font-size: 14px;
  2899. font-weight: 400;
  2900. margin: 0px 4px;
  2901. padding: 3px;
  2902. border: 1px solid #dedede;
  2903. border-radius: 10px;
  2904. box-shadow: 0px 2px 4px 0px rgba(0,0,0,.05);
  2905. box-sizing: border-box;
  2906. }
  2907. .config .config_bar_ul li button {
  2908. background-color: #e6e6e6;
  2909. color: #727378;
  2910. height: 23px;
  2911. font-size: 14px;
  2912. border-radius: 0.3rem;
  2913. padding: 1px 5px;
  2914. margin: 5px;
  2915. border: none;
  2916. box-shadow: 0 0 1px #0000004d;
  2917. }
  2918. .config .config_bar_ul {
  2919. display: flex;
  2920. align-items: center;
  2921. list-style-type: none;
  2922. padding-inline-start: 0px;
  2923. overflow-x: auto;
  2924. max-width: 100%;
  2925. margin: 0px;
  2926. padding: 5px;
  2927. }
  2928. .config .config_bar_ul li {
  2929. width: 80px;
  2930. display: grid;
  2931. margin: 4px 4px;
  2932. min-width: 100px;
  2933. box-sizing: border-box;
  2934. }
  2935. .config .config_bar_ul_li_text:hover {
  2936. background-color: #eae4dc24;
  2937. }
  2938. input[type="radio"]:checked + .config_bar_ul_li_text {
  2939. background: #41b3e430;
  2940. border: 1px solid #5e7ce0;
  2941. color: #5e7ce0;
  2942. }
  2943. .config .config_bar_ul::-webkit-scrollbar {
  2944. width: 5px;
  2945. height: 4px;
  2946. }
  2947. .config .config_bar_ul::-webkit-scrollbar-thumb {
  2948. background-clip: padding-box;
  2949. background-color: #d7d9e4;
  2950. border-radius: 8px;
  2951. }
  2952. .config .config_bar_ul::-webkit-scrollbar-button:start:decrement {
  2953. width: 4px;
  2954. background-color: transparent;
  2955. }
  2956. .config .config_bar_ul::-webkit-scrollbar-button:end:increment {
  2957. width: 4px;
  2958. background-color: transparent;
  2959. }
  2960. .config .config_bar_ul::-webkit-scrollbar-track {
  2961. border-radius: 5px;
  2962. }
  2963. .config .config_bar_ul_li_text::-webkit-scrollbar {
  2964. width: 5px;
  2965. height: 7px;
  2966. background-color: #aaa;
  2967. }
  2968. .config .config_bar_ul_li_text::-webkit-scrollbar-thumb {
  2969. background-clip: padding-box;
  2970. background-color: #d7d9e4;
  2971. }
  2972. .config .config_bar_ul_li_text::-webkit-scrollbar-track {
  2973. background-color: #f1f1f1;
  2974. }
  2975. .config .config_bar_list_add_div {
  2976. display: flex;
  2977. height: 40px;
  2978. margin: 4px 2px;
  2979. }
  2980.  
  2981. /* 修改菜单 */
  2982. #config_bar_menu {
  2983. z-index: 400;
  2984. position: fixed;
  2985. width: 60px;
  2986. background: #ffffff;
  2987. box-shadow: 1px 1px 4px 0px #0000004d;
  2988. border: 0px solid rgba(0,0,0,0.04);
  2989. border-radius: 4px;
  2990. padding: 8px 0;
  2991. }
  2992. .config_bar_menu_item {
  2993. cursor: pointer;
  2994. padding: 2px 6px;
  2995. display: flex;
  2996. justify-content: center;
  2997. align-items: center;
  2998. height: 32px;
  2999. font-size: 14px;
  3000. font-weight: 500;
  3001. box-shadow: inset 0px 0px 0px 0px #8bb2d9;
  3002. }
  3003. #config_bar_menu_edit:hover {
  3004. background-color: #00aeec;
  3005. color: white;
  3006. }
  3007. #config_bar_menu_delete:hover {
  3008. background-color: #FF5722;
  3009. color: white;
  3010. }
  3011.  
  3012. /* 配置编辑页面 */
  3013. #config_edit_menu {
  3014. z-index: 300;
  3015. width: 450px;
  3016. }
  3017.  
  3018. /* 黑暗模式选项按钮 */
  3019. .dark-mode-selection {
  3020. display: flex;
  3021. justify-content: center;
  3022. align-items: center;
  3023. max-width: 350px;
  3024. -webkit-user-select: none;
  3025. -moz-user-select: none;
  3026. -ms-user-select: none;
  3027. user-select: none;
  3028. }
  3029. .dark-mode-selection label {
  3030. margin: 8px 0px 8px 8px;
  3031. }
  3032. .dark-mode-selection > * {
  3033. margin: 6px;
  3034. }
  3035. .dark-mode-selection .OJBetter_setting_menu_label_text {
  3036. border-radius: 8px;
  3037. margin-bottom: 0px;
  3038. }
  3039.  
  3040. /*确认弹窗*/
  3041. .OJBetter_modal {
  3042. z-index: 600;
  3043. display: grid;
  3044. position: fixed;
  3045. top: 50%;
  3046. left: 50%;
  3047. transform: translate(-50%, -50%);
  3048. font-size: 12px;
  3049. font-family: var(--vp-font-family-base);
  3050. width: max-content;
  3051. padding: 10px 20px;
  3052. box-shadow: 0px 0px 0px 4px #ffffff;
  3053. border-radius: 6px;
  3054. background-color: #f0f4f9;
  3055. border-collapse: collapse;
  3056. border: 1px solid #ffffff;
  3057. color: #697e91;
  3058. }
  3059. .OJBetter_modal h2 {
  3060. font-size: 1.6em;
  3061. font-weight: 700;
  3062. }
  3063. .OJBetter_modal .content{
  3064. white-space: nowrap;
  3065. max-height: 500px;
  3066. overflow-y: auto;
  3067. }
  3068. .OJBetter_modal .buttons{
  3069. display: flex;
  3070. padding-top: 15px;
  3071. }
  3072. .OJBetter_modal button {
  3073. display: inline-flex;
  3074. justify-content: center;
  3075. align-items: center;
  3076. line-height: 1;
  3077. white-space: nowrap;
  3078. cursor: pointer;
  3079. text-align: center;
  3080. box-sizing: border-box;
  3081. outline: none;
  3082. transition: .1s;
  3083. user-select: none;
  3084. vertical-align: middle;
  3085. -webkit-appearance: none;
  3086. height: 24px;
  3087. padding: 5px 11px;
  3088. margin-right: 15px;
  3089. font-size: 12px;
  3090. border-radius: 4px;
  3091. color: #ffffff;
  3092. background: #009688;
  3093. border-color: #009688;
  3094. border: none;
  3095. }
  3096. .OJBetter_modal button.secondary{
  3097. background-color:#4DB6AC;
  3098. }
  3099. .OJBetter_modal button:hover{
  3100. background-color:#4DB6AC;
  3101. }
  3102. .OJBetter_modal button.secondary:hover {
  3103. background-color: #80CBC4;
  3104. }
  3105. .OJBetter_modal .help-icon {
  3106. margin: 0px 8px 0px 0px;
  3107. height: 1em;
  3108. width: 1em;
  3109. line-height: 1em;
  3110. display: inline-flex;
  3111. justify-content: center;
  3112. align-items: center;
  3113. position: relative;
  3114. fill: currentColor;
  3115. font-size: inherit;
  3116. }
  3117. .OJBetter_modal p {
  3118. margin: 5px 0px;
  3119. }
  3120.  
  3121. /* 右键菜单 */
  3122. .OJBetter_contextmenu {
  3123. z-index: 500;
  3124. display: grid;
  3125. position: absolute;
  3126. background-color: #f0f4f9;
  3127. border-collapse: collapse;
  3128. color: #697e91;
  3129. font-family: var(--vp-font-family-base);
  3130. overflow: hidden;
  3131. box-sizing: content-box;
  3132. box-shadow: 0px 0px 0px 2px #eddbdb4d;
  3133. }
  3134. .OJBetter_contextmenu label {
  3135. margin: 0px;
  3136. }
  3137. input[type="radio"]:checked+.OJBetter_contextmenu_label_text {
  3138. background: #41e49930;
  3139. border: 1px solid green;
  3140. color: green;
  3141. font-weight: 500;
  3142. }
  3143. .OJBetter_contextmenu_label_text {
  3144. display: flex;
  3145. border: 1px dashed #80cbc4;
  3146. height: 26px;
  3147. width: 100%;
  3148. color: gray;
  3149. font-size: 13px;
  3150. font-weight: initial;
  3151. padding: 4px;
  3152. align-items: center;
  3153. -webkit-box-sizing: border-box;
  3154. -moz-box-sizing: border-box;
  3155. box-sizing: border-box;
  3156. }
  3157. .OJBetter_contextmenu_label_text:hover {
  3158. color: #F44336;
  3159. border: 1px dashed #009688;
  3160. background-color: #ffebcd;
  3161. }
  3162.  
  3163. /* RatingByClist */
  3164. .ratingBadge, html[data-theme=dark] button.ratingBadge{
  3165. display: block;
  3166. font-weight: 700;
  3167. font-size: 11px;
  3168. margin-top: 5px;
  3169. padding: 2px;
  3170. border-radius: 4px;
  3171. color: #B0BEC5;
  3172. border: 1px solid #cccccc66;
  3173. }
  3174.  
  3175. /* 多选翻译 */
  3176. .block_selected{
  3177. box-shadow: 0px 0px 0px 1px #FF9800;
  3178. outline: none;
  3179. }
  3180.  
  3181. /* 悬浮菜单 */
  3182. .OJBetter_MiniTranslateButton {
  3183. z-index: 100;
  3184. display: grid;
  3185. position: absolute;
  3186. border-collapse: collapse;
  3187. fill: #F57C00;
  3188. background-color: #FFF3E0;
  3189. overflow: hidden;
  3190. box-sizing: content-box;
  3191. box-shadow: 0px 0px 0px 2px #FFE0B2;
  3192. border-radius: 100%;
  3193. }
  3194. .OJBetter_MiniTranslateButton:hover {
  3195. cursor: pointer;
  3196. box-shadow: 0px 0px 0px 2px #FFB74D;
  3197. }
  3198.  
  3199. /* acmsguru划分块 */
  3200. .OJBetter_acmsguru {
  3201. margin: 0 0 1em!important;
  3202. }
  3203.  
  3204. /* 代码提交表单 */
  3205. #OJBetter_SubmitForm.input-output-copier:hover {
  3206. background-color: #ffffff00;
  3207. }
  3208. #OJBetter_SubmitForm input[type="number"] {
  3209. width: 40px;
  3210. color: #009688;
  3211. appearance: none;
  3212. border-radius: 6px;
  3213. border-style: solid;
  3214. border: none;
  3215. background-color: #ffffff00;
  3216. }
  3217. #OJBetter_SubmitForm :focus-visible {
  3218. outline: none;
  3219. }
  3220. #OJBetter_SubmitForm .topDiv {
  3221. height: 50px;
  3222. display: flex;
  3223. align-items: center;
  3224. justify-content: space-between;
  3225. padding: 10px 0px;
  3226. box-sizing: border-box;
  3227. }
  3228. #OJBetter_SubmitForm .topDiv .topRightDiv {
  3229. height: 100%;
  3230. display: flex;
  3231. flex-wrap: wrap;
  3232. gap: 0px;
  3233. }
  3234. #OJBetter_SubmitForm input[type="checkbox"], #OJBetter_SubmitForm label {
  3235. margin: 0px;
  3236. font-weight: initial;
  3237. }
  3238. #OJBetter_SubmitForm #fontSizeInput {
  3239. border: none;
  3240. background-color: #ffffff00;
  3241. }
  3242.  
  3243. /* 顶部区域 */
  3244. #OJBetter_SubmitForm .topRightDiv>* {
  3245. height: 100%;
  3246. box-sizing: border-box;
  3247. }
  3248. #OJBetter_SubmitForm .topRightDiv>button{
  3249. padding: 0px 8px;
  3250. }
  3251. #OJBetter_SubmitForm .topRightDiv {
  3252. display: flex;
  3253. flex-wrap: wrap;
  3254. gap: 0px;
  3255. align-items: center;
  3256. }
  3257.  
  3258. /* LSP连接Log */
  3259. #LSPLog{
  3260. width: 500px;
  3261. height: 500px;
  3262. position: fixed;
  3263. top: 50%;
  3264. left: 50%;
  3265. padding: 10px;
  3266. transform: translate(-50%, -50%);
  3267. border: 1px solid;
  3268. z-index: 200;
  3269. background-color: #ffffff;
  3270. }
  3271. #LSPLog button{
  3272. position: fixed;
  3273. top: 10px;
  3274. right: 10px;
  3275. z-index: 200;
  3276. }
  3277. #LSPLog #LSPLogList{
  3278. width: 500px;
  3279. height: 500px;
  3280. overflow: auto;
  3281. color: #424242;
  3282. }
  3283. #LSPLog li:nth-child(odd){
  3284. background-color: #f5f5f5;
  3285. }
  3286. #LSPLog details{
  3287. padding: 2px;
  3288. }
  3289.  
  3290. /* 代码编辑器 */
  3291. #OJBetter_editor{
  3292. box-sizing: border-box;
  3293. height: 600px;
  3294. border: 1px solid #d3d3d3;
  3295. width: 100%;
  3296. resize: vertical;
  3297. display: flex;
  3298. flex-direction: column;
  3299. }
  3300. #OJBetter_editor.fullscreen{
  3301. position: fixed;
  3302. top: 0;
  3303. left: 0;
  3304. width: 100%;
  3305. height: 100vh;
  3306. z-index: 2000;
  3307. }
  3308. #OJBetter_editor.bottom{
  3309. position: fixed;
  3310. bottom: 0;
  3311. left: 0;
  3312. width: 100%;
  3313. height: 50vh;
  3314. z-index: 2000;
  3315. }
  3316. .ojb_btn.exit_button_bottom {
  3317. position: fixed;
  3318. bottom: 30px;
  3319. right: 15px;
  3320. z-index: 2000;
  3321. height: 28px;
  3322. }
  3323.  
  3324. /* monaco */
  3325. #OJBetter_monaco {
  3326. flex: 1;
  3327. min-height: 0;
  3328. width: 100%;
  3329. }
  3330. #OJBetter_monaco .highlight {
  3331. border: 1px solid #ffffff00;
  3332. background-color: #ffffff00!important
  3333. }
  3334. .monaco-hover hr {
  3335. margin: 4px -8px 4px !important;
  3336. }
  3337.  
  3338. /* 状态底栏 */
  3339. #OJBetter_statusBar{
  3340. height: 22px;
  3341. font-size: 12px;
  3342. color: #757575;
  3343. border: 1px solid #d3d3d3;
  3344. background-color: #f8f8f8;
  3345. padding: 3px;
  3346. box-sizing: border-box;
  3347. }
  3348.  
  3349. /* 提交 */
  3350. #OJBetter_submitDiv{
  3351. display: flex;
  3352. padding-top: 15px;
  3353. height: 50px;
  3354. box-sizing: border-box;
  3355. }
  3356. #OJBetter_submitDiv >* {
  3357. border-radius: 6px;
  3358. }
  3359. #OJBetter_submitDiv > button {
  3360. height: 100%;
  3361. aspect-ratio: 1 / 1;
  3362. }
  3363. #SubmitButton {
  3364. color: #fff;
  3365. background-color: #209978;
  3366. border-color: #17795E;
  3367. }
  3368. #SubmitButton:hover {
  3369. background-color: #17795e;
  3370. }
  3371. #SubmitButton.disabled {
  3372. background-color: red;
  3373. animation: shake 0.07s infinite alternate;
  3374. }
  3375. #programTypeId{
  3376. height: 100%;
  3377. padding: 5px 10px;
  3378. border-radius: 6px;
  3379. border-style: solid;
  3380. border: 1px solid #ced4da;
  3381. color: #212529;
  3382. }
  3383.  
  3384. /* 调试 */
  3385. .OJBetter_loding{
  3386. padding: 6px 0px 0px 5px;
  3387. height: 22px;
  3388. }
  3389. #CompilerArgsInput{
  3390. flex-grow: 1;
  3391. width: 100%;
  3392. height: 100%;
  3393. margin-bottom: 10px;
  3394. padding: 5px 10px;
  3395. border-radius: 6px;
  3396. box-sizing: border-box;
  3397. border: 1px solid #ccc;
  3398. box-shadow: inset 0px 1px 1px rgba(0,0,0,.075);
  3399. }
  3400. #CompilerArgsInput[disabled] {
  3401. cursor: not-allowed;
  3402. }
  3403. #CompilerSetting{
  3404. font-size: 14px;
  3405. margin-top: 10px;
  3406. display: none;
  3407. }
  3408. #CompilerSetting select, #CompilerSetting textarea{
  3409. padding: 4px 10px;
  3410. border-radius: 6px;
  3411. border-style: solid;
  3412. border: 1px solid #ced4da;
  3413. color: #212529;
  3414. }
  3415. #CompilerBox{
  3416. display: grid;
  3417. margin-top: 10px;
  3418. border: #d0d7de solid 1px;
  3419. border-radius: 6px;
  3420. }
  3421. #CompilerBox > * {
  3422. margin: 5px;
  3423. }
  3424.  
  3425. /* 自定义样例 */
  3426. #customTestBlock {
  3427. margin-top: 10px;
  3428. font-size: 14px;
  3429. color: #616161;
  3430. border: 1px solid #d3d3d3;
  3431. box-sizing: border-box;
  3432. position: relative;
  3433. }
  3434. #customTestBlock #customTests{
  3435. border-top: 1px solid #d3d3d3;
  3436. margin: 0px 0px 40px 0px;
  3437. }
  3438. #customTestBlock summary {
  3439. cursor: pointer;
  3440. padding: 10px;
  3441. }
  3442. #customTestBlock textarea {
  3443. resize: vertical;
  3444. }
  3445. .sampleDiv {
  3446. color: #727378;
  3447. background-color: #FAFAFA;
  3448. padding: 5px;
  3449. margin-bottom: 10px;
  3450. box-shadow: inset 0 0 1px #0000004d;
  3451. position: relative;
  3452. }
  3453. .dynamicTextarea {
  3454. width: 98%;
  3455. height: 120px;
  3456. margin: 10px 5px;
  3457. border: 1px solid #E0E0E0;
  3458. }
  3459. .deleteCustomTest {
  3460. cursor: pointer;
  3461. position: absolute;
  3462. top: 5px;
  3463. right: 5px;
  3464. display: flex;
  3465. fill: #9E9E9E;
  3466. padding: 2px 2px;
  3467. border-radius: 4px;
  3468. border: 1px solid #ffffff00;
  3469. background-color: #ffffff00;
  3470. align-items: center;
  3471. }
  3472. .deleteCustomTest:hover {
  3473. fill: #EF5350;
  3474. border: 1px solid #ef9a9a;
  3475. background-color: #FFEBEE;
  3476. }
  3477. #addCustomTest {
  3478. cursor: pointer;
  3479. position: absolute;
  3480. bottom: 5px;
  3481. right: 5px;
  3482. padding: 3px 10px;
  3483. color: #795548;
  3484. border: 1px solid #ccc;
  3485. border-radius: 4px;
  3486. background-color: #FAFAFA;
  3487. }
  3488. #addCustomTest:hover {
  3489. background-color: #f5f5f5;
  3490. }
  3491.  
  3492. /* 调试结果 */
  3493. #statePanel{
  3494. display: none;
  3495. padding: 5px;
  3496. margin-top: 10px;
  3497. border: 1px solid #ddd;
  3498. border-radius: 4px;
  3499. }
  3500. .test-case {
  3501. padding: 10px;
  3502. border: 1px solid #ddd;
  3503. border-radius: 4px;
  3504. }
  3505. .test-case:not(:first-child){
  3506. margin-top: 5px;
  3507. }
  3508. .test-case > * {
  3509. margin: 5px 0px;
  3510. }
  3511. .test-case > :first-child {
  3512. margin-top: 0px;
  3513. }
  3514. .test-case > :last-child {
  3515. margin-bottom: 0px;
  3516. }
  3517. .test-case-title, .test-case-status {
  3518. font-size: 16px;
  3519. display: inline;
  3520. }
  3521. .test-case-status{
  3522. margin-left: 5px;
  3523. }
  3524. .test-case-status.error{
  3525. color: red;
  3526. }
  3527. .test-case-status.success{
  3528. color: #449d44;
  3529. }
  3530. .test-case-judge {
  3531. font-size: 13px;
  3532. }
  3533.  
  3534. /* 差异对比 */
  3535. .output_diff {
  3536. color: #5d4037;
  3537. margin: 5px 0px;
  3538. display: grid;
  3539. border: 1px solid #bcaaa4;
  3540. font-size: 13px;
  3541. font-family: Consolas, "Lucida Console", "Andale Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
  3542. overflow: auto;
  3543. }
  3544. .output_diff .added {
  3545. background-color: #c8f7c5;
  3546. user-select: none;
  3547. }
  3548. .output_diff .removed {
  3549. background-color: #f7c5c5;
  3550. }
  3551. .output_diff .diffLine {
  3552. display: flex;
  3553. }
  3554. .output_diff .diffLine:nth-child(odd) {
  3555. background-color: #f5f5f5;
  3556. }
  3557. .lineNo {
  3558. display: flex;
  3559. align-items: center;
  3560. justify-content: center;
  3561. width: 17px;
  3562. color: #BDBDBD;
  3563. font-size: 10px;
  3564. border-right: 1px solid;
  3565. user-select: none;
  3566. }
  3567. .lineContent {
  3568. display: grid;
  3569. width: 100%;
  3570. }
  3571. .lineContent>span {
  3572. height: 16px;
  3573. padding-left: 3px;
  3574. }
  3575. .output_no_diff {
  3576. padding: 5px;
  3577. border: 1px solid #ddd;
  3578. }
  3579. .diff_note {
  3580. font-size: 10px;
  3581. }
  3582.  
  3583. /* 网站本地化替换规则标记 */
  3584. .markingTextReplaceRule{
  3585. color: #FFF3E0;
  3586. background-color: #FF9800;
  3587. }
  3588.  
  3589. /* SelectPage样式 */
  3590. .sp_input {
  3591. padding: 4px 6px px !important;
  3592. height: 20px !important;
  3593. min-height: 20px !important;
  3594. line-height: 20px !important;
  3595. }
  3596. div.sp_clear_btn {
  3597. padding: 0px !important;
  3598. }
  3599.  
  3600. /* 移动设备 */
  3601. @media (max-device-width: 450px) {
  3602. .ojb_btn{
  3603. height: 2em;
  3604. font-size: 1.2em;
  3605. }
  3606. .ojb_btn.OJBetter_setting{
  3607. height: 2.5em;
  3608. font-size: 1em;
  3609. }
  3610. .OJBetter_setting_menu{
  3611. width: 90%;
  3612. }
  3613. .OJBetter_setting_menu label, #darkMode_span, #loaded_span, .OJBetter_setting_menu_label_text,
  3614. .OJBetter_setting_sidebar li{
  3615. font-size: 1em;
  3616. }
  3617. .translate-problem-statement{
  3618. font-size: 1.2em;
  3619. }
  3620. .OJBetter_modal{
  3621. font-size: 1.5em;
  3622. }
  3623. .OJBetter_setting_list, .translate-problem-statement{
  3624. padding: 0.5em;
  3625. }
  3626. .OJBetter_setting_menu_label_text{
  3627. height: 2.5em;
  3628. padding: 0.5em;
  3629. }
  3630. #pagBar #jump-input, #pagBar #items-per-page, .OJBetter_modal button{
  3631. height: 2.5em;
  3632. font-size: 1em;
  3633. }
  3634. .translate-problem-statement p, .translate-problem-statement ul li{
  3635. line-height: 1.5em !important;
  3636. }
  3637. .OJBetter_contextmenu_label_text{
  3638. height: 3em;
  3639. font-size: 1em;
  3640. }
  3641. }
  3642.  
  3643. /* 覆盖网站原本的样式 */
  3644. div#select-lang {
  3645. padding: 0px;
  3646. }
  3647. `);
  3648.  
  3649. /**
  3650. * 添加一些依赖库和条件加载的css样式
  3651. */
  3652. function addDependencyStyles() {
  3653. GM_addStyle(GM_getResourceText("xtermcss"));
  3654. GM_addStyle(GM_getResourceText("selectpagecss"));
  3655. GM_addStyle(GM_getResourceText("dialogpolyfillcss"));
  3656. // 自定义图标大小
  3657. GM_addStyle(`
  3658. .iconfont {
  3659. font-size: ${OJBetter.preference.iconButtonSize}px;
  3660. }
  3661. `);
  3662. }
  3663.  
  3664. /**
  3665. * 添加包含i18n内容的css样式
  3666. */
  3667. function addI18nStyles() {
  3668. GM_addStyle(`
  3669. /* 加载鼠标悬浮覆盖层css */
  3670. .overlay::before {
  3671. content: '';
  3672. position: absolute;
  3673. top: 0;
  3674. left: 0;
  3675. width: 100%;
  3676. height: 100%;
  3677. 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);
  3678. z-index: 100;
  3679. }
  3680. .overlay::after {
  3681. content: '${i18next.t('targetArea', { ns: 'common' })}';
  3682. position: absolute;
  3683. top: 50%;
  3684. left: 50%;
  3685. transform: translate(-50%, -50%);
  3686. color: #00695C;
  3687. font-size: 16px;
  3688. font-weight: bold;
  3689. z-index: 100;
  3690. }
  3691.  
  3692. .config::before {
  3693. content: "${i18next.t('common.configManageTitle', { ns: 'settings' })}";
  3694. display: block;
  3695. height: 20px;
  3696. background-color: #f0f8ff;
  3697. border: 1px solid #c5cae9;
  3698. border-bottom: 0px;
  3699. line-height: 20px;
  3700. padding: 2px 10px;
  3701. border-radius: 8px 8px 0px 0px;
  3702. box-sizing: content-box;
  3703. }
  3704. .config.missing::before {
  3705. content: "${i18next.t('common.missing.radio', { ns: 'settings' })}";
  3706. background-color: #fef0f0;
  3707. color: #f56c6c;
  3708. border: 1px solid #fab6b6;
  3709. }
  3710. `);
  3711. }
  3712.  
  3713. // ------------------------------
  3714. // 一些工具类
  3715. // ------------------------------
  3716.  
  3717.  
  3718. /**
  3719. * 自定义错误类,以区分不同的错误类型
  3720. */
  3721. class OJB_GMError extends Error {
  3722. constructor(type, message, originalError) {
  3723. super(message);
  3724. this.name = 'GMError';
  3725. this.type = type;
  3726. this.stack = originalError.stack;
  3727. Object.assign(this, originalError);
  3728. }
  3729. }
  3730.  
  3731. /**
  3732. * 文本块替换/恢复类
  3733. */
  3734. class TextBlockReplacer {
  3735. constructor() {
  3736. /** @type {string[]} 匹配项 */
  3737. this.matches = [];
  3738. /** @type {Map<string, string>} 待还原项 */
  3739. this.replacements = new Map();
  3740. /** @type {Map<string, string>} 暂时未找到的待还原项 */
  3741. this.tempReplacements = new Map();
  3742. /** @type {string} 替换符号 */
  3743. this.replaceSymbol = OJBetter.translation.replaceSymbol;
  3744. }
  3745.  
  3746. /**
  3747. * 替换文本
  3748. * @param {string} text 原文本
  3749. * @param {RegExp} regex 匹配规则
  3750. * @returns {string} 替换后的文本
  3751. */
  3752. replace(text, regex) {
  3753. this.matches = text.match(regex) || [];
  3754. try {
  3755. for (let i = 0; i < this.matches.length; i++) {
  3756. const match = this.matches[i];
  3757. const id = OJB_getRandomNumber(8);
  3758. let replacement = '';
  3759. switch (this.replaceSymbol) {
  3760. case "1":
  3761. replacement = `【${id}】`;
  3762. break;
  3763. case "2":
  3764. replacement = `{${id}}`;
  3765. break;
  3766. case "3":
  3767. replacement = `[${id}]`;
  3768. break;
  3769. default:
  3770. replacement = `【${id}】`;
  3771. break;
  3772. }
  3773. text = text.replace(match, replacement);
  3774. this.replacements.set(id, match);
  3775. }
  3776. } catch (e) { }
  3777. return text;
  3778. }
  3779.  
  3780.  
  3781. /**
  3782. * 恢复替换的文本
  3783. * @param {string} text 还原前的文本
  3784. * @returns {string} 还原后的文本
  3785. */
  3786. recover(text) {
  3787. let textCopy = text;
  3788.  
  3789. /**
  3790. * 替换回文本
  3791. * @param {string} replacement 替换的文本
  3792. * @param {string} regexPattern 匹配规则
  3793. * @returns {void}
  3794. */
  3795. const replaceText = (replacement, regexPattern) => {
  3796. const latexMatch = '(?<latex_block>\\$\\$(\\\\\\$|[^\\$])*?\\$\\$)|(?<latex_inline>\\$(\\\\\\$|[^\\$])*?\\$)|';
  3797. const regex = new RegExp(latexMatch + regexPattern, 'g');
  3798. textCopy = textCopy.replace(regex, (match, ...args) => {
  3799. // LaTeX中的不替换
  3800. const groups = args[args.length - 1]; // groups是replace方法的最后一个参数
  3801. if (groups.latex_block || groups.latex_inline) return match;
  3802. // 没有空格则加一个
  3803. const offset = args[args.length - 3]; // offset是replace方法的倒数第三个参数
  3804. let leftSpace = "", rightSpace = "";
  3805. if (!/\s/.test(textCopy[offset - 1])) leftSpace = " ";
  3806. if (!/\s/.test(textCopy[offset + match.length])) rightSpace = " ";
  3807. return leftSpace + replacement + rightSpace;
  3808. });
  3809. };
  3810.  
  3811. /**
  3812. * 尝试还原
  3813. * @param {string} replacement 替换的文本
  3814. * @param {string} id 替换的 id
  3815. * @returns {boolean} 是否替换成功
  3816. */
  3817. const tryRecover = (replacement, id) => {
  3818. // 尝试还原,如果还原成功,则从 replacements 中删除
  3819. const originalText = textCopy;
  3820. replaceText(replacement, `【\\s*${id}\\s*】|\\[\\s*${id}\\s*\\]|{\\s*${id}\\s*}`); // 替换符完整匹配(考虑了多出空格的情况)
  3821. replaceText(replacement, `【\\s*${id}(?![】\\d])|(?<![【\\d])${id}\\s*】|\\[\\s*${id}(?![\\]\\d])|(?<![\\[\\d])${id}\\s*\\]|{\\s*${id}(?![}\\d])|(?<![{\\d])${id}\\s*}`); // 替换符部分匹配
  3822.  
  3823. if (textCopy === originalText) {
  3824. // 如果文本没有变化,说明没有找到,加入到 tempReplacements
  3825. this.tempReplacements.set(id, replacement);
  3826. return false;
  3827. } else {
  3828. // 如果文本变化了,说明找到并成功替换,则删除
  3829. this.replacements.delete(id);
  3830. this.tempReplacements.delete(id);
  3831. return true;
  3832. }
  3833. }
  3834.  
  3835. // 处理 replacements 中的项
  3836. this.replacements.forEach((replacement, id) => {
  3837. tryRecover(replacement, id);
  3838. });
  3839.  
  3840. // 处理 tempReplacements 中的项
  3841. while (this.tempReplacements.size > 0) {
  3842. let found = false;
  3843. this.tempReplacements.forEach((replacement, id) => {
  3844. found = tryRecover(replacement, id) || found;
  3845. });
  3846. if (!found) break; // 如果这一轮没有找到任何项,终止循环
  3847. }
  3848.  
  3849. // 如果 tempReplacements 还有未找到的项
  3850. if (this.tempReplacements.size > 0) {
  3851. console.warn("There are still some replacements not found:", this.tempReplacements);
  3852. }
  3853.  
  3854. return textCopy;
  3855. }
  3856. }
  3857.  
  3858. // ------------------------------
  3859. // 一些工具函数
  3860. // ------------------------------
  3861.  
  3862. /**
  3863. * 格式化链接格式
  3864. * @param {string} url 链接字符串
  3865. * @returns {string} 清理后的链接字符串
  3866. */
  3867. function OJB_cleanLink(url) {
  3868. if (url === null || url === undefined) return "";
  3869.  
  3870. // 替换'http://'为'https://'
  3871. let cleanUrl = url.replace(/^http:\/\//i, 'https://');
  3872.  
  3873. // 移除末尾的斜杠
  3874. cleanUrl = cleanUrl.replace(/\/$/, '');
  3875.  
  3876. return cleanUrl;
  3877. }
  3878.  
  3879. /**
  3880. * 深度比较两个对象或数组是否完全相等。
  3881. * @param {any} a - 第一个比较对象。
  3882. * @param {any} b - 第二个比较对象。
  3883. * @returns {boolean} - 如果两个对象或数组深度相等,则返回true,否则返回false。
  3884. */
  3885. function OJB_deepEquals(a, b) {
  3886. if (a === b) return true;
  3887. if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false;
  3888. const keysA = Object.keys(a);
  3889. const keysB = Object.keys(b);
  3890. if (keysA.length !== keysB.length) return false;
  3891. for (let key of keysA) {
  3892. if (!b.hasOwnProperty(key)) return false;
  3893. if (!OJB_deepEquals(a[key], b[key])) return false;
  3894. }
  3895. return true;
  3896. }
  3897.  
  3898. /**
  3899. * 用于封装需要重试的异步函数
  3900. * @param {Function} task 需要封装的异步函数
  3901. * @param {Object} options 配置项
  3902. * @param {Number} options.maxRetries 重试次数,默认为 5
  3903. * @param {Number} options.retryInterval 重试时间间隔,默认为 0 毫秒
  3904. * @param {Function} options.errorHandler 错误处理函数,默认为抛出错误
  3905. * @param {...any} args task 函数的参数
  3906. * @returns {Promise} 返回 Promise
  3907. */
  3908. async function OJB_promiseRetryWrapper(task, {
  3909. maxRetries = 5,
  3910. retryInterval = 0,
  3911. errorHandler = (err) => { throw err }
  3912. } = {}, ...args) {
  3913. let attemptsLeft = maxRetries;
  3914. while (attemptsLeft--) {
  3915. try {
  3916. return await task(...args);
  3917. } catch (err) {
  3918. if (attemptsLeft <= 0) {
  3919. return errorHandler(err, maxRetries, attemptsLeft);
  3920. }
  3921. if (retryInterval > 0) {
  3922. await OJB_delay(retryInterval);
  3923. }
  3924. }
  3925. }
  3926. }
  3927.  
  3928. /**
  3929. * GM_xmlhttpRequest 的 Promise 封装
  3930. * @param {Object} options GM_xmlhttpRequest 的参数
  3931. * @param {Boolean} isStream 是否为流式请求
  3932. * @returns {Promise<OJB_GMError>} 返回 Promise
  3933. */
  3934. function OJB_GMRequest(options, isStream = false) {
  3935. return new Promise((resolve, reject) => {
  3936. GM_xmlhttpRequest({
  3937. ...options,
  3938. ...(isStream ? {
  3939. onloadstart: resolve
  3940. } : {
  3941. onload: resolve
  3942. }),
  3943. onerror: (error) => reject(new OJB_GMError('error', 'An error occurred during the request.', error)),
  3944. ontimeout: (error) => reject(new OJB_GMError('timeout', 'The request timed out.', error)),
  3945. onabort: (error) => reject(new OJB_GMError('abort', 'The request was aborted.', error)),
  3946. });
  3947. });
  3948. }
  3949.  
  3950. /**
  3951. * 获取cookie
  3952. * @param {string} name cookie名称
  3953. * @returns {string} cookie值
  3954. */
  3955. function OJB_getCookie(name) {
  3956. const cookies = document.cookie.split(";");
  3957. for (let i = 0; i < cookies.length; i++) {
  3958. const cookie = cookies[i].trim();
  3959. const [cookieName, cookieValue] = cookie.split("=");
  3960.  
  3961. if (cookieName === name) {
  3962. return decodeURIComponent(cookieValue);
  3963. }
  3964. }
  3965. return "";
  3966. }
  3967.  
  3968. /**
  3969. * 检查是否仍在同一浏览器会话中
  3970. * @param {string} sessionKey - 会话键名,用于标识会话
  3971. * @returns {boolean} - 如果在当前会话中之前已经设置过这个键,则返回true,否则返回false
  3972. */
  3973. function OJB_isSameBrowserSession(sessionKey) {
  3974. const fullCookieName = `OJB_Session_${sessionKey}`;
  3975. const sessionValue = OJB_getCookie(fullCookieName);
  3976. if (sessionValue === "") {
  3977. document.cookie = `${fullCookieName}=true; path=/`;
  3978. return false;
  3979. }
  3980. return true;
  3981. }
  3982.  
  3983. /**
  3984. * 随机数生成
  3985. * @param {number} numDigits 位数
  3986. * @returns {number} 一个随机数
  3987. */
  3988. function OJB_getRandomNumber(numDigits) {
  3989. let min = Math.pow(10, numDigits - 1);
  3990. let max = Math.pow(10, numDigits) - 1;
  3991. return Math.floor(Math.random() * (max - min + 1)) + min;
  3992. }
  3993.  
  3994. /**
  3995. * 随机数生成-范围内
  3996. * @param {number} min 最小值
  3997. * @param {number} max 最大值
  3998. * @returns {number} 一个随机数
  3999. */
  4000. function OJB_getRandomNumberInRange(min, max) {
  4001. return Math.floor(Math.random() * (max - min + 1)) + min;
  4002. }
  4003.  
  4004. /**
  4005. * 防抖函数
  4006. * @param {Function} callback 回调函数
  4007. * @returns {Function}
  4008. */
  4009. function OJB_debounce(callback) {
  4010. let timer;
  4011. let immediateExecuted = false;
  4012. const delay = 500;
  4013. return function () {
  4014. clearTimeout(timer);
  4015. if (!immediateExecuted) { callback.call(this); immediateExecuted = true; }
  4016. timer = setTimeout(() => { immediateExecuted = false; }, delay);
  4017. };
  4018. }
  4019.  
  4020. /**
  4021. * 为元素添加鼠标拖拽支持
  4022. * @param {JQuery<HTMLElement>} element 要添加拖拽支持的元素
  4023. * @returns {void}
  4024. */
  4025. function OJB_addDraggable(element) {
  4026. let isDragging = false;
  4027. let x, y, l, t, nl, nt;
  4028. let isSpecialMouseDown = false; // 选取某些元素时不拖动
  4029.  
  4030. element.on('mousedown', function (e) {
  4031. isSpecialMouseDown = $(e.target).is('label, p, input, textarea, span, select, details, summary');
  4032. if (isSpecialMouseDown) return;
  4033.  
  4034. isDragging = true;
  4035. x = e.clientX;
  4036. y = e.clientY;
  4037. l = element.offset().left - $(window).scrollLeft();
  4038. t = element.offset().top - $(window).scrollTop();
  4039.  
  4040. element.css({ left: l + 'px', top: t + 'px', transform: 'none' });
  4041.  
  4042. $(document).on("mousemove", drag);
  4043. $(document).on("mouseup", stopDrag);
  4044. element.css('cursor', 'all-scroll');
  4045. });
  4046.  
  4047. const drag = (e) => {
  4048. if (!isDragging) return;
  4049. // 不执行拖动操作
  4050. if ($(e.target).is('label, p, input, textarea, span') || isSpecialMouseDown && !$(e.target).is('input, textarea')) return;
  4051. e.preventDefault();
  4052.  
  4053. const nx = e.clientX;
  4054. const ny = e.clientY;
  4055. nl = nx - (x - l);
  4056. nt = ny - (y - t);
  4057. element.css({ transform: `translate(${nx - x}px, ${ny - y}px)` });
  4058. };
  4059.  
  4060. const stopDrag = () => {
  4061. isDragging = false;
  4062. isSpecialMouseDown = false;
  4063. element.css('cursor', 'default');
  4064.  
  4065. // 在停止拖拽后,设置元素的left和top,并还原transform
  4066. element.css({ left: nl + 'px', top: nt + 'px', transform: 'none' });
  4067. $(document).off("mousemove", drag);
  4068. $(document).off("mouseup", stopDrag);
  4069. };
  4070. }
  4071.  
  4072. /**
  4073. * 切换元素的折叠/展开过渡动画
  4074. * @param {HTMLElement} element
  4075. */
  4076. function OJB_toggleCollapseExpand(element) {
  4077. // 设置transitionend事件监听器的函数
  4078. const setTransitionListener = (listener) => {
  4079. const listenerName = `transitionEndListener${Date.now()}`;
  4080. window[listenerName] = listener;
  4081. element.addEventListener('transitionend', listener);
  4082. element.setAttribute('data-transition-end-listener', listenerName);
  4083. };
  4084.  
  4085. // 移除事件监听器的函数
  4086. const removeTransitionListener = () => {
  4087. const transitionEndListenerName = element.getAttribute('data-transition-end-listener');
  4088. if (transitionEndListenerName) {
  4089. element.removeEventListener('transitionend', window[transitionEndListenerName]);
  4090. element.removeAttribute('data-transition-end-listener');
  4091. }
  4092. };
  4093.  
  4094. const collapsed = element.getAttribute('data-collapsed') === 'true';
  4095. const sectionHeight = element.scrollHeight;
  4096.  
  4097. // 移除事件监听器
  4098. removeTransitionListener();
  4099.  
  4100. // 设置初始样式
  4101. element.style.overflow = 'hidden';
  4102. element.style.transition = 'height 0.3s ease-out 0s';
  4103. element.style.height = collapsed ? `0px` : `${sectionHeight}px`;
  4104. element.style.opacity = collapsed ? '' : '1';
  4105.  
  4106. // 需要立即开始动画
  4107. requestAnimationFrame(() => {
  4108. // 设置结束样式
  4109. element.style.height = collapsed ? `${sectionHeight}px` : `0px`;
  4110. });
  4111.  
  4112. const transitionEndListener = (event) => {
  4113. if (event.propertyName === 'height') {
  4114. if (collapsed) {
  4115. // 展开后的设置
  4116. element.style.height = '';
  4117. element.style.overflow = '';
  4118. } else {
  4119. // 折叠后的设置
  4120. element.style.opacity = '0';
  4121. }
  4122. removeTransitionListener();
  4123. }
  4124. };
  4125.  
  4126. setTransitionListener(transitionEndListener);
  4127.  
  4128. // 更新data-collapsed属性
  4129. element.setAttribute('data-collapsed', collapsed ? 'false' : 'true');
  4130. }
  4131.  
  4132. /**
  4133. * 获取外部JSON并转换为Object
  4134. * @param {string} url JSON Url
  4135. * @param {boolean} [nacache=true] 是否不使用缓存
  4136. * @returns {Promise<Object>} JSON Object
  4137. */
  4138. async function OJB_getExternalJSON(url, nacache = true) {
  4139. const response = await OJB_GMRequest({
  4140. method: "GET",
  4141. url: url,
  4142. nocache: nacache
  4143. });
  4144. try {
  4145. return JSON.parse(response.responseText);
  4146. } catch (e) {
  4147. throw new Error(`JSON parse error\n${e}`);
  4148. }
  4149. }
  4150.  
  4151. /**
  4152. * 创建确认对话框dialog
  4153. * @param {string} title 标题
  4154. * @param {string} content 内容
  4155. * @param {string[]} buttons 按钮 (取消 确定) 可以为null
  4156. * @param {boolean} renderMarkdown 是否使用markdown渲染文本
  4157. * @returns {Promise<boolean>} 用户点击了确定按钮返回true, 否则返回false
  4158. */
  4159. function OJB_createDialog(title, content, buttons, renderMarkdown = false) {
  4160. return new Promise(resolve => {
  4161. let contentHtml = content;
  4162.  
  4163. if (renderMarkdown) {
  4164. const md = window.markdownit();
  4165. contentHtml = md.render(content);
  4166. }
  4167.  
  4168. const dialog = OJB_safeCreateJQElement(`
  4169. <dialog class="OJBetter_modal">
  4170. <h2>${title}</h2>
  4171. <div class="content">${contentHtml}</div>
  4172. </dialog>
  4173. `);
  4174. const buttonbox = OJB_safeCreateJQElement(`<div class="buttons"></div>`);
  4175. const cancelButton = OJB_safeCreateJQElement(`<button class="cancelButton">${buttons[0]}</button>`)
  4176. .addClass("secondary");
  4177. const continueButton = OJB_safeCreateJQElement(`<button class="continueButton">${buttons[1]}</button>`);
  4178. if (buttons[0] !== null) buttonbox.append(cancelButton);
  4179. if (buttons[1] !== null) buttonbox.append(continueButton);
  4180. dialog.append(buttonbox);
  4181. $('body').append(dialog);
  4182.  
  4183. OJB_showModal(dialog);
  4184. OJB_addDraggable(dialog);
  4185.  
  4186. continueButton.click(function () {
  4187. OJB_closeAndRemoveModal(dialog);
  4188. resolve(true);
  4189. });
  4190.  
  4191. cancelButton.click(function () {
  4192. OJB_closeAndRemoveModal(dialog);
  4193. resolve(false);
  4194. });
  4195. });
  4196. }
  4197.  
  4198. /**
  4199. * 显示模态对话框并阻止页面滚动,同时考虑滚动条宽度变化和原始marginRight
  4200. * @param {JQuery<HTMLElement>} element
  4201. */
  4202. function OJB_showModal(element) {
  4203. const dialog = element.get(0);
  4204. dialogPolyfill.registerDialog(dialog);
  4205. dialog.showModal();
  4206. OJBetter.state.openDialogCount++;
  4207.  
  4208. if (OJBetter.state.openDialogCount === 1) {
  4209. const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
  4210. // 获取原始的html marginRight,考虑到可能的非数字值,比如auto
  4211. const originalMarginRight = window.getComputedStyle(document.documentElement).marginRight;
  4212. const marginRightValue = parseFloat(originalMarginRight) || 0; // 将非数字值转换为0
  4213.  
  4214. if (scrollbarWidth > 0) {
  4215. // 保存原始的marginRight,并设置新的值以补偿滚动条宽度
  4216. document.documentElement.style.setProperty('--original-margin-right', originalMarginRight);
  4217. document.documentElement.style.marginRight = `${marginRightValue + scrollbarWidth}px`;
  4218. }
  4219.  
  4220. // 保存原始的overflow样式
  4221. document.documentElement.setAttribute('data-original-overflow', document.documentElement.style.overflow);
  4222. document.documentElement.style.overflow = 'hidden';
  4223. }
  4224.  
  4225. const allowScrollIfNeeded = () => {
  4226. OJBetter.state.openDialogCount--;
  4227. if (OJBetter.state.openDialogCount === 0) {
  4228. // 恢复原始的html marginRight和overflow样式
  4229. const originalMarginRight = document.documentElement.style.getPropertyValue('--original-margin-right');
  4230. document.documentElement.style.marginRight = originalMarginRight;
  4231. document.documentElement.style.removeProperty('--original-margin-right');
  4232.  
  4233. const originalOverflow = document.documentElement.getAttribute('data-original-overflow');
  4234. document.documentElement.style.overflow = originalOverflow;
  4235. document.documentElement.removeAttribute('data-original-overflow');
  4236. }
  4237. };
  4238.  
  4239. dialog.addEventListener('close', allowScrollIfNeeded);
  4240. }
  4241.  
  4242. /**
  4243. * 关闭并移除模态对话框
  4244. * @param {JQuery<HTMLElement>} element
  4245. */
  4246. function OJB_closeAndRemoveModal(element) {
  4247. const dialog = element.get(0);
  4248. dialog.close();
  4249. dialog.remove();
  4250. }
  4251.  
  4252. /**
  4253. * 关闭并移除模态对话框
  4254. * @param {JQuery<HTMLElement>} element
  4255. */
  4256. function OJB_closeModal(element) {
  4257. const dialog = element.get(0);
  4258. dialog.close();
  4259. }
  4260.  
  4261. /**
  4262. * 清除i18next的缓存数据并刷新
  4263. */
  4264. function clearI18nextCache() {
  4265. Object.keys(localStorage)
  4266. .filter(key => key.startsWith('i18next_res_'))
  4267. .forEach(key => localStorage.removeItem(key));
  4268. window.location.reload();
  4269. }
  4270.  
  4271. /**
  4272. * 清除网站本地化数据
  4273. */
  4274. async function clearWebsiteL10nData() {
  4275. OJBetter.common.database.localizeSubsData.clear().then(() => {
  4276. console.log('localizeSubsData table has been cleared');
  4277. window.location.reload();
  4278. }).catch((error) => {
  4279. console.error('Failed to clear localizeSubsData table:', error);
  4280. });
  4281. }
  4282.  
  4283. /**
  4284. * 从Pre代码块中获取原始代码
  4285. * @param {HTMLElement} element pre代码块元素
  4286. * @returns {string|null} 代码文本
  4287. */
  4288. function OJB_getCodeFromPre(element) {
  4289. /**
  4290. * 从Ace格式化的代码块中获取原始代码
  4291. * @param {HTMLElement} element pre代码块元素
  4292. * @returns {string} 代码文本
  4293. */
  4294. const getCodeFromAcePre = function (element) {
  4295. const editor = ace.edit(element);
  4296. return editor.getValue();
  4297. }
  4298.  
  4299. /**
  4300. * 从Pretty格式化的代码块中获取原始代码-1
  4301. * 代码直接存放在 pre 元素中
  4302. * @param {HTMLElement} element pre代码块元素
  4303. * @returns {string} 代码文本
  4304. */
  4305. const getCodeFromPrettyPre = function (element) {
  4306. return Array.from(element.querySelectorAll('li')).map(function (li) {
  4307. return li.textContent;
  4308. }).join('\n');
  4309. }
  4310.  
  4311. /**
  4312. * 从Pretty格式化的代码块中获取原始代码-2
  4313. * 代码存放在子元素 code 中
  4314. * @param {HTMLElement} element pre代码块元素
  4315. * @returns {string} 代码文本
  4316. */
  4317. const getCodeFromPreChild = function (element) {
  4318. const code = element.querySelector("code.prettyprint");
  4319. if (code.classList.contains("linenums")) {
  4320. return getCodeFromPrettyPre(element);
  4321. } else {
  4322. return element.querySelector("code.prettyprint").textContent;
  4323. }
  4324. }
  4325.  
  4326. let result;
  4327. if (element.id === "submission-code") {
  4328. result = getCodeFromAcePre(element);
  4329. } else if (element.classList.contains("prettyprint")) {
  4330. result = getCodeFromPrettyPre(element);
  4331. } else if (element.querySelector("code.prettyprint")) {
  4332. result = getCodeFromPreChild(element);
  4333. } else {
  4334. result = "";
  4335. }
  4336. result = result.replace(/\u00A0/g, ''); // 过滤文本中的U+00a0字符(由&nbsp;造成的)
  4337. return result;
  4338. }
  4339.  
  4340. /**
  4341. * 判断代码的语言
  4342. * @param {string} code 代码文本
  4343. * @returns {string} 可能的语言
  4344. */
  4345. function OJB_codeLangDetect(code) {
  4346. result = hljs.highlightAuto(code);
  4347. return result.language;
  4348. }
  4349.  
  4350. /**
  4351. * 获取指定命名空间下的所有i18n翻译键值对。
  4352. *
  4353. * @param {string} namespace - 要获取键值对的i18next命名空间。
  4354. * @returns {Map<string, string>} 一个包含命名空间下所有键值对的Map对象。
  4355. */
  4356. function OJB_getAllI18nKeysForNamespace(namespace) {
  4357. const language = i18next.language; // 获取当前语言
  4358. const resources = i18next.store.data[language]; // 获取当前语言的所有资源
  4359. const nsResources = resources[namespace]; // 获取特定命名空间的资源
  4360. const resultMap = new Map();
  4361.  
  4362. if (nsResources) {
  4363. // 遍历命名空间下的所有键值对,并添加到Map中
  4364. Object.keys(nsResources).forEach(key => {
  4365. resultMap.set(key, nsResources[key]);
  4366. });
  4367. } else {
  4368. console.log(`No resources found for namespace "${namespace}"`);
  4369. }
  4370.  
  4371. return resultMap;
  4372. }
  4373.  
  4374. /**
  4375. * 更新检查
  4376. */
  4377. async function checkScriptVersion() {
  4378. try {
  4379. const versionResponse = await OJB_GMRequest({
  4380. method: "GET",
  4381. url: "https://aowuucdn.oss-accelerate.aliyuncs.com/script/versions.json",
  4382. timeout: 10 * 1e3,
  4383. nocache: true
  4384. });
  4385. const versionData = JSON.parse(versionResponse.responseText);
  4386. const { [OJBetter.state.formatName]: { dev: version_dev, release: version_release } } = versionData;
  4387. const baseUrls = {
  4388. // greasyfork: 'https://update.greatest.deepsurf.us/scripts/465777/Codeforces%20Better%21.user.js',
  4389. greasyfork: 'https://update.greatest.deepsurf.us/scripts/471106/Atcoder%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. $("#main-container").prepend(this._statusElement);
  4517. }
  4518.  
  4519. /**
  4520. * 显示提示信息
  4521. */
  4522. showStatus() {
  4523. this._statusElement.show();
  4524. this._isDisplayed = true;
  4525. }
  4526.  
  4527. /**
  4528. * 隐藏提示信息
  4529. */
  4530. hideStatus() {
  4531. this._statusElement.fadeOut(500);
  4532. this._isDisplayed = false;
  4533. }
  4534.  
  4535. /**
  4536. * 移除提示信息
  4537. */
  4538. removeStatus() {
  4539. this._statusElement.remove();
  4540. this._isDisplayed = false;
  4541. }
  4542.  
  4543. /**
  4544. * 更新提示信息
  4545. * @param {string} text 提示信息文本
  4546. * @param {string} type 提示信息类型,可选值:info, success, warning, error
  4547. * @param {number} timeout 提示信息显示的持续时间(毫秒), 默认为无限长
  4548. */
  4549. updateStatus(text, type = 'info', timeout = Infinity, isMarkdown = false) {
  4550. if (isMarkdown) {
  4551. let md = window.markdownit({
  4552. html: !is_escapeHTML,
  4553. });
  4554. text = md.render(text);
  4555. }
  4556. this._statusElement.html(text).removeClass("alert-info alert-success alert-warning alert-error").addClass(`alert-${type}`);
  4557. if (!this._isDisplayed) {
  4558. this.showStatus();
  4559. }
  4560. if (timeout !== Infinity) {
  4561. setTimeout(() => {
  4562. this.hideStatus();
  4563. }, timeout);
  4564. }
  4565. }
  4566. }
  4567.  
  4568. /**
  4569. * 获取网站本地化的数据
  4570. * @param {*} localizationLanguage 本地化语言
  4571. * @returns {Promise<Object>} 本地化数据
  4572. */
  4573. async function getLocalizeWebsiteJson(localizationLanguage) {
  4574. let data = await OJBetter.common.database.localizeSubsData.get(localizationLanguage);
  4575. let url = localizationLanguage === "zh" ?
  4576. `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/subs/${OJBetter.state.formatName}.json` :
  4577. `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${localizationLanguage}/resources/subs/${OJBetter.state.formatName}.json`;
  4578. if (data) data = data.data;
  4579. if (!data) {
  4580. // 如果本地没有数据,从远端获取并保存
  4581. data = await OJB_getExternalJSON(url);
  4582. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: data });
  4583. } else {
  4584. // 如果本地有数据,检查是否已经在当前会话中尝试过更新
  4585. const sessionKey = `ojb_updateL10nWebsiteJson_${localizationLanguage}`;
  4586. if (!OJB_isSameBrowserSession(sessionKey)) {
  4587. // 如果尚未更新,则在后台更新
  4588. (async () => {
  4589. try {
  4590. const newData = await OJB_getExternalJSON(url);
  4591. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: newData });
  4592. console.log("Website local data has been refreshed!");
  4593. } catch (error) {
  4594. console.error('Failed to update localization data:', error);
  4595. }
  4596. })();
  4597. }
  4598. }
  4599. return data;
  4600. }
  4601.  
  4602. /**
  4603. * 网站本地化替换
  4604. * @returns
  4605. */
  4606. async function localizeWebsite() {
  4607. if (OJBetter.localization.websiteLang === "initial") return;
  4608.  
  4609. // 设置网页语言
  4610. var htmlTag = document.getElementsByTagName("html")[0];
  4611. htmlTag.setAttribute("lang", OJBetter.localization.websiteLang);
  4612.  
  4613. // 获取网站本地化的数据
  4614. var subs = await getLocalizeWebsiteJson(OJBetter.localization.websiteLang);
  4615.  
  4616. /**
  4617. * 文本节点遍历替换
  4618. * @param {JQuery} $nodes jQuery对象
  4619. * @param {Object} textReplaceRules 文本替换规则对象
  4620. * @param {string} key 应用的规则集的名字
  4621. */
  4622. const traverseTextNodes = ($nodes, textReplaceRules, key) => {
  4623. if (!$nodes) return;
  4624.  
  4625. $nodes.each((_, node) => {
  4626. if (node.nodeType === Node.TEXT_NODE) {
  4627. Object.entries(textReplaceRules).forEach(([match, replace]) => {
  4628. try {
  4629. const regex = new RegExp(match, 'g');
  4630. const beforeText = node.textContent;
  4631. node.textContent = node.textContent.replace(regex, replace);
  4632. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4633. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4634. }
  4635. } catch (error) {
  4636. console.error(`Error processing text replacement for match: ${match}`, error);
  4637. }
  4638. });
  4639. } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() !== 'iframe') {
  4640. $(node).contents().each((_, childNode) => {
  4641. traverseTextNodes($(childNode), textReplaceRules, key);
  4642. });
  4643. }
  4644. });
  4645. };
  4646.  
  4647. /**
  4648. * value替换
  4649. * @param {JQuery} $nodes jQuery对象
  4650. * @param {Object} valueReplaceRules 值替换规则对象
  4651. * @param {string} key 应用的规则集的名字
  4652. */
  4653. function traverseValueNodes($nodes, valueReplaceRules, key) {
  4654. if (!$nodes) return;
  4655.  
  4656. $nodes.each(function () {
  4657. let $node = $(this);
  4658. if ($node.is('[value]')) {
  4659. Object.keys(valueReplaceRules).forEach(match => {
  4660. const replace = valueReplaceRules[match];
  4661. const regex = new RegExp(match, 'g');
  4662. let currentValue = $node.val();
  4663. let newValue = currentValue.replace(regex, replace);
  4664. $node.val(newValue);
  4665. if (OJBetter.dev.isRuleMarkingEnabled) {
  4666. if (newValue !== currentValue) $($node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4667. }
  4668. });
  4669. } else {
  4670. $node.children().each(function () {
  4671. traverseValueNodes($(this), valueReplaceRules, key);
  4672. });
  4673. }
  4674. });
  4675. }
  4676.  
  4677. /**
  4678. * 严格的文本节点遍历替换
  4679. * 要求被替换文本严格与规则文本一致
  4680. * @param {JQuery} $nodes jQuery对象
  4681. * @param {Object} textReplaceRules 文本替换规则对象
  4682. * @param {string} key 应用的规则集的名字
  4683. */
  4684. const strictTraverseTextNodes = ($nodes, textReplaceRules, key) => {
  4685. if (!$nodes) return;
  4686.  
  4687. $nodes.each((_, node) => {
  4688. if (node.nodeType === Node.TEXT_NODE) {
  4689. const trimmedNodeText = node.textContent.trim();
  4690. for (const [match, replacement] of Object.entries(textReplaceRules)) {
  4691. if (trimmedNodeText === match) {
  4692. const beforeText = node.textContent;
  4693. node.textContent = replacement;
  4694. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4695. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4696. }
  4697. }
  4698. }
  4699. } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() !== 'iframe') {
  4700. $(node).contents().each((_, childNode) => {
  4701. strictTraverseTextNodes($(childNode), textReplaceRules, key);
  4702. });
  4703. }
  4704. });
  4705. };
  4706.  
  4707. /**
  4708. * 应用文本替换
  4709. */
  4710. let commonReplacements = subs.commonReplacements;
  4711. Object.entries(commonReplacements).forEach(([key, value]) => {
  4712. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4713. classSelectors.forEach(classSelector => {
  4714. if (value.isStrict) {
  4715. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4716. } else {
  4717. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4718. }
  4719. });
  4720. });
  4721.  
  4722. /**
  4723. * 应用value替换
  4724. */
  4725. let InputValueReplacements = subs.InputValueReplacements;
  4726. Object.entries(InputValueReplacements).forEach(([key, value]) => {
  4727. const classSelectors = Array.isArray(value.class) ? value.class : [value.class];
  4728. classSelectors.forEach(classSelector => {
  4729. traverseValueNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4730. });
  4731. });
  4732.  
  4733. /**
  4734. * 动态添加的文本的替换
  4735. */
  4736. let dynamicReplacements = subs.dynamicReplacements;
  4737. Object.entries(dynamicReplacements).forEach(([key, value]) => {
  4738. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4739. classSelectors.forEach(classSelector => {
  4740. OJB_observeElement({
  4741. selector: classSelector,
  4742. callback: (node) => {
  4743. // let popupContent = node.textContent.replace(/^×/, ''); // 去除开头多余的 '×' 字符
  4744. if (value.isStrict) {
  4745. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4746. } else {
  4747. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4748. }
  4749. }
  4750. });
  4751. });
  4752. });
  4753.  
  4754. // // 杂项
  4755. // (function () {
  4756. // // 选项汉化input[type="radio"]
  4757. // var translations = {
  4758. // "as individual participant": "个人",
  4759. // "as a team member": "作为一个团队成员",
  4760. // };
  4761. // $('input[type="radio"]').each(function () {
  4762. // var tag = $(this).parent().contents().filter(function () {
  4763. // return this.nodeType === Node.TEXT_NODE;
  4764. // });
  4765. // for (var i = 0; i < tag.length; i++) {
  4766. // var text = tag[i].textContent.trim();
  4767. // if (translations.hasOwnProperty(text)) {
  4768. // $(this).addClass(text);
  4769. // tag[i].replaceWith(translations[text]);
  4770. // break;
  4771. // }
  4772. // }
  4773. // });
  4774. // })();
  4775. // (function () {
  4776. // var translations = {
  4777. // "(standard input\/output)": "标准输入/输出",
  4778. // };
  4779. // $("div.notice").each(function () {
  4780. // var tag = $(this).children().eq(0).text();
  4781. // for (var property in translations) {
  4782. // if (tag.match(property)) {
  4783. // $(this).children().eq(0).text(translations[property]);
  4784. // break;
  4785. // }
  4786. // }
  4787. // });
  4788. // })();
  4789.  
  4790. // // 轻量站特殊
  4791. // if (OJBetter.typeOfPage.is_mSite) {
  4792. // traverseTextNodes($('nav'), commonReplacements['.second-level-menu']['rules']);
  4793. // }
  4794. // if (OJBetter.typeOfPage.is_mSite) {
  4795. // (function () {
  4796. // var translations = {
  4797. // "Announcements": "公告",
  4798. // "Submissions": "提交记录",
  4799. // "Contests": "比赛",
  4800. // };
  4801. // $(".caption").each(function () {
  4802. // var optionValue = $(this).text();
  4803. // if (translations[optionValue]) {
  4804. // $(this).text(translations[optionValue]);
  4805. // }
  4806. // });
  4807. // })();
  4808. // }
  4809. };
  4810.  
  4811. /**
  4812. * i18next初始化
  4813. */
  4814. async function initI18next() {
  4815. return new Promise((resolve, reject) => {
  4816. i18next
  4817. .use(i18nextChainedBackend)
  4818. .init({
  4819. lng: OJBetter.localization.scriptLang,
  4820. ns: ['common', 'settings', 'config', 'dialog', 'alert', 'translator',
  4821. 'button', 'codeEditor', 'comments', 'announce', 'logMessage'], // 命名空间列表
  4822. defaultNS: 'settings',
  4823. fallbackLng: ['zh', OJBetter.translation.targetLang],
  4824. load: 'currentOnly',
  4825. debug: false,
  4826. backend: {
  4827. backends: [
  4828. i18nextLocalStorageBackend,
  4829. i18nextHttpBackend
  4830. ],
  4831. backendOptions: [{
  4832. prefix: 'i18next_res_',
  4833. expirationTime: 7 * 24 * 60 * 60 * 1000,
  4834. defaultVersion: `v${OJBetter.state.version}`,
  4835. store: typeof window !== 'undefined' ? window.localStorage : null
  4836. }, {
  4837. /* options for secondary backend */
  4838. loadPath: (lng, ns) => {
  4839. if (lng[0] === 'zh' || lng[0] === 'zh-Hans') {
  4840. return `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4841. }
  4842. return `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${lng}/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4843. }
  4844. }]
  4845. }
  4846. }, (err, t) => {
  4847. if (err) {
  4848. reject(err);
  4849. } else {
  4850. jqueryI18next.init(i18next, $, {
  4851. useOptionsAttr: true
  4852. });
  4853. resolve(t);
  4854. }
  4855. });
  4856. });
  4857. };
  4858.  
  4859. /**
  4860. * 抽象命令类
  4861. */
  4862. class Command {
  4863. execute() { }
  4864. undo() { }
  4865. }
  4866.  
  4867. /**
  4868. * 命令调用者
  4869. */
  4870. class CommandInvoker {
  4871. constructor() {
  4872. this.history = [];
  4873. }
  4874.  
  4875. /**
  4876. * 执行命令
  4877. * @param {Command} command 命令对象
  4878. */
  4879. execute(command) {
  4880. this.history.push(command);
  4881. command.execute();
  4882. }
  4883.  
  4884. /**
  4885. * 撤销命令
  4886. */
  4887. undo() {
  4888. const command = this.history.pop();
  4889. if (command) {
  4890. command.undo();
  4891. }
  4892. }
  4893. }
  4894.  
  4895. /**
  4896. * 接收者
  4897. */
  4898. class DOMContainer {
  4899. /**
  4900. * @param {JQueryObject} element 容器对象
  4901. */
  4902. constructor(element) {
  4903. this.containerElement = element;
  4904. }
  4905.  
  4906. /**
  4907. * 添加元素
  4908. * @param {JQueryObject} element 元素对象
  4909. * @returns {JQueryObject} 添加的元素对象
  4910. */
  4911. add(element) {
  4912. this.containerElement.append(element);
  4913. return this.containerElement.children().last();
  4914. }
  4915.  
  4916. /**
  4917. * 删除元素
  4918. * @param {JQueryObject} element 元素对象
  4919. */
  4920. remove(element) {
  4921. $(element).remove();
  4922. }
  4923. }
  4924.  
  4925. /**
  4926. * 具体命令类:添加元素
  4927. */
  4928. class AddElementCommand extends Command {
  4929. /**
  4930. * @param {DOMContainer} receiver 接收者
  4931. * @param {JQueryObject} element 元素对象
  4932. */
  4933. constructor(receiver, element) {
  4934. super();
  4935. this.receiver = receiver;
  4936. this.element = element;
  4937. this.addedElement = null;
  4938. }
  4939.  
  4940. execute() {
  4941. this.addedElement = this.receiver.add(this.element);
  4942. }
  4943.  
  4944. undo() {
  4945. if (this.addedElement) {
  4946. this.receiver.remove(this.addedElement);
  4947. }
  4948. }
  4949. }
  4950.  
  4951. /**
  4952. * 具体命令类:删除元素
  4953. */
  4954. class RemoveElementCommand extends Command {
  4955. /**
  4956. * @param {DOMContainer} receiver 接收者
  4957. * @param {JQueryObject} element 元素对象
  4958. */
  4959. constructor(receiver, element) {
  4960. super();
  4961. this.receiver = receiver;
  4962. this.element = element;
  4963. this.parent = $(element).parent();
  4964. this.nextSibling = $(element).next();
  4965. }
  4966.  
  4967. execute() {
  4968. this.receiver.remove(this.element);
  4969. }
  4970.  
  4971. undo() {
  4972. if (this.nextSibling.length > 0) {
  4973. $(this.element).insertBefore(this.nextSibling);
  4974. } else {
  4975. this.parent.append(this.element);
  4976. }
  4977. }
  4978. }
  4979.  
  4980. /**
  4981. * 验证器
  4982. */
  4983. class Validator {
  4984. /**
  4985. * 表单必填项空值校验
  4986. */
  4987. static required(structure) {
  4988. let config = {};
  4989. let allFieldsValid = true;
  4990. for (const key in structure) {
  4991. let value = key.type == 'checkbox' ?
  4992. $(key).prop("checked") : $(key).val();
  4993.  
  4994. config[structure[key].value] = value;
  4995.  
  4996. if (value || structure[key].require === false) {
  4997. $(key).removeClass('is_null');
  4998. } else {
  4999. $(key).addClass('is_null');
  5000. allFieldsValid = false;
  5001. }
  5002. }
  5003. return {
  5004. valid: allFieldsValid,
  5005. config: config
  5006. };
  5007. }
  5008.  
  5009. /**
  5010. * 表单合法性校验
  5011. */
  5012. static checkKeyValuePairs(structure, config) {
  5013. let errorKeys = [];
  5014. let allFieldsValid = true;
  5015.  
  5016. for (const key in structure) {
  5017. const { check, value } = structure[key];
  5018. const fieldValue = config[value];
  5019.  
  5020. // 如果字段没有值或校验类型不匹配,则跳过当前迭代
  5021. if (!fieldValue) continue;
  5022.  
  5023. let isValid = true;
  5024. switch (check) {
  5025. case 'keyValuePairs':
  5026. isValid = Validator.keyValuePairs(fieldValue);
  5027. break;
  5028. case 'dotSeparatedPath':
  5029. isValid = Validator.validateDotSeparatedPath(fieldValue);
  5030. break;
  5031. default:
  5032. // 没有匹配的校验类型
  5033. continue;
  5034. }
  5035.  
  5036. Validator.toggleErrorDisplay(key, isValid);
  5037. if (!isValid) {
  5038. allFieldsValid = false;
  5039. errorKeys.push(key);
  5040. }
  5041. }
  5042.  
  5043. return {
  5044. valid: allFieldsValid,
  5045. errorKeys: errorKeys
  5046. };
  5047. }
  5048.  
  5049. /**
  5050. * 切换错误信息的显示和隐藏
  5051. * @param {string} key - 字段的键
  5052. * @param {boolean} isValid - 字段值是否有效
  5053. */
  5054. static toggleErrorDisplay(key, isValid) {
  5055. const errorMessage = i18next.t('common.unValid', { ns: 'settings' });
  5056. const $errorSpan = $(key).prev('span.text-error');
  5057. if (!isValid) {
  5058. if (!$errorSpan.length) {
  5059. $(key).before(`<span class="text-error" style="color: red;">${errorMessage}</span>`);
  5060. }
  5061. } else {
  5062. $errorSpan.remove();
  5063. }
  5064. }
  5065.  
  5066. /**
  5067. * 键值对合法性校验
  5068. * @param {string} value
  5069. * @returns {boolean}
  5070. */
  5071. static keyValuePairs(value) {
  5072. const keyValuePairs = value.split('\n');
  5073. // 允许值中包含空格和冒号
  5074. const regex = /^[a-zA-Z0-9_-]+\s*:\s*.+$/;
  5075. return keyValuePairs.every(pair => regex.test(pair));
  5076. }
  5077.  
  5078.  
  5079. /**
  5080. * 点分隔符路径格式校验,允许加减运算
  5081. * @param {string} path
  5082. * @returns {boolean}
  5083. */
  5084. static validateDotSeparatedPath(path) {
  5085. // 正则表达式允许标识符之间有点号,标识符可以包含加减运算
  5086. const regex = /^([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*\.)*([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*)$/;
  5087. return regex.test(path);
  5088. }
  5089. }
  5090.  
  5091. /**
  5092. * 配置管理
  5093. */
  5094. class ConfigManager {
  5095. /**
  5096. * @param {HTMLElement} element - 挂载容器
  5097. * @param {string} prefix - 前缀
  5098. * @param {object} tempConfig - 配置内容
  5099. * @param {object} structure - 配置结构
  5100. * @param {object} configHTML - 配置编辑页面HTML
  5101. * @param {boolean} allowChoice - 是否允许选择列表项
  5102. */
  5103. constructor(element, prefix, tempConfig, structure, configHTML, allowChoice = true) {
  5104. /** @param 设置面板DIV */
  5105. this.settingMenuDiv = $('#OJBetter_setting_menu');
  5106. this.element = $(element);
  5107. this.prefix = prefix;
  5108. this.tempConfig = tempConfig;
  5109. this.structure = structure;
  5110. this.configHTML = configHTML;
  5111. this.allowChoice = allowChoice;
  5112.  
  5113. this.controlTip = null;
  5114. this.config_bar_list = null;
  5115. this.config_bar_ul = null;
  5116. this.config_add_button = null;
  5117. this.menu = null;
  5118. this.editItem = null;
  5119. this.deleteItem = null;
  5120.  
  5121. // 绑定方法
  5122. this.onAdd = this.onAdd.bind(this);
  5123. this.onEdit = this.onEdit.bind(this);
  5124. this.onDelete = this.onDelete.bind(this);
  5125. this.createListItemElement = this.createListItemElement.bind(this);
  5126.  
  5127. this.lastItemId = 0; // 列表中当前最后一个元素的id号
  5128. this.init();
  5129. }
  5130.  
  5131. init() {
  5132. this.createControlBar();
  5133. this.createContextMenu();
  5134. this.renderList();
  5135. }
  5136.  
  5137. /**
  5138. * 创建控制栏
  5139. */
  5140. createControlBar() {
  5141. this.controlTip = OJB_safeCreateJQElement(`<div id='${this.prefix}configControlTip' style='color:red;'></div>`);
  5142. this.config_bar_list = OJB_safeCreateJQElement(`<div class='config_bar_list' id='${this.prefix}config_bar_list'></div>`);
  5143. this.config_bar_ul = OJB_safeCreateJQElement(`<ul class='config_bar_ul' id='${this.prefix}config_bar_ul'></ul>`);
  5144. this.element.append(this.controlTip);
  5145. this.element.append(this.config_bar_list);
  5146. this.config_bar_list.append(this.config_bar_ul);
  5147. }
  5148.  
  5149. /**
  5150. * 创建右键菜单
  5151. */
  5152. createContextMenu() {
  5153. const menu = OJB_safeCreateJQElement(`<div id='config_bar_menu' style='display: none;'></div>`);
  5154. const editItem = OJB_safeCreateJQElement(`
  5155. <div class='config_bar_menu_item' id='config_bar_menu_edit'>
  5156. ${i18next.t('contextMenu.edit', { ns: 'translator' })}
  5157. </div>`);
  5158. const deleteItem = OJB_safeCreateJQElement(`
  5159. <div class='config_bar_menu_item' id='config_bar_menu_delete'>
  5160. ${i18next.t('contextMenu.delete', { ns: 'translator' })}
  5161. </div>`);
  5162. menu.append(editItem);
  5163. menu.append(deleteItem);
  5164. this.editItem = editItem;
  5165. this.deleteItem = deleteItem;
  5166. this.menu = menu;
  5167. this.settingMenuDiv.append(menu);
  5168. }
  5169.  
  5170. /**
  5171. * 关闭右键菜单
  5172. */
  5173. closeContextMenu() {
  5174. this.menu.css({ display: "none" });
  5175. }
  5176.  
  5177. /**
  5178. * 创建列表项
  5179. * @param {string} text - 列表项文本
  5180. * @returns {HTMLElement} - 列表项
  5181. */
  5182. createListItemElement(text) {
  5183. const id = OJB_getRandomNumber(4);
  5184. const li = $("<li></li>");
  5185. const radio = OJB_safeCreateJQElement(`<input type='radio' name='${this.prefix}config_item'></input>`)
  5186. .attr("value", text)
  5187. .attr("id", id)
  5188. .attr("prev_id", this.lastItemId)
  5189. .appendTo(li);
  5190. if (!this.allowChoice) {
  5191. radio.prop("disabled", true);
  5192. }
  5193. const label = OJB_safeCreateJQElement(`<label for='${id}' class='config_bar_ul_li_text'>${text}</label>`).appendTo(li);
  5194.  
  5195.  
  5196. this.lastItemId = id;
  5197.  
  5198. // 添加右键菜单
  5199. li.on("contextmenu", (event) => {
  5200. event.preventDefault();
  5201. this.menu.css({
  5202. display: "block",
  5203. left: event.pageX, top: event.pageY
  5204. });
  5205.  
  5206. const deleteItem = this.deleteItem;
  5207. const editItem = this.editItem;
  5208.  
  5209. // 移除旧事件
  5210. deleteItem.off("click");
  5211. editItem.off("click");
  5212.  
  5213. // 获取 li 在 ul 中的索引
  5214. const index = li.index();
  5215.  
  5216. deleteItem.on("click", () => this.onDelete(index, li));
  5217. editItem.on("click", () => this.onEdit(index, li));
  5218.  
  5219. $(document).one("click", (event) => {
  5220. if (!this.menu.get(0).contains(event.target)) {
  5221. this.closeContextMenu();
  5222. deleteItem.off("click", () => this.onDelete);
  5223. editItem.off("click", () => this.onEdit);
  5224. }
  5225. });
  5226. });
  5227.  
  5228. return li;
  5229. }
  5230.  
  5231. /**
  5232. * 渲染配置列表
  5233. */
  5234. renderList() {
  5235. const list = this.config_bar_ul;
  5236. list.empty(); // 清空
  5237. this.tempConfig.configurations.forEach((item) => {
  5238. list.append(this.createListItemElement(item['name']));
  5239. });
  5240.  
  5241. // 添加按钮
  5242. let addButton = OJB_safeCreateJQElement(`<li id='${this.prefix}add_button' class="tempConfig_add_button">
  5243. <span>+ ${i18next.t('add', { ns: 'common' })}</span>
  5244. </li>`);
  5245. this.config_add_button = addButton;
  5246. list.append(addButton);
  5247. addButton.on("click", this.onAdd);
  5248. }
  5249.  
  5250. /**
  5251. * 添加配置项
  5252. */
  5253. onAdd() {
  5254. const configMenu = this.createConfigHTML();
  5255. const structure = this.structure;
  5256.  
  5257. configMenu.on("click", "#tempConfig_save", () => {
  5258.  
  5259. // 检查必填字段
  5260. const { valid, config } = Validator.required(structure);
  5261. if (!valid) return;
  5262.  
  5263. // 检查键值对
  5264. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5265. if (!checkOk) return;
  5266.  
  5267. this.tempConfig.configurations.push(config);
  5268.  
  5269. this.createListItemElement(config.name).insertBefore(this.config_add_button);
  5270.  
  5271. configMenu.remove();
  5272. });
  5273.  
  5274. configMenu.on("click", ".btn-close", () => {
  5275. configMenu.remove();
  5276. });
  5277. }
  5278.  
  5279. /**
  5280. * 修改配置项
  5281. * @param {number} index - 配置项索引
  5282. * @param {HTMLElement} li - 配置项
  5283. * @returns {void}
  5284. */
  5285. onEdit(index, li) {
  5286. const configMenu = this.createConfigHTML();
  5287. const structure = this.structure;
  5288.  
  5289. this.closeContextMenu();
  5290.  
  5291. // 填充表单
  5292. for (const [key, { value, type }] of Object.entries(this.structure)) {
  5293. const configValue = this.tempConfig.configurations[index][value];
  5294. const $element = $(key);
  5295. if (type === 'checkbox') {
  5296. $element.prop("checked", configValue);
  5297. } else {
  5298. $element.val(configValue);
  5299. }
  5300. }
  5301.  
  5302. configMenu.on("click", "#tempConfig_save", () => {
  5303. // 检查必填字段
  5304. const { valid, config } = Validator.required(structure);
  5305. if (!valid) return;
  5306.  
  5307. // 检查键值对
  5308. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5309. if (!checkOk) return;
  5310.  
  5311. // 更新配置
  5312. this.tempConfig.configurations[index] = config;
  5313. li.find('label').text(config.name);
  5314.  
  5315. OJB_closeAndRemoveModal(configMenu);
  5316. });
  5317.  
  5318. configMenu.on("click", ".btn-close", () => {
  5319. OJB_closeAndRemoveModal(configMenu);
  5320. });
  5321. }
  5322.  
  5323. /**
  5324. * 删除配置项
  5325. * @param {number} index - 配置项索引
  5326. * @param {HTMLElement} li - 配置项
  5327. * @returns {void}
  5328. */
  5329. onDelete(index, li) {
  5330. this.closeContextMenu();
  5331. this.tempConfig.configurations.splice(index, 1);
  5332. li.remove();
  5333. }
  5334.  
  5335. /**
  5336. * 创建配置编辑页面
  5337. * @returns {JQuery<HTMLElement>} 返回配置编辑页面
  5338. */
  5339. createConfigHTML() {
  5340. const configMenu = OJB_safeCreateJQElement(this.configHTML);
  5341. this.settingMenuDiv.after(configMenu);
  5342. OJB_showModal(configMenu);
  5343. OJB_addDraggable(configMenu);
  5344. elementLocalize(configMenu);
  5345. return configMenu;
  5346. }
  5347.  
  5348. /**
  5349. * 获取配置内容
  5350. * @returns {object} - 配置内容
  5351. */
  5352. getTempConfig() {
  5353. return this.tempConfig;
  5354. }
  5355.  
  5356. /**
  5357. * 注册列表项选中改变监听
  5358. */
  5359. registerChoiceChange() {
  5360. this.config_bar_ul.on("change", "input[type='radio']", (event) => {
  5361. const value = event.target.value;
  5362. this.tempConfig.choice = value;
  5363. });
  5364. }
  5365. }
  5366.  
  5367. const OJBetter_setting_sidebar_HTML = `
  5368. <div class="OJBetter_setting_sidebar">
  5369. <ul>
  5370. <li><a href="#basic-settings" id="sidebar-basic-settings" class="active" data-i18n="settings:sidebar.basic"></a></li>
  5371. <li><a href="#preference-settings" id="sidebar-preference-settings" data-i18n="settings:sidebar.preference"></a></li>
  5372. <li><a href="#translation-settings" id="sidebar-translation-settings" data-i18n="settings:sidebar.translation"></a></li>
  5373. <li><a href="#clist_rating-settings" id="sidebar-clist_rating-settings" data-i18n="settings:sidebar.clist"></a></li>
  5374. <li><a href="#code_editor-settings" id="sidebar-code_editor-settings" data-i18n="settings:sidebar.monaco"></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="scriptL10nLanguage" style="display: flex;" data-i18n="settings:localization.scriptLanguageLabel"></label>
  5410. <select id="scriptL10nLanguage" name="scriptL10nLanguage">
  5411. <option value="zh">简体中文</option>
  5412. <option value="zh-Hant">繁體中文</option>
  5413. <option value="en">English</option>
  5414. <option value="de">Deutsch</option>
  5415. <option value="fr">Français</option>
  5416. <option value="ko">한국어</option>
  5417. <option value="pt">Português</option>
  5418. <option value="ja">日本語</option>
  5419. <option value="es">Español</option>
  5420. <option value="it">Italiano</option>
  5421. <option value="hi">हिन्दी</option>
  5422. </select>
  5423. </div>
  5424. <div class='OJBetter_setting_list'>
  5425. <label for="localizationLanguage" style="display: flex;" data-i18n="settings:localization.websiteLanguageLabel"></label>
  5426. <select id="localizationLanguage" name="localizationLanguage">
  5427. <option value="initial">——</option>
  5428. <option value="zh">简体中文</option>
  5429. <option value="zh-Hant">繁體中文</option>
  5430. <option value="de">Deutsch</option>
  5431. <option value="fr">Français</option>
  5432. <option value="ko">한국어</option>
  5433. <option value="pt">Português</option>
  5434. <option value="ja">日本語</option>
  5435. <option value="es">Español</option>
  5436. <option value="it">Italiano</option>
  5437. <option value="hi">हिन्दी</option>
  5438. </select>
  5439. </div>
  5440. <div class='OJBetter_setting_list alert_tip'>
  5441. <div data-i18n="[html]settings:localization.notice.1"></div>
  5442. </div>
  5443. <div class='OJBetter_setting_list alert_tip'>
  5444. <div data-i18n="[html]settings:localization.notice.2"></div>
  5445. </div>
  5446. </div>
  5447. `;
  5448.  
  5449.  
  5450. const translation_settings_HTML = `
  5451. <div id="translation-settings" class="settings-page">
  5452. <h3 data-i18n="settings:translation.title"></h3>
  5453. <hr>
  5454. <h4 data-i18n="settings:translation.options.title"></h4>
  5455. <div class='OJBetter_setting_list'>
  5456. <label for="transTargetLang" style="display: flex;" data-i18n="settings:translation.preference.target.title"></label>
  5457. <div class="help_tip">
  5458. ${helpCircleHTML}
  5459. <div class="tip_text" data-i18n="[html]settings:translation.preference.target.helpText"></div>
  5460. </div>
  5461. <select id="transTargetLang" name="transTargetLang">
  5462. <option value="zh">简体中文</option>
  5463. <option value="zh-Hant">繁體中文</option>
  5464. <option value="de">Deutsch</option>
  5465. <option value="fr">Français</option>
  5466. <option value="ko">한국어</option>
  5467. <option value="pt">Português</option>
  5468. <option value="ja">日本語</option>
  5469. <option value="es">Español</option>
  5470. <option value="it">Italiano</option>
  5471. <option value="hi">हिन्दी</option>
  5472. </select>
  5473. </div>
  5474. <div id="translationServices">
  5475. <label>
  5476. <input type='radio' name='translation' value='deepl'>
  5477. <span class='OJBetter_setting_menu_label_text'
  5478. data-i18n="settings:translation.options.services.deepl"></span>
  5479. </label>
  5480. <label>
  5481. <input type='radio' name='translation' value='iflyrec'>
  5482. <span class='OJBetter_setting_menu_label_text'
  5483. data-i18n="settings:translation.options.services.iflyrec"></span>
  5484. </label>
  5485. <label>
  5486. <input type='radio' name='translation' value='youdao'>
  5487. <span class='OJBetter_setting_menu_label_text'
  5488. data-i18n="settings:translation.options.services.youdao"></span>
  5489. </label>
  5490. <label>
  5491. <input type='radio' name='translation' value='google'>
  5492. <span class='OJBetter_setting_menu_label_text'
  5493. data-i18n="settings:translation.options.services.google"></span>
  5494. </label>
  5495. <label>
  5496. <input type='radio' name='translation' value='caiyun'>
  5497. <span class='OJBetter_setting_menu_label_text'
  5498. data-i18n="settings:translation.options.services.caiyun"></span>
  5499. </label>
  5500. <label>
  5501. <input type='radio' name='translation' value='openai'>
  5502. <span class='OJBetter_setting_menu_label_text'
  5503. data-i18n="settings:translation.options.services.openai.name">
  5504. <div class="help_tip">
  5505. ${helpCircleHTML}
  5506. <div class="tip_text"
  5507. data-i18n="[html]settings:translation.options.services.openai.helpText"></div>
  5508. </div>
  5509. </span>
  5510. </label>
  5511. </div>
  5512. <hr>
  5513. <h4>DeepL</h4>
  5514. <div class='OJBetter_setting_list'>
  5515. <label for="deepl_type" style="display: flex;" data-i18n="settings:translation.deepl.mode.title"></label>
  5516. <div class="help_tip">
  5517. ${helpCircleHTML}
  5518. <div class="tip_text" data-i18n="[html]settings:translation.deepl.mode.helpText"></div>
  5519. </div>
  5520. <select id="deepl_type" name="deepl_type">
  5521. <option value="free" data-i18n="settings:translation.deepl.mode.select.free"></option>
  5522. <option value="api" data-i18n="settings:translation.deepl.mode.select.api"></option>
  5523. </select>
  5524. </div>
  5525. <div id="deepl_config" class="config"></div>
  5526. <div class='OJBetter_setting_list'>
  5527. <label for="enableEmphasisProtection" data-i18n="settings:translation.deepl.enableEmphasisProtection.title"></label>
  5528. <div class="help_tip" style="margin-right: initial;">
  5529. ${helpCircleHTML}
  5530. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableEmphasisProtection.helpText"></div>
  5531. </div>
  5532. <div class="badge">Official API Only</div>
  5533. <input type="checkbox" id="enableEmphasisProtection" name="enableEmphasisProtection">
  5534. </div>
  5535. <div class='OJBetter_setting_list'>
  5536. <label for="enableLinkProtection" data-i18n="settings:translation.deepl.enableLinkProtection.title"></label>
  5537. <div class="help_tip" style="margin-right: initial;">
  5538. ${helpCircleHTML}
  5539. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableLinkProtection.helpText"></div>
  5540. </div>
  5541. <div class="badge">Official API Only</div>
  5542. <input type="checkbox" id="enableLinkProtection" name="enableLinkProtection">
  5543. </div>
  5544. <hr>
  5545. <h4>ChatGPT</h4>
  5546. <div id="chatgpt_config" class="config"></div>
  5547. <div class='OJBetter_setting_list'>
  5548. <label for="openai_isStream" data-i18n="settings:translation.chatgpt.isStream.name"></label>
  5549. <div class="help_tip">
  5550. ${helpCircleHTML}
  5551. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.isStream.helpText"></div>
  5552. </div>
  5553. <input type="checkbox" id="openai_isStream" name="openai_isStream">
  5554. </div>
  5555. <div class='OJBetter_setting_list'>
  5556. <label for="openai_asSystemPrompt" data-i18n="settings:translation.chatgpt.asSystemPrompt.name"></label>
  5557. <div class="help_tip">
  5558. ${helpCircleHTML}
  5559. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.asSystemPrompt.helpText"></div>
  5560. </div>
  5561. <input type="checkbox" id="openai_asSystemPrompt" name="openai_asSystemPrompt">
  5562. </div>
  5563. <div class="OJBetter_setting_list">
  5564. <label for='openai_customPrompt'>
  5565. <div style="display: flex;align-items: center;">
  5566. <span class="input_label" data-i18n="settings:translation.chatgpt.customPrompt.name"></span>
  5567. <div class="help_tip">
  5568. ${helpCircleHTML}
  5569. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.customPrompt.helpText"></div>
  5570. </div>
  5571. </div>
  5572. </label>
  5573. <textarea id="openai_customPrompt" placeholder='' require = false data-i18n="[placeholder]settings:translation.chatgpt.customPrompt.placeholder"></textarea>
  5574. </div>
  5575. <hr>
  5576. <h4 data-i18n="settings:translation.preference.title"></h4>
  5577. <div class='OJBetter_setting_list'>
  5578. <label for="comment_translation_choice" style="display: flex;"
  5579. data-i18n="settings:translation.preference.comment_translation_choice.title">
  5580. </label>
  5581. <select id="comment_translation_choice" name="comment_translation_choice">
  5582. <option value="0" data-i18n="settings:translation.preference.comment_translation_choice.services.follow"></option>
  5583. <option value="deepl" data-i18n="settings:translation.preference.comment_translation_choice.services.deepl"></option>
  5584. <option value="iflyrec" data-i18n="settings:translation.preference.comment_translation_choice.services.iflyrec"></option>
  5585. <option value="youdao" data-i18n="settings:translation.preference.comment_translation_choice.services.youdao"></option>
  5586. <option value="google" data-i18n="settings:translation.preference.comment_translation_choice.services.google"></option>
  5587. <option value="caiyun" data-i18n="settings:translation.preference.comment_translation_choice.services.caiyun"></option>
  5588. <option value="openai" data-i18n="settings:translation.preference.comment_translation_choice.services.openai"></option>
  5589. </select>
  5590. </div>
  5591. <hr>
  5592.  
  5593. <h4 data-i18n="settings:translation.autoTranslation.title"></h4>
  5594. <div class='OJBetter_setting_list'>
  5595. <label for="autoTranslation" data-i18n="settings:translation.autoTranslation.enable"></label>
  5596. <div class="help_tip">
  5597. ${helpCircleHTML}
  5598. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.helpText"></div>
  5599. </div>
  5600. <input type="checkbox" id="autoTranslation" name="autoTranslation">
  5601. </div>
  5602. <div class='OJBetter_setting_list'>
  5603. <label for='shortTextLength'>
  5604. <div style="display: flex;align-items: center;"
  5605. data-i18n="settings:translation.autoTranslation.shortTextLength.name"></div>
  5606. </label>
  5607. <div class="help_tip">
  5608. ${helpCircleHTML}
  5609. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.shortTextLength.helpText">
  5610. </div>
  5611. </div>
  5612. <input type='number' id='shortTextLength' class='no_default' require=true data-i18n="[placeholder]settings:translation.autoTranslation.shortTextLength.placeholder">
  5613. <span data-i18n="settings:translation.autoTranslation.shortTextLength.end"></span>
  5614. </div>
  5615. <div style="display:none;">
  5616. <div class='OJBetter_setting_list'>
  5617. <label for="allowMixTrans" data-i18n="settings:translation.autoTranslation.allowMixTrans.name"></label>
  5618. <div class="help_tip">
  5619. ${helpCircleHTML}
  5620. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.allowMixTrans.helpText">
  5621. </div>
  5622. </div>
  5623. <input type="checkbox" id="allowMixTrans" name="allowMixTrans">
  5624. <div class='OJBetter_checkboxs'>
  5625. <input type="checkbox" id="deepl" name="mixedTranslation" value="deepl">
  5626. <label for="deepl" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.deepl"></label>
  5627. <input type="checkbox" id="iflyrec" name="mixedTranslation" value="iflyrec">
  5628. <label for="iflyrec" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.iflyrec"></label>
  5629. <input type="checkbox" id="youdao" name="mixedTranslation" value="youdao">
  5630. <label for="youdao" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.youdao"></label>
  5631. <input type="checkbox" id="google" name="mixedTranslation" value="google">
  5632. <label for="google" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.google">Google</label>
  5633. <input type="checkbox" id="caiyun" name="mixedTranslation" value="caiyun">
  5634. <label for="caiyun" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.caiyun"></label>
  5635. </div>
  5636. </div>
  5637. </div>
  5638. <hr>
  5639.  
  5640. <h4 data-i18n="settings:translation.advanced.name"></h4>
  5641. <div class='OJBetter_setting_list'>
  5642. <label for="comment_translation_mode" style="display: flex;" data-i18n="settings:translation.advanced.mode.name"></label>
  5643. <div class="help_tip">
  5644. ${helpCircleHTML}
  5645. <div class="tip_text" data-i18n="[html]settings:translation.advanced.mode.helpText"></div>
  5646. </div>
  5647. <select id="comment_translation_mode" name="comment_translation_mode">
  5648. <option value="0" data-i18n="settings:translation.advanced.mode.options.0"></option>
  5649. <option value="1" data-i18n="settings:translation.advanced.mode.options.1"></option>
  5650. <option value="2" data-i18n="settings:translation.advanced.mode.options.2"></option>
  5651. </select>
  5652. </div>
  5653. <div class='OJBetter_setting_list'>
  5654. <label for="memoryTranslateHistory" data-i18n="settings:translation.advanced.memory.name"></label>
  5655. <div class="help_tip">
  5656. ${helpCircleHTML}
  5657. <div class="tip_text" data-i18n="[html]settings:translation.advanced.memory.helpText"></div>
  5658. </div>
  5659. <input type="checkbox" id="memoryTranslateHistory" name="memoryTranslateHistory">
  5660. </div>
  5661. <div class='OJBetter_setting_list'>
  5662. <label for="translation_retransAction" style="display: flex;" data-i18n="settings:translation.advanced.retrans.name"></label>
  5663. <div class="help_tip">
  5664. ${helpCircleHTML}
  5665. <div class="tip_text" data-i18n="[html]settings:translation.advanced.retrans.helpText"></div>
  5666. </div>
  5667. <select id="translation_retransAction" name="translation_retransAction">
  5668. <option value=0 data-i18n="settings:translation.advanced.retrans.options.0"></option>
  5669. <option value=1 data-i18n="settings:translation.advanced.retrans.options.1"></option>
  5670. </select>
  5671. </div>
  5672. <div class='OJBetter_setting_list'>
  5673. <label for='transWaitTime'>
  5674. <div style="display: flex;align-items: center;" data-i18n="settings:translation.advanced.transWaitTime.name"></div>
  5675. </label>
  5676. <div class="help_tip">
  5677. ${helpCircleHTML}
  5678. <div class="tip_text" data-i18n="[html]settings:translation.advanced.transWaitTime.helpText"></div>
  5679. </div>
  5680. <input type='number' id='transWaitTime' class='no_default' require=true data-i18n="[placeholder]settings:translation.advanced.transWaitTime.placeholder">
  5681. <span data-i18n="settings:translation.advanced.transWaitTime.end"></span>
  5682. </div>
  5683. <div class='OJBetter_setting_list'>
  5684. <label for="translation_replaceSymbol" style="display: flex;" data-i18n="settings:translation.advanced.replaceSymbol.name"></label>
  5685. <div class="help_tip">
  5686. ${helpCircleHTML}
  5687. <div class="tip_text" data-i18n="[html]settings:translation.advanced.replaceSymbol.helpText"></div>
  5688. </div>
  5689. <select id="translation_replaceSymbol" name="translation_replaceSymbol">
  5690. <option value=2 data-i18n="settings:translation.advanced.replaceSymbol.options.2"></option>
  5691. <option value=1 data-i18n="settings:translation.advanced.replaceSymbol.options.1"></option>
  5692. <option value=3 data-i18n="settings:translation.advanced.replaceSymbol.options.3"></option>
  5693. </select>
  5694. </div>
  5695. <div class='OJBetter_setting_list'>
  5696. <label for="filterTextWithoutEmphasis" data-i18n="settings:translation.advanced.filterTextWithoutEmphasis.name"></label>
  5697. <div class="help_tip">
  5698. ${helpCircleHTML}
  5699. <div class="tip_text" data-i18n="[html]settings:translation.advanced.filterTextWithoutEmphasis.helpText"></div>
  5700. </div>
  5701. <input type="checkbox" id="filterTextWithoutEmphasis" name="filterTextWithoutEmphasis">
  5702. </div>
  5703. </div>
  5704. `;
  5705.  
  5706. const clist_rating_settings_HTML = `
  5707. <div id="clist_rating-settings" class="settings-page">
  5708. <h3 data-i18n="settings:clist.title"></h3>
  5709. <hr>
  5710. <h4 data-i18n="settings:clist.basics.name"></h4>
  5711. <div class='OJBetter_setting_list alert_tip'>
  5712. <div>
  5713. <p data-i18n="[html]settings:clist.basics.notice"></p>
  5714. </div>
  5715. </div>
  5716. <div class='OJBetter_setting_list'>
  5717. <label for='clist_Authorization'>
  5718. <div style="display: flex;align-items: center;">
  5719. <span class="input_label" data-i18n="settings:clist.basics.key.title"></span>
  5720. </div>
  5721. </label>
  5722. <div class="help_tip">
  5723. ${helpCircleHTML}
  5724. <div class="tip_text" data-i18n="[html]settings:clist.basics.key.helpText"></div>
  5725. </div>
  5726. <input type='text' id='clist_Authorization' class='no_default' required="true"
  5727. data-i18n="[placeholder]settings:clist.basics.key.keyPlaceholder">
  5728. </div>
  5729. <hr>
  5730. <h4 data-i18n="settings:clist.displayRating.title"></h4>
  5731. <div class='OJBetter_setting_list'>
  5732. <label for="showClistRating_contest"><span data-i18n="settings:clist.displayRating.contest.name"></span></label>
  5733. <input type="checkbox" id="showClistRating_contest" name="showClistRating_contest">
  5734. </div>
  5735. <div class='OJBetter_setting_list'>
  5736. <label for="showClistRating_problem"><span data-i18n="settings:clist.displayRating.problem.name"></span></label>
  5737. <input type="checkbox" id="showClistRating_problem" name="showClistRating_problem">
  5738. </div>
  5739. <div class='OJBetter_setting_list' style='display:none;'>
  5740. <label for="showClistRating_problemset"><span data-i18n="settings:clist.displayRating.problemset.name"></span></label>
  5741. <input type="checkbox" id="showClistRating_problemset" name="showClistRating_problemset">
  5742. </div>
  5743. <hr>
  5744. <div class='OJBetter_setting_list'>
  5745. <label for="RatingHidden"><span data-i18n="settings:clist.spoilerProtection.title"></span></label>
  5746. <div class="help_tip">
  5747. ${helpCircleHTML}
  5748. <div class="tip_text" data-i18n="[html]settings:clist.spoilerProtection.helpText"></div>
  5749. </div>
  5750. <input type="checkbox" id="RatingHidden" name="RatingHidden">
  5751. </div>
  5752. </div>
  5753. `;
  5754.  
  5755. const code_editor_settings_HTML = `
  5756. <div id="code_editor-settings" class="settings-page">
  5757. <h3 data-i18n="settings:codeEditor.title"></h3>
  5758. <hr>
  5759. <h4 data-i18n="settings:codeEditor.basics"></h4>
  5760. <div class='OJBetter_setting_list'>
  5761. <label for="problemPageCodeEditor"><span
  5762. data-i18n="settings:codeEditor.problemPageCodeEditor.label"></span></label>
  5763. <div class="help_tip">
  5764. ${helpCircleHTML}
  5765. <div class="tip_text" data-i18n="settings:codeEditor.problemPageCodeEditor.helpText"></div>
  5766. </div>
  5767. <input type="checkbox" id="problemPageCodeEditor" name="problemPageCodeEditor">
  5768. </div>
  5769. <div class='OJBetter_setting_list'>
  5770. <label for="beautifyPreBlocks"><span
  5771. data-i18n="settings:codeEditor.beautifyPreBlocks.label"></span></label>
  5772. <div class="help_tip">
  5773. ${helpCircleHTML}
  5774. <div class="tip_text" data-i18n="settings:codeEditor.beautifyPreBlocks.helpText"></div>
  5775. </div>
  5776. <input type="checkbox" id="beautifyPreBlocks" name="beautifyPreBlocks">
  5777. </div>
  5778. <hr>
  5779. <h4 data-i18n="settings:codeEditor.preferences.title"></h4>
  5780. <div class='OJBetter_setting_list'>
  5781. <label for="isCodeSubmitConfirm"><span
  5782. data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.label"></span></label>
  5783. <div class="help_tip">
  5784. ${helpCircleHTML}
  5785. <div class="tip_text" data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.helpText"></div>
  5786. </div>
  5787. <input type="checkbox" id="isCodeSubmitConfirm" name="isCodeSubmitConfirm">
  5788. </div>
  5789. <div class='OJBetter_setting_list'>
  5790. <label for="autoSubmitAfterPass"><span
  5791. data-i18n="settings:codeEditor.preferences.autoSubmitAfterPass.label"></span></label>
  5792. <div class="help_tip">
  5793. ${helpCircleHTML}
  5794. <div class="tip_text" data-i18n="settings:codeEditor.preferences.autoSubmitAfterPass.helpText"></div>
  5795. </div>
  5796. <input type="checkbox" id="autoSubmitAfterPass" name="autoSubmitAfterPass">
  5797. </div>
  5798. <div class='OJBetter_setting_list'>
  5799. <label for="alwaysConsumeMouseWheel"><span
  5800. data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.label"></span></label>
  5801. <div class="help_tip">
  5802. ${helpCircleHTML}
  5803. <div class="tip_text" data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.helpText"></div>
  5804. </div>
  5805. <input type="checkbox" id="alwaysConsumeMouseWheel" name="alwaysConsumeMouseWheel">
  5806. </div>
  5807. <div class='OJBetter_setting_list'>
  5808. <label for="autoMemoryCode"><span
  5809. data-i18n="settings:codeEditor.preferences.autoMemoryCode.label"></span></label>
  5810. <div class="help_tip">
  5811. ${helpCircleHTML}
  5812. <div class="tip_text" data-i18n="settings:codeEditor.preferences.autoMemoryCode.helpText"></div>
  5813. </div>
  5814. <input type="checkbox" id="autoMemoryCode" name="autoMemoryCode">
  5815. </div>
  5816. <div class='OJBetter_setting_list'>
  5817. <label for="submitButtonPosition"><span
  5818. data-i18n="settings:codeEditor.preferences.submitButtonPosition.label"></span></label>
  5819. <div class="help_tip">
  5820. ${helpCircleHTML}
  5821. <div class="tip_text" data-i18n="settings:codeEditor.preferences.submitButtonPosition.helpText"></div>
  5822. </div>
  5823. <select id="submitButtonPosition" name="submitButtonPosition">
  5824. <option value="bottom" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.bottom"></option>
  5825. <option value="top" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.top"></option>
  5826. </select>
  5827. </div>
  5828. <hr>
  5829. <h4 data-i18n="settings:codeEditor.onlineCodeExecution.title"></h4>
  5830. <label>
  5831. <input type='radio' name='compiler' value='official'>
  5832. <span class='OJBetter_setting_menu_label_text'
  5833. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.codeforces"></span>
  5834. </label>
  5835. <label>
  5836. <input type='radio' name='compiler' value='wandbox'>
  5837. <span class='OJBetter_setting_menu_label_text'
  5838. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.wandbox"></span>
  5839. </label>
  5840. <label>
  5841. <input type='radio' name='compiler' value='rextester'>
  5842. <span class='OJBetter_setting_menu_label_text'
  5843. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.rextester"></span>
  5844. </label>
  5845. <hr>
  5846. <h4 data-i18n="settings:codeEditor.lsp.title"></h4>
  5847. <div class='OJBetter_setting_list'>
  5848. <label for="useLSP"><span data-i18n="settings:codeEditor.lsp.useLSP.label"></span></label>
  5849. <div class="help_tip">
  5850. ${helpCircleHTML}
  5851. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.useLSP.helpText"></div>
  5852. </div>
  5853. <input type="checkbox" id="useLSP" name="useLSP">
  5854. </div>
  5855. <div class='OJBetter_setting_list'>
  5856. <label for='OJBetter_Bridge_WorkUri'>
  5857. <div style="display: flex;align-items: center;">
  5858. <span class="input_label" data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.label"></span>
  5859. </div>
  5860. </label>
  5861. <div class="help_tip">
  5862. ${helpCircleHTML}
  5863. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.helpText"></div>
  5864. </div>
  5865. <input type='text' id='OJBetter_Bridge_WorkUri' class='no_default'
  5866. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.placeholder">
  5867. </div>
  5868. <div class='OJBetter_setting_list'>
  5869. <label for='OJBetter_Bridge_SocketUrl'>
  5870. <div style="display: flex;align-items: center;">
  5871. <span class="input_label"
  5872. data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.label"></span>
  5873. </div>
  5874. </label>
  5875. <div class="help_tip">
  5876. ${helpCircleHTML}
  5877. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.helpText"></div>
  5878. </div>
  5879. <input type='text' id='OJBetter_Bridge_SocketUrl' class='no_default'
  5880. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.placeholder">
  5881. </div>
  5882. <hr>
  5883. <h4 data-i18n="settings:codeEditor.staticCompletionEnhancement.title"></h4>
  5884. <div class='OJBetter_setting_list'>
  5885. <label for="cppCodeTemplateComplete"><span
  5886. data-i18n="settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.label"></span></label>
  5887. <div class="help_tip">
  5888. ${helpCircleHTML}
  5889. <div class="tip_text" data-i18n="[html]settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.helpText"></div>
  5890. </div>
  5891. <input type="checkbox" id="cppCodeTemplateComplete" name="cppCodeTemplateComplete">
  5892. </div>
  5893. <hr>
  5894. <h5 data-i18n="settings:codeEditor.staticCompletionEnhancement.customization"></h5>
  5895. <div class='OJBetter_setting_list alert_warn'>
  5896. <div>
  5897. <p data-i18n="settings:codeEditor.staticCompletionEnhancement.performanceWarning"></p>
  5898. </div>
  5899. </div>
  5900. <div id="Complet_config" class="config"></div>
  5901. </div>
  5902. `;
  5903.  
  5904. const preference_settings_HTML = `
  5905. <div id="preference-settings" class="settings-page">
  5906. <h3 data-i18n="settings:preference.title"></h3>
  5907. <hr>
  5908. <div class='OJBetter_setting_list' style="display:none;">
  5909. <label for="expandFoldingblocks" data-i18n="settings:basic.expandBlocks"></label>
  5910. <input type="checkbox" id="expandFoldingblocks" name="expandFoldingblocks">
  5911. </div>
  5912. <div class='OJBetter_setting_list' style="display:none;">
  5913. <label for="renderPerfOpt" data-i18n="settings:basic.renderOptimization.label"></label>
  5914. <div class="help_tip">
  5915. ${helpCircleHTML}
  5916. <div class="tip_text" data-i18n="[html]settings:basic.renderOptimization.helpText"></div>
  5917. </div>
  5918. <input type="checkbox" id="renderPerfOpt" name="renderPerfOpt">
  5919. </div>
  5920. <div class='OJBetter_setting_list' style="display:none;">
  5921. <label for="selectElementPerfOpt" data-i18n="settings:basic.selectElementOptimization.label"></label>
  5922. <div class="help_tip">
  5923. ${helpCircleHTML}
  5924. <div class="tip_text" data-i18n="[html]settings:basic.selectElementOptimization.helpText"></div>
  5925. </div>
  5926. <input type="checkbox" id="selectElementPerfOpt" name="selectElementPerfOpt">
  5927. </div>
  5928. <div class='OJBetter_setting_list' style="display:none;">
  5929. <label for="commentPaging" data-i18n="settings:basic.paging.label"></label>
  5930. <div class="help_tip">
  5931. ${helpCircleHTML}
  5932. <div class="tip_text" data-i18n="[html]settings:basic.paging.helpText"></div>
  5933. </div>
  5934. <input type="checkbox" id="commentPaging" name="commentPaging">
  5935. </div>
  5936. <div class='OJBetter_setting_list'>
  5937. <label for="showJumpToLuogu" data-i18n="settings:basic.luoguJump.label"></label>
  5938. <div class="help_tip">
  5939. ${helpCircleHTML}
  5940. <div class="tip_text" data-i18n="[html]settings:basic.luoguJump.helpText"></div>
  5941. </div>
  5942. <input type="checkbox" id="showJumpToLuogu" name="showJumpToLuogu">
  5943. </div>
  5944. <div class='OJBetter_setting_list'>
  5945. <label for="showCF2vjudge" data-i18n="settings:basic.vjudgeJump.label"></label>
  5946. <div class="help_tip">
  5947. ${helpCircleHTML}
  5948. <div class="tip_text" data-i18n="[html]settings:basic.vjudgeJump.helpText"></div>
  5949. </div>
  5950. <input type="checkbox" id="showCF2vjudge" name="showCF2vjudge">
  5951. </div>
  5952. <div class='OJBetter_setting_list' style="display:none;">
  5953. <label for="standingsRecolor" data-i18n="settings:basic.recolor.label"></label>
  5954. <div class="help_tip">
  5955. ${helpCircleHTML}
  5956. <div class="tip_text" data-i18n="[html]settings:basic.recolor.helpText"></div>
  5957. </div>
  5958. <input type="checkbox" id="standingsRecolor" name="standingsRecolor">
  5959. </div>
  5960. <div class='OJBetter_setting_list'>
  5961. <label for="showLoading" data-i18n="settings:preference.loadingInfo.label"></label>
  5962. <div class="help_tip">
  5963. ${helpCircleHTML}
  5964. <div class="tip_text" data-i18n="[html]settings:preference.loadingInfo.helpText" data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }'></div>
  5965. </div>
  5966. <input type="checkbox" id="showLoading" name="showLoading">
  5967. </div>
  5968. <div class='OJBetter_setting_list'>
  5969. <label for="hoverTargetAreaDisplay" data-i18n="settings:preference.targetArea.label"></label>
  5970. <div class="help_tip">
  5971. ${helpCircleHTML}
  5972. <div class="tip_text" data-i18n="[html]settings:preference.targetArea.helpText"></div>
  5973. </div>
  5974. <input type="checkbox" id="hoverTargetAreaDisplay" name="hoverTargetAreaDisplay">
  5975. </div>
  5976. <div class='OJBetter_setting_list'>
  5977. <label for='iconButtonSize'>
  5978. <div style="display: flex;align-items: center;" data-i18n="settings:preference.iconButtonSize.title"></div>
  5979. </label>
  5980. <div class="help_tip">
  5981. ${helpCircleHTML}
  5982. <div class="tip_text" data-i18n="[html]settings:preference.iconButtonSize.helpText"></div>
  5983. </div>
  5984. <input type='number' id='iconButtonSize' class='no_default' require=true data-i18n="[placeholder]settings:preference.iconButtonSize.placeholder">
  5985. <span>px</span>
  5986. </div>
  5987. </div>
  5988. `;
  5989.  
  5990. const dev_settings_HTML = `
  5991. <div id="dev-settings" class="settings-page">
  5992. <h3 data-i18n="settings:dev.title"></h3>
  5993. <hr>
  5994. <div class='OJBetter_setting_list alert_danger'>
  5995. <div>
  5996. <p data-i18n="[html]settings:dev.notice"></p>
  5997. </div>
  5998. </div>
  5999. <hr>
  6000. <h5 data-i18n="settings:dev.load.title"></h5>
  6001. <div class='OJBetter_setting_list'>
  6002. <label for="notWaiteLoaded"><span data-i18n="settings:dev.load.notWaiteLoaded.label"></span></label>
  6003. <div class="help_tip">
  6004. ${helpCircleHTML}
  6005. <div class="tip_text" data-i18n="[html]settings:dev.load.notWaiteLoaded.helpText"></div>
  6006. </div>
  6007. <input type="checkbox" id="notWaiteLoaded" name="notWaiteLoaded">
  6008. </div>
  6009. <hr>
  6010. <h5 data-i18n="settings:dev.l10n.title"></h5>
  6011. <div class='OJBetter_setting_list'>
  6012. <label><span data-i18n="settings:dev.l10n.refreshScrpitCache.label"></span></label>
  6013. <div class="help_tip">
  6014. ${helpCircleHTML}
  6015. <div class="tip_text" data-i18n="[html]settings:dev.l10n.refreshScrpitCache.helpText"></div>
  6016. </div>
  6017. <button type="button" id="l10n_refreshScrpitCacheButton" name="l10n_refreshScrpitCacheButton" data-i18n="settings:dev.l10n.refreshScrpitCache.button"></button>
  6018. </div>
  6019. <hr>
  6020. <h5 data-i18n="settings:dev.l10n_web.title"></h5>
  6021. <div class='OJBetter_setting_list'>
  6022. <label><span data-i18n="settings:dev.l10n_web.refreshScrpitCache.label"></span></label>
  6023. <div class="help_tip">
  6024. ${helpCircleHTML}
  6025. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.refreshScrpitCache.helpText"></div>
  6026. </div>
  6027. <button type="button" id="l10n_web_refreshScrpitCacheButton" name="l10n_web_refreshScrpitCacheButton" data-i18n="settings:dev.l10n_web.refreshScrpitCache.button"></button>
  6028. </div>
  6029. <div class='OJBetter_setting_list'>
  6030. <label for="isRuleMarkingEnabled"><span data-i18n="settings:dev.l10n_web.isRuleMarkingEnabled.label"></span></label>
  6031. <div class="help_tip">
  6032. ${helpCircleHTML}
  6033. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.isRuleMarkingEnabled.helpText"></div>
  6034. </div>
  6035. <input type="checkbox" id="isRuleMarkingEnabled" name="isRuleMarkingEnabled">
  6036. </div>
  6037. <hr>
  6038. <h5 data-i18n="settings:dev.indexedDB.title"></h5>
  6039. <div class='OJBetter_setting_list'>
  6040. <label><span data-i18n="settings:dev.indexedDB.clear.label"></span></label>
  6041. <div class="help_tip">
  6042. ${helpCircleHTML}
  6043. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.clear.helpText"></div>
  6044. </div>
  6045. <button type="button" id="indexedDB_clearButton" name="indexedDB_clearButton" data-i18n="settings:dev.indexedDB.clear.button"></button>
  6046. </div>
  6047. <div class='OJBetter_setting_list'>
  6048. <label><span data-i18n="settings:dev.indexedDB.inputOrExport.label"></span></label>
  6049. <div class="help_tip">
  6050. ${helpCircleHTML}
  6051. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.inputOrExport.helpText"></div>
  6052. </div>
  6053. <button type="button" id="indexedDB_exportButton" name="indexedDB_exportButton" data-i18n="settings:dev.indexedDB.inputOrExport.export"></button>
  6054. <button type="button" id="indexedDB_importButton" name="indexedDB_importButton" data-i18n="settings:dev.indexedDB.inputOrExport.import"></button>
  6055. </div>
  6056. <hr>
  6057. <h5 data-i18n="settings:dev.configuration.title"></h5>
  6058. <div class='OJBetter_setting_list'>
  6059. <label><span data-i18n="settings:dev.configuration.clear.label"></span></label>
  6060. <div class="help_tip">
  6061. ${helpCircleHTML}
  6062. <div class="tip_text" data-i18n="[html]settings:dev.configuration.clear.helpText"></div>
  6063. </div>
  6064. <button type="button" id="configuration_clearButton" name="configuration_clearButton" data-i18n="settings:dev.configuration.clear.button"></button>
  6065. </div>
  6066. <div class='OJBetter_setting_list'>
  6067. <label><span data-i18n="settings:dev.configuration.inputOrExport.label"></span></label>
  6068. <div class="help_tip">
  6069. ${helpCircleHTML}
  6070. <div class="tip_text" data-i18n="[html]settings:dev.configuration.inputOrExport.helpText"></div>
  6071. </div>
  6072. <button type="button" id="configuration_exportButton" name="configuration_exportButton" data-i18n="settings:dev.configuration.inputOrExport.export"></button>
  6073. <button type="button" id="configuration_importButton" name="configuration_importButton" data-i18n="settings:dev.configuration.inputOrExport.import"></button>
  6074. </div>
  6075. </div>
  6076. `;
  6077.  
  6078. const about_settings_HTML = `
  6079. <div id="about-settings" class="settings-page">
  6080. <h3 data-i18n="settings:about.title"></h3>
  6081. <hr>
  6082. <div class='versionInfo'>
  6083. <p>${OJBetter.state.name}</p>
  6084. <p><span data-i18n="settings:about.version"></span><span id="nowVersion">${OJBetter.state.version}</span></p>
  6085. <p> @北极小狐 <a target="_blank" href="https://github.com/beijixiaohu/OJBetter">Github</a>
  6086. <a target="_blank" href="https://greatest.deepsurf.us/zh-CN/scripts/465777">GreasyFork</a></p>
  6087. </div>
  6088. <hr>
  6089. <h5 data-i18n="settings:about.update.title"></h5>
  6090. <div id="thanksforDevChannelNotice" class='OJBetter_setting_list alert_info'>
  6091. <div data-i18n="[html]settings:about.update.thanksforDevChannelNotice"} data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }' ></div>
  6092. </div>
  6093. <div class='OJBetter_setting_list'>
  6094. <label for="updateChannel"><span data-i18n="settings:about.update.channel.label"></span></label>
  6095. <div class="help_tip">
  6096. ${helpCircleHTML}
  6097. <div class="tip_text" data-i18n="[html]settings:about.update.channel.helpText"></div>
  6098. </div>
  6099. <select id="updateChannel" name="updateChannel">
  6100. <option value="release" data-i18n="settings:about.update.channel.options.release"></option>
  6101. <option value="dev" data-i18n="settings:about.update.channel.options.dev"></option>
  6102. </select>
  6103. </div>
  6104. <div class='OJBetter_setting_list'>
  6105. <label for="updateSource"><span data-i18n="settings:about.update.source.label"></span></label>
  6106. <div class="help_tip">
  6107. ${helpCircleHTML}
  6108. <div class="tip_text" data-i18n="[html]settings:about.update.source.helpText"></div>
  6109. </div>
  6110. <select id="updateSource" name="updateSource">
  6111. <option value="greasyfork" data-i18n="settings:about.update.source.options.greasyfork"></option>
  6112. <option value="github" data-i18n="settings:about.update.source.options.github"></option>
  6113. <option value="aliyunoss" data-i18n="settings:about.update.source.options.aliyunoss"></option>
  6114. </select>
  6115. </div>
  6116. </div>
  6117. `;
  6118.  
  6119. const OJBetter_setting_content_HTML = `
  6120. <div class="OJBetter_setting_content">
  6121. ${basic_settings_HTML}
  6122. ${translation_settings_HTML}
  6123. ${clist_rating_settings_HTML}
  6124. ${code_editor_settings_HTML}
  6125. ${preference_settings_HTML}
  6126. ${dev_settings_HTML}
  6127. ${about_settings_HTML}
  6128. </div>
  6129. `;
  6130.  
  6131. // 设置界面HTML
  6132. const OJBetterSettingMenu_HTML = `
  6133. <dialog class='OJBetter_setting_menu' id='OJBetter_setting_menu'>
  6134. <div class="tool-box">
  6135. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6136. <i class="iconfont">&#xe614;</i>
  6137. </button>
  6138. </div>
  6139. <div class="OJBetter_setting_container">
  6140. ${OJBetter_setting_sidebar_HTML}
  6141. ${OJBetter_setting_content_HTML}
  6142. </div>
  6143. </dialog>
  6144. `;
  6145.  
  6146. const apiCustomConfigHTML = (prefix) => {
  6147. return `
  6148. <div class="OJBetter_setting_list">
  6149. <label for='${prefix}_header'>
  6150. <div style="display: flex;align-items: center;">
  6151. <span class="input_label" data-i18n="config:common.advanced.header.label"></span>
  6152. <div class="help_tip">
  6153. ${helpCircleHTML}
  6154. <div class="tip_text" data-i18n="[html]config:common.advanced.header.tipText"></div>
  6155. </div>
  6156. </div>
  6157. </label>
  6158. <textarea id="${prefix}_header" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.header.placeholder"></textarea>
  6159. </div>
  6160. <div class="OJBetter_setting_list">
  6161. <label for='${prefix}_data'>
  6162. <div style="display: flex;align-items: center;">
  6163. <span class="input_label" data-i18n="config:common.advanced.data.label"></span>
  6164. <div class="help_tip">
  6165. ${helpCircleHTML}
  6166. <div class="tip_text" data-i18n="[html]config:common.advanced.data.tipText"></div>
  6167. </div>
  6168. </div>
  6169. </label>
  6170. <textarea id="${prefix}_data" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.data.placeholder"></textarea>
  6171. </div>
  6172. `;
  6173. };
  6174.  
  6175. const apiQuotaConfigHTML = (prefix) => {
  6176. return `
  6177. <div class="OJBetter_setting_list">
  6178. <label for='${prefix}_quota_url'>
  6179. <div style="display: flex;align-items: center;">
  6180. <span class="input_label" data-i18n="config:common.quota.url.label"></span>
  6181. <div class="help_tip">
  6182. ${helpCircleHTML}
  6183. <div class="tip_text" data-i18n="[html]config:common.quota.url.tipText"></div>
  6184. </div>
  6185. </div>
  6186. </label>
  6187. <input type='text' id='${prefix}_quota_url' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.url.placeholder">
  6188. </div>
  6189. <div class="OJBetter_setting_list">
  6190. <label for="${prefix}_quota_method" style="display: flex;" data-i18n="config:common.quota.method.label"></label>
  6191. <div class="help_tip">
  6192. ${helpCircleHTML}
  6193. <div class="tip_text" data-i18n="[html]config:common.quota.method.tipText"></div>
  6194. </div>
  6195. <select id="${prefix}_quota_method" name="${prefix}_quota_method">
  6196. <option value="get">GET</option>
  6197. <option value="post">POST</option>
  6198. </select>
  6199. </div>
  6200. <div class="OJBetter_setting_list">
  6201. <label for='${prefix}_quota_header'>
  6202. <div style="display: flex;align-items: center;">
  6203. <span class="input_label" data-i18n="config:common.quota.header.label"></span>
  6204. <div class="help_tip">
  6205. ${helpCircleHTML}
  6206. <div class="tip_text" data-i18n="[html]config:common.quota.header.tipText"></div>
  6207. </div>
  6208. </div>
  6209. </label>
  6210. <textarea id="${prefix}_quota_header" placeholder='' require = false data-i18n="[placeholder]config:common.quota.header.placeholder"></textarea>
  6211. </div>
  6212. <div class="OJBetter_setting_list">
  6213. <label for='${prefix}_quota_data'>
  6214. <div style="display: flex;align-items: center;">
  6215. <span class="input_label" data-i18n="config:common.quota.data.label"></span>
  6216. <div class="help_tip">
  6217. ${helpCircleHTML}
  6218. <div class="tip_text" data-i18n="[html]config:common.quota.data.tipText"></div>
  6219. </div>
  6220. </div>
  6221. </label>
  6222. <textarea id="${prefix}_quota_data" placeholder='' require = false data-i18n="[placeholder]config:common.quota.data.placeholder"></textarea>
  6223. </div>
  6224. <div class="OJBetter_setting_list">
  6225. <div style="display: flex;align-items: center;">
  6226. <span class="input_label" data-i18n="config:common.quota.surplus.label"></span>
  6227. <div class="help_tip">
  6228. ${helpCircleHTML}
  6229. <div class="tip_text" data-i18n="[html]config:common.quota.surplus.tipText"></div>
  6230. </div>
  6231. </div>
  6232. </label>
  6233. <input type='text' id='${prefix}_quota_surplus' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.surplus.placeholder">
  6234. </div>
  6235. `;
  6236. }
  6237.  
  6238. const deeplConfigEditHTML = `
  6239. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6240. <div class='OJBetter_setting_content'>
  6241. <div class="tool-box">
  6242. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6243. <i class="iconfont">&#xe614;</i>
  6244. </button>
  6245. </div>
  6246. <h4 data-i18n="config:deepl.title"></h4>
  6247. <h5 data-i18n="config:deepl.basic.title"></h5>
  6248. <hr>
  6249. <div class="OJBetter_setting_list">
  6250. <label for='name'>
  6251. <span class="input_label" data-i18n="config:deepl.basic.name.label"></span>
  6252. </label>
  6253. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.name.placeholder">
  6254. </div>
  6255. <div class='OJBetter_setting_list'>
  6256. <label for="deepl_apiGenre" style="display: flex;" data-i18n="config:deepl.genre.label"></label>
  6257. <div class="help_tip">
  6258. ${helpCircleHTML}
  6259. <div class="tip_text" data-i18n="[html]config:deepl.genre.tipText"></div>
  6260. </div>
  6261. <select id="deepl_apiGenre" name="deepl_apiGenre">
  6262. <option value="api-free">api-free</option>
  6263. <option value="api-pro">api-pro</option>
  6264. <option value="deeplx">deeplx</option>
  6265. </select>
  6266. </div>
  6267. <div class="OJBetter_setting_list">
  6268. <label for='deepl_key'>
  6269. <div style="display: flex;align-items: center;">
  6270. <span class="input_label" data-i18n="config:deepl.basic.key.label"></span>
  6271. <div class="help_tip">
  6272. ${helpCircleHTML}
  6273. <div class="tip_text" data-i18n="[html]config:deepl.basic.key.tipText"></div>
  6274. </div>
  6275. </div>
  6276. </label>
  6277. <input type='text' id='deepl_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.key.placeholder">
  6278. </div>
  6279. <div class="OJBetter_setting_list">
  6280. <label for='deepl_proxy'>
  6281. <div style="display: flex;align-items: center;">
  6282. <span class="input_label" data-i18n="config:deepl.basic.proxy.label">Proxy API:</span>
  6283. <div class="help_tip">
  6284. ${helpCircleHTML}
  6285. <div class="tip_text" data-i18n="[html]config:deepl.basic.proxy.tipText"></div>
  6286. </div>
  6287. </div>
  6288. </label>
  6289. <input type='text' id='deepl_proxy' placeholder='' require = false>
  6290. </div>
  6291. <hr>
  6292. <details>
  6293. <summary data-i18n="config:common.advanced.title"></summary>
  6294. ${apiCustomConfigHTML('deepl')}
  6295. </details>
  6296. <details>
  6297. <summary data-i18n="config:common.quota.title"></summary>
  6298. ${apiQuotaConfigHTML('deepl')}
  6299. </details>
  6300. <button id='tempConfig_save' data-i18n="common:save"></button>
  6301. </div>
  6302. </dialog>
  6303. `;
  6304.  
  6305. const chatgptConfigEditHTML = `
  6306. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6307. <div class='OJBetter_setting_content'>
  6308. <div class="tool-box">
  6309. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6310. <i class="iconfont">&#xe614;</i>
  6311. </button>
  6312. </div>
  6313. <h4 data-i18n="config:chatgpt.title"></h4>
  6314. <h5 data-i18n="config:chatgpt.basic.title"></h5>
  6315. <hr>
  6316. <div class="OJBetter_setting_list">
  6317. <label for='name'>
  6318. <span class="input_label" data-i18n="config:chatgpt.basic.name.label"></span>
  6319. </label>
  6320. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.name.placeholder">
  6321. </div>
  6322. <div class="OJBetter_setting_list">
  6323. <label for='chatgpt_model'>
  6324. <div style="display: flex;align-items: center;">
  6325. <span class="input_label" data-i18n="[html]config:chatgpt.basic.model.label"></span>
  6326. <div class="help_tip">
  6327. ${helpCircleHTML}
  6328. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.model.tipText"></div>
  6329. </div>
  6330. </div>
  6331. </label>
  6332. <input type='text' id='chatgpt_model' placeholder='gpt-3.5-turbo' require = false>
  6333. </div>
  6334. <div class="OJBetter_setting_list">
  6335. <label for='chatgpt_key'>
  6336. <div style="display: flex;align-items: center;">
  6337. <span class="input_label" data-i18n="config:chatgpt.basic.key.label"></span>
  6338. <div class="help_tip">
  6339. ${helpCircleHTML}
  6340. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.key.tipText"></div>
  6341. </div>
  6342. </div>
  6343. </label>
  6344. <input type='text' id='chatgpt_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.key.placeholder">
  6345. </div>
  6346. <div class="OJBetter_setting_list">
  6347. <label for='chatgpt_proxy'>
  6348. <div style="display: flex;align-items: center;">
  6349. <span class="input_label" data-i18n="config:chatgpt.basic.proxy.label">Proxy API:</span>
  6350. <div class="help_tip">
  6351. ${helpCircleHTML}
  6352. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.proxy.tipText"></div>
  6353. </div>
  6354. </div>
  6355. </label>
  6356. <input type='text' id='chatgpt_proxy' placeholder='https://api.openai.com/v1/chat/completions' require = false>
  6357. </div>
  6358. <hr>
  6359. <details>
  6360. <summary data-i18n="config:common.advanced.title"></summary>
  6361. ${apiCustomConfigHTML('chatgpt')}
  6362. </details>
  6363. <details>
  6364. <summary data-i18n="config:common.quota.title"></summary>
  6365. ${apiQuotaConfigHTML('chatgpt')}
  6366. </details>
  6367. <button id='tempConfig_save' data-i18n="common:save"></button>
  6368. </div>
  6369. </dialog>
  6370. `;
  6371.  
  6372. const CompletConfigEditHTML = `
  6373. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6374. <div class='OJBetter_setting_content'>
  6375. <div class="tool-box">
  6376. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6377. <i class="iconfont">&#xe614;</i>
  6378. </button>
  6379. </div>
  6380. <h4 data-i18n="config:complet.title"></h4>
  6381. <hr>
  6382. <div class="OJBetter_setting_list">
  6383. <label for='name'>
  6384. <span class="input_label" data-i18n="config:complet.name.label"></span>
  6385. </label>
  6386. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.name.placeholder">
  6387. </div>
  6388. <div class='OJBetter_setting_list'>
  6389. <label for="complet_isChoose"><span id="loaded_span" data-i18n="config:complet.choose.label"></span></label>
  6390. <input type="checkbox" id="complet_isChoose" name="complet_isChoose" require = false>
  6391. </div>
  6392. <div class='OJBetter_setting_list'>
  6393. <label for="complet_genre" style="display: flex;" data-i18n="config:complet.genre.label"></label>
  6394. <div class="help_tip">
  6395. ${helpCircleHTML}
  6396. <div class="tip_text" data-i18n="[html]config:complet.genre.tipText"></div>
  6397. </div>
  6398. <select id="complet_genre" name="complet_genre">
  6399. <option value="monaco">monaco</option>
  6400. <option value="ace">ace</option>
  6401. </select>
  6402. </div>
  6403. <div class='OJBetter_setting_list'>
  6404. <label for="complet_language" style="display: flex;" data-i18n="config:complet.language.label"></label>
  6405. <select id="complet_language" name="complet_language">
  6406. <option value="cpp">cpp</option>
  6407. <option value="python">python</option>
  6408. <option value="java">java</option>
  6409. <option value="c">c</option>
  6410. </select>
  6411. </div>
  6412. <div class="OJBetter_setting_list">
  6413. <label for='complet_jsonUrl'>
  6414. <div style="display: flex;align-items: center;">
  6415. <span class="input_label">JSON URL:</span>
  6416. <div class="help_tip">
  6417. ${helpCircleHTML}
  6418. <div class="tip_text" data-i18n="[html]config:complet.jsonurl.tipText"></div>
  6419. </div>
  6420. </div>
  6421. </label>
  6422. <div class='OJBetter_setting_list alert_warn' data-i18n="[html]config:complet.jsonurl.alert"></div>
  6423. <input type='text' id='complet_jsonUrl' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.jsonurl.placeholder">
  6424. </div>
  6425. <button id='tempConfig_save' data-i18n="common:save"></button>
  6426. </div>
  6427. </dialog>
  6428. `;
  6429.  
  6430. /**
  6431. * 加载设置按钮面板
  6432. */
  6433. async function initSettingsPanel() {
  6434. /**
  6435. * 添加右上角设置按钮
  6436. * @param {string} location 位置选择器
  6437. * @param {string} method 插入方法
  6438. */
  6439. function insertOJBetterSettingButton(location, method) {
  6440. $(location)[method](`<button class='ojb_btn OJBetter_setting'>
  6441. ${OJBetter.state.name} ${i18next.t('settings', { ns: 'common' })}</button>`);
  6442. }
  6443.  
  6444. /**
  6445. * ============================================
  6446. * 该网站插入设置按钮的位置和方式
  6447. */
  6448. if (OJBetter.typeOfPage.isEnglishLanguage) {
  6449. insertOJBetterSettingButton("#navbar-collapse > ul:nth-child(2) > li:last-child", "after");
  6450. } else {
  6451. if ($('.header-mypage').length > 0) insertOJBetterSettingButton(".header-mypage", "after");
  6452. else insertOJBetterSettingButton("#navbar-collapse > ul:nth-child(2) > li:last-child", "after");
  6453. }
  6454. /**
  6455. * ============================================
  6456. */
  6457.  
  6458. const $settingBtns = $(".OJBetter_setting");
  6459. $settingBtns.click(() => {
  6460. $settingBtns.prop("disabled", true).addClass("open");
  6461.  
  6462. // 设置面板div
  6463. const settingMenu = OJB_safeCreateJQElement(OJBetterSettingMenu_HTML);
  6464. $("body").append(settingMenu);
  6465.  
  6466. elementLocalize(settingMenu); // 加载i18n
  6467. OJB_showModal(settingMenu);
  6468. OJB_addDraggable($('#OJBetter_setting_menu')); // 窗口支持拖拽
  6469.  
  6470. // help帮助悬浮窗位置更新
  6471. $(document).on('mouseenter', '.help-icon', function (event) {
  6472. var menuOffset = $('.OJBetter_setting_menu:last').offset();
  6473. var mouseX = event.pageX - menuOffset.left;
  6474. var mouseY = event.pageY - menuOffset.top;
  6475.  
  6476. $('.tip_text').css({
  6477. 'top': mouseY + 'px',
  6478. 'left': mouseX + 'px'
  6479. });
  6480. });
  6481.  
  6482. // 选项卡切换
  6483. $('.OJBetter_setting_sidebar a').click(function (event) {
  6484. event.preventDefault();
  6485. $('.OJBetter_setting_sidebar a').removeClass('active');
  6486. $('.settings-page').removeClass('active');
  6487. $(this).addClass('active');
  6488. const targetPageId = $(this).attr('href').substring(1);
  6489. $('#' + targetPageId).addClass('active');
  6490. });
  6491.  
  6492. /**
  6493. * 更新单选按钮组的可用状态
  6494. * @param {string} selector 单选按钮组的选择器
  6495. * @param {string} targetLanguage 目标语言
  6496. * @param {Object} translationSupport 翻译支持的语言对应表
  6497. */
  6498. const updateRadioButtonsAvailability = (selector, targetLanguage) => {
  6499. Object.entries(OJBetter.supportList.translationSupport).forEach(([service, languages]) => {
  6500. const radioButton = $(selector).find(`input[value="${service}"]`);
  6501. const isEnabled = languages[targetLanguage];
  6502. $(radioButton).prop('disabled', !isEnabled);
  6503. if (!isEnabled) {
  6504. $(radioButton).prop('checked', false);
  6505. }
  6506. });
  6507. };
  6508.  
  6509. /**
  6510. * 检查下拉框选中项是否有效,若无效则清空
  6511. * @param {string} selector 下拉框的选择器
  6512. */
  6513. const validateSelectOption = (selector) => {
  6514. const selectedValue = $(selector).val();
  6515. if (!selectedValue) {
  6516. $(selector).val('');
  6517. }
  6518. };
  6519.  
  6520. /**
  6521. * 更新翻译目标语言下拉框的可用状态
  6522. * @param {string} selector 下拉框的选择器
  6523. * @param {string} targetLanguage 目标语言
  6524. * @param {Object} translationSupport 翻译支持的语言对应表
  6525. */
  6526. const updateSelectOptionsAvailability = (selector, targetLanguage) => {
  6527. $(selector).children('option').each(function () {
  6528. const optionValue = $(this).val();
  6529. const isEnabled = OJBetter.supportList.translationSupport[optionValue] ? OJBetter.supportList.translationSupport[optionValue][targetLanguage] : true;
  6530. $(this).prop('disabled', !isEnabled);
  6531. });
  6532. validateSelectOption(selector);
  6533. };
  6534.  
  6535. /**
  6536. * 更新翻译服务复选框的可用状态
  6537. * @param {string} selector 复选框的选择器
  6538. * @param {string} targetLanguage 目标语言
  6539. * @param {Object} translationSupport 翻译支持的语言对应表
  6540. */
  6541. const updateCheckboxesAvailability = (selector, targetLanguage) => {
  6542. $(selector).children('input').each(function () {
  6543. const checkboxValue = $(this).val();
  6544. const isEnabled = OJBetter.supportList.translationSupport[checkboxValue][targetLanguage];
  6545. $(this).prop('disabled', !isEnabled);
  6546. if (!isEnabled) {
  6547. $(this).prop('checked', false);
  6548. }
  6549. });
  6550. };
  6551.  
  6552. /**
  6553. * 更新更新源下拉框的可用状态
  6554. * @param {string} selector 下拉框的选择器
  6555. * @param {string} targetLanguage 目标语言
  6556. * @param {Object} translationSupport 翻译支持的语言对应表
  6557. */
  6558. const updateUpdateSourceSelectOptionsAvailability = (selector, updateChannel) => {
  6559. $(selector).children('option').each(function () {
  6560. const optionValue = $(this).val();
  6561. const isEnabled = OJBetter.supportList.updateSourceSupportList[optionValue][updateChannel];
  6562. $(this).prop('disabled', !isEnabled);
  6563. });
  6564. validateSelectOption(selector);
  6565. };
  6566.  
  6567. /**
  6568. * 创建配置结构
  6569. * @param {string} type - 该字段的在表单中的类型
  6570. * @param {string} value - 在配置中的键值
  6571. * @param {boolean} require - 是否是表单的必填项
  6572. * @param {string} [check=""] check - 调用的合法性检查
  6573. */
  6574. function createStructure(type, value, require, check = "") {
  6575. return { type, value, require, check };
  6576. }
  6577.  
  6578. // deepl配置
  6579. const deeplStructure = {
  6580. '#name': createStructure('text', 'name', true),
  6581. '#deepl_apiGenre': createStructure('text', 'apiGenre', true),
  6582. '#deepl_key': createStructure('text', 'key', false),
  6583. '#deepl_proxy': createStructure('text', 'proxy', false),
  6584. '#deepl_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6585. '#deepl_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6586. '#deepl_quota_url': createStructure('text', 'quota_url', false),
  6587. '#deepl_quota_method': createStructure('text', 'quota_method', false),
  6588. '#deepl_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6589. '#deepl_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6590. '#deepl_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6591. };
  6592. let tempConfig_deepl = GM_getValue('deepl_config'); // 获取配置信息
  6593. const configManager_deepl = new ConfigManager('#deepl_config', 'deepl_config_', tempConfig_deepl, deeplStructure, deeplConfigEditHTML);
  6594. configManager_deepl.registerChoiceChange();
  6595.  
  6596. // chatgpt配置
  6597. const chatgptStructure = {
  6598. '#name': createStructure('text', 'name', true),
  6599. '#chatgpt_model': createStructure('text', 'model', false),
  6600. '#chatgpt_key': createStructure('text', 'key', true),
  6601. '#chatgpt_proxy': createStructure('text', 'proxy', false),
  6602. '#chatgpt_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6603. '#chatgpt_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6604. '#chatgpt_quota_url': createStructure('text', 'quota_url', false),
  6605. '#chatgpt_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6606. '#chatgpt_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6607. '#chatgpt_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6608. '#chatgpt_quota_method': createStructure('text', 'quota_method', false),
  6609. };
  6610. let tempConfig_chatgpt = GM_getValue('chatgpt_config'); // 获取配置信息
  6611. const configManager_chatgpt = new ConfigManager('#chatgpt_config', 'chatgpt_config_', tempConfig_chatgpt, chatgptStructure, chatgptConfigEditHTML);
  6612. configManager_chatgpt.registerChoiceChange();
  6613.  
  6614. // Complet配置
  6615. const CompletStructure = {
  6616. '#name': createStructure('text', 'name', true),
  6617. '#complet_isChoose': createStructure('checkbox', 'isChoose', true),
  6618. '#complet_genre': createStructure('text', 'genre', true),
  6619. '#complet_language': createStructure('text', 'language', true),
  6620. '#complet_jsonUrl': createStructure('text', 'jsonUrl', true)
  6621. };
  6622. let tempConfig_Complet = GM_getValue('Complet_config'); // 获取配置信息
  6623. const configManager_complet = new ConfigManager('#Complet_config', 'complet_config_', tempConfig_Complet, CompletStructure, CompletConfigEditHTML, false);
  6624.  
  6625. // 状态更新
  6626. $("input[name='darkMode'][value='" + OJBetter.basic.darkMode + "']").prop("checked", true);
  6627. $("#showLoading").prop("checked", GM_getValue("showLoading") === true);
  6628. $("#expandFoldingblocks").prop("checked", GM_getValue("expandFoldingblocks") === true);
  6629. $("#renderPerfOpt").prop("checked", GM_getValue("renderPerfOpt") === true);
  6630. $("#selectElementPerfOpt").prop("checked", GM_getValue("selectElementPerfOpt") === true);
  6631. $("#commentPaging").prop("checked", GM_getValue("commentPaging") === true);
  6632. $("#standingsRecolor").prop("checked", GM_getValue("standingsRecolor") === true);
  6633. $("#showJumpToLuogu").prop("checked", GM_getValue("showJumpToLuogu") === true);
  6634. $("#showCF2vjudge").prop("checked", GM_getValue("showCF2vjudge") === true);
  6635. $("#hoverTargetAreaDisplay").prop("checked", GM_getValue("hoverTargetAreaDisplay") === true);
  6636. $("#showClistRating_contest").prop("checked", GM_getValue("showClistRating_contest") === true);
  6637. $("#showClistRating_problemset").prop("checked", GM_getValue("showClistRating_problemset") === true);
  6638. $("#showClistRating_problem").prop("checked", GM_getValue("showClistRating_problem") === true);
  6639. $("#RatingHidden").prop("checked", GM_getValue("RatingHidden") === true);
  6640. $('#scriptL10nLanguage').val(GM_getValue("scriptL10nLanguage"));
  6641. $('#localizationLanguage').val(GM_getValue("localizationLanguage"));
  6642. $("input[name='translation'][value='" + OJBetter.translation.choice + "']").prop("checked", true);
  6643. $("input[name='translation']").css("color", "gray");
  6644. $('#deepl_type').val(GM_getValue("deepl_type"));
  6645. $("#deepl_config_config_bar_ul").find(`input[name='deepl_config_config_item'][value='${tempConfig_deepl.choice}']`).prop("checked", true);
  6646. $('#enableEmphasisProtection').prop("checked", GM_getValue("enableEmphasisProtection") === true);
  6647. $('#enableLinkProtection').prop("checked", GM_getValue("enableLinkProtection") === true);
  6648. $("#chatgpt_config_config_bar_ul").find(`input[name='chatgpt_config_config_item'][value='${tempConfig_chatgpt.choice}']`).prop("checked", true);
  6649. $("#openai_isStream").prop("checked", GM_getValue("openai_isStream") === true);
  6650. $("#openai_asSystemPrompt").prop("checked", GM_getValue("openai_asSystemPrompt") === true);
  6651. $('#openai_customPrompt').val(GM_getValue("openai_customPrompt"));
  6652. $('#comment_translation_choice').val(GM_getValue("commentTranslationChoice"));
  6653. $('#iconButtonSize').val(GM_getValue("iconButtonSize"));
  6654. $("#autoTranslation").prop("checked", GM_getValue("autoTranslation") === true);
  6655. $('#shortTextLength').val(GM_getValue("shortTextLength"));
  6656. $("#allowMixTrans").prop("checked", GM_getValue("allowMixTrans") === true);
  6657. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6658. if (OJBetter.translation.auto.mixTrans.servers.indexOf($(this).val()) > -1) {
  6659. $(this).prop('checked', true);
  6660. }
  6661. });
  6662. // 翻译目标语言下拉框
  6663. $('#transTargetLang').change(function () {
  6664. var selectedLang = $(this).val();
  6665. updateRadioButtonsAvailability('#translationServices', selectedLang);
  6666. updateSelectOptionsAvailability('#comment_translation_choice', selectedLang);
  6667. updateCheckboxesAvailability('.OJBetter_checkboxs', selectedLang);
  6668. });
  6669. $('#transTargetLang').val(GM_getValue("transTargetLang"));
  6670. $('#transTargetLang').change();
  6671. //
  6672. $('#comment_translation_mode').val(GM_getValue("commentTranslationMode"));
  6673. $("#memoryTranslateHistory").prop("checked", GM_getValue("memoryTranslateHistory") === true);
  6674. $('#transWaitTime').val(GM_getValue("transWaitTime"));
  6675. $('#translation_replaceSymbol').val(GM_getValue("replaceSymbol"));
  6676. $("#filterTextWithoutEmphasis").prop("checked", GM_getValue("filterTextWithoutEmphasis") === true);
  6677. $('#translation_retransAction').val(GM_getValue("retransAction"));
  6678. $("#clist_Authorization").val(GM_getValue("clist_Authorization"));
  6679. $("#problemPageCodeEditor").prop("checked", GM_getValue("problemPageCodeEditor") === true);
  6680. $("#beautifyPreBlocks").prop("checked", GM_getValue("beautifyPreBlocks") === true);
  6681. $("#isCodeSubmitConfirm").prop("checked", GM_getValue("isCodeSubmitConfirm") === true);
  6682. $("#autoSubmitAfterPass").prop("checked", GM_getValue("autoSubmitAfterPass") === true);
  6683. $("#alwaysConsumeMouseWheel").prop("checked", GM_getValue("alwaysConsumeMouseWheel") === true);
  6684. $("#autoMemoryCode").prop("checked", GM_getValue("autoMemoryCode") === true);
  6685. $("#submitButtonPosition").val(GM_getValue("submitButtonPosition"));
  6686. $("#cppCodeTemplateComplete").prop("checked", GM_getValue("cppCodeTemplateComplete") === true);
  6687. $("#useLSP").prop("checked", GM_getValue("useLSP") === true);
  6688. $("#OJBetter_Bridge_WorkUri").val(GM_getValue("OJBetter_Bridge_WorkUri"));
  6689. $("#OJBetter_Bridge_SocketUrl").val(GM_getValue("OJBetter_Bridge_SocketUrl"));
  6690. $("input[name='compiler'][value='" + OJBetter.monaco.onlineCompilerChoice + "']").prop("checked", true);
  6691. $("input[name='compiler']").css("color", "gray");
  6692. // 调试
  6693. $("#notWaiteLoaded").prop("checked", GM_getValue("notWaiteLoaded") === true);
  6694. $('#l10n_refreshScrpitCacheButton').click(clearI18nextCache);
  6695. $('#l10n_web_refreshScrpitCacheButton').click(clearWebsiteL10nData);
  6696. $("#isRuleMarkingEnabled").prop("checked", GM_getValue("isRuleMarkingEnabled") === true);
  6697. $('#indexedDB_clearButton').click(async () => { await clearDatabase(); });
  6698. $('#indexedDB_exportButton').click(async () => { downloadDataAsFile(await exportDatabase(), 'database_export.json') });
  6699. $('#indexedDB_importButton').click(() => { readFileInput(async (fileContent) => { await importDatabase(fileContent); }); });
  6700. $('#configuration_clearButton').click(deleteAllConfigSettings);
  6701. $('#configuration_exportButton').click(() => { downloadDataAsFile(exportSettingsToJSON(), 'configuration_export.json') });
  6702. $('#configuration_importButton').click(() => { readFileInput((fileContent) => { importSettingsFromJSON(fileContent); }) });
  6703. // 关于
  6704. $('#updateChannel').val(GM_getValue("updateChannel"));
  6705. $('#updateSource').val(GM_getValue("updateSource"));
  6706. $('#updateChannel').change(function () {
  6707. var selectedLang = $(this).val();
  6708. updateUpdateSourceSelectOptionsAvailability('#updateSource', selectedLang);
  6709. if (selectedLang == "dev") $('#thanksforDevChannelNotice').show();
  6710. else $('#thanksforDevChannelNotice').hide();
  6711. });
  6712. $('#updateChannel').change();
  6713.  
  6714. // 关闭
  6715. const $settingMenu = $(".OJBetter_setting_menu");
  6716. $settingMenu.on("click", ".btn-close", async () => {
  6717. // 设置的数据
  6718. const settings = {
  6719. darkMode: $("input[name='darkMode']:checked").val(),
  6720. showLoading: $("#showLoading").prop("checked"),
  6721. hoverTargetAreaDisplay: $("#hoverTargetAreaDisplay").prop("checked"),
  6722. expandFoldingblocks: $("#expandFoldingblocks").prop("checked"),
  6723. renderPerfOpt: $("#renderPerfOpt").prop("checked"),
  6724. selectElementPerfOpt: $("#selectElementPerfOpt").prop("checked"),
  6725. commentPaging: $("#commentPaging").prop("checked"),
  6726. standingsRecolor: $("#standingsRecolor").prop("checked"),
  6727. showJumpToLuogu: $("#showJumpToLuogu").prop("checked"),
  6728. showCF2vjudge: $("#showCF2vjudge").prop("checked"),
  6729. scriptL10nLanguage: $('#scriptL10nLanguage').val(),
  6730. localizationLanguage: $('#localizationLanguage').val(),
  6731. transTargetLang: $('#transTargetLang').val(),
  6732. translation: $("input[name='translation']:checked").val(),
  6733. deepl_type: $('#deepl_type').val(),
  6734. enableEmphasisProtection: $("#enableEmphasisProtection").prop("checked"),
  6735. enableLinkProtection: $("#enableLinkProtection").prop("checked"),
  6736. openai_isStream: $("#openai_isStream").prop("checked"),
  6737. openai_asSystemPrompt: $("#openai_asSystemPrompt").prop("checked"),
  6738. openai_customPrompt: $('#openai_customPrompt').val(),
  6739. commentTranslationChoice: $('#comment_translation_choice').val(),
  6740. iconButtonSize: $('#iconButtonSize').val(),
  6741. autoTranslation: $("#autoTranslation").prop("checked"),
  6742. shortTextLength: $('#shortTextLength').val(),
  6743. allowMixTrans: $("#allowMixTrans").prop("checked"),
  6744. mixedTranslation: (() => {
  6745. let mixedTranslation = [];
  6746. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6747. if ($(this).is(":checked")) {
  6748. mixedTranslation.push($(this).val());
  6749. }
  6750. });
  6751. return mixedTranslation;
  6752. })(),
  6753. commentTranslationMode: $('#comment_translation_mode').val(),
  6754. memoryTranslateHistory: $('#memoryTranslateHistory').prop("checked"),
  6755. transWaitTime: $('#transWaitTime').val(),
  6756. replaceSymbol: $('#translation_replaceSymbol').val(),
  6757. filterTextWithoutEmphasis: $('#filterTextWithoutEmphasis').prop("checked"),
  6758. retransAction: $('#translation_retransAction').val(),
  6759. showClistRating_contest: $('#showClistRating_contest').prop("checked"),
  6760. showClistRating_problemset: $('#showClistRating_problemset').prop("checked"),
  6761. showClistRating_problem: $('#showClistRating_problem').prop("checked"),
  6762. RatingHidden: $('#RatingHidden').prop("checked"),
  6763. clist_Authorization: $('#clist_Authorization').val(),
  6764. problemPageCodeEditor: $("#problemPageCodeEditor").prop("checked"),
  6765. beautifyPreBlocks: $("#beautifyPreBlocks").prop("checked"),
  6766. isCodeSubmitConfirm: $("#isCodeSubmitConfirm").prop("checked"),
  6767. autoSubmitAfterPass: $("#autoSubmitAfterPass").prop("checked"),
  6768. alwaysConsumeMouseWheel: $("#alwaysConsumeMouseWheel").prop("checked"),
  6769. autoMemoryCode: $("#autoMemoryCode").prop("checked"),
  6770. submitButtonPosition: $('#submitButtonPosition').val(),
  6771. cppCodeTemplateComplete: $("#cppCodeTemplateComplete").prop("checked"),
  6772. useLSP: $("#useLSP").prop("checked"),
  6773. OJBetter_Bridge_WorkUri: $('#OJBetter_Bridge_WorkUri').val().replace(/\\/g, '/').replace(/\/$/, ''),
  6774. OJBetter_Bridge_SocketUrl: $('#OJBetter_Bridge_SocketUrl').val(),
  6775. onlineCompilerChoice: $("input[name='compiler']:checked").val(),
  6776. notWaiteLoaded: $("#notWaiteLoaded").prop("checked"),
  6777. isRuleMarkingEnabled: $("#isRuleMarkingEnabled").prop("checked"),
  6778. updateChannel: $('#updateChannel').val(),
  6779. updateSource: $('#updateSource').val()
  6780. };
  6781. // tempConfigs的数据
  6782. const tempConfigs = {
  6783. 'deepl_config': configManager_deepl.getTempConfig(),
  6784. 'chatgpt_config': configManager_chatgpt.getTempConfig(),
  6785. 'Complet_config': configManager_complet.getTempConfig()
  6786. }
  6787.  
  6788. // 判断是否改变
  6789. let changes = {};
  6790. const combinedConfigs = Object.assign({}, settings, tempConfigs); // 合并settings和tempConfigs对象
  6791. for (const [key, value] of Object.entries(combinedConfigs)) {
  6792. const storedValue = GM_getValue(key);
  6793. if (!OJB_deepEquals(value, storedValue)) {
  6794. changes[key] = { oldValue: storedValue, newValue: value };
  6795. }
  6796. }
  6797.  
  6798. // 如果changes对象不为空,则有变化
  6799. if (Object.keys(changes).length > 0) {
  6800. console.log("Changes detected:", changes);
  6801. const shouldSave = await OJB_createDialog(
  6802. i18next.t('saveSetting.title', { ns: 'dialog' }),
  6803. i18next.t('saveSetting.content', { ns: 'dialog' }),
  6804. [
  6805. i18next.t('saveSetting.buttons.0', { ns: 'dialog' }),
  6806. i18next.t('saveSetting.buttons.1', { ns: 'dialog' })
  6807. ]
  6808. ); // 配置改变保存确认
  6809. if (shouldSave) {
  6810. // 数据校验
  6811. // TODO
  6812. if (settings.deepl_type !== 'free') {
  6813. let selectedIndex = $('input[name="deepl_config_config_item"]:checked').length > 0;
  6814. if (!selectedIndex) {
  6815. $('.deepl_config a').removeClass('active');
  6816. $('.settings-page').removeClass('active');
  6817. $('#sidebar-translation-settings').addClass('active');
  6818. $('#translation-settings').addClass('active');
  6819.  
  6820. $('#deepl_config').addClass('missing');
  6821. return;
  6822. } else {
  6823. $('#deepl_config').removeClass('missing');
  6824. }
  6825. }
  6826. if (settings.translation === "openai") {
  6827. let selectedIndex = $('input[name="chatgpt_config_config_item"]:checked').length > 0;
  6828. if (!selectedIndex) {
  6829. $('.chatgpt_config a').removeClass('active');
  6830. $('.settings-page').removeClass('active');
  6831. $('#sidebar-translation-settings').addClass('active');
  6832. $('#translation-settings').addClass('active');
  6833.  
  6834. $('#chatgpt_config').addClass('missing');
  6835. return;
  6836. } else {
  6837. $('#chatgpt_config').removeClass('missing');
  6838. }
  6839. }
  6840. {
  6841. let selectedIndex = $('input[name="translation"]:checked').length > 0;
  6842. if (!selectedIndex) {
  6843. $('.OJBetter_setting_sidebar a').removeClass('active');
  6844. $('.settings-page').removeClass('active');
  6845. $('#sidebar-translation-settings').addClass('active');
  6846. $('#translation-settings').addClass('active');
  6847.  
  6848. $('#translationServices').addClass('missing');
  6849. return;
  6850. } else {
  6851. $('#translationServices').removeClass('missing');
  6852. }
  6853. }
  6854.  
  6855. // 保存数据
  6856. let refreshPage = false; // 是否需要刷新页面
  6857. for (const [key, value] of Object.entries(settings)) {
  6858. if (!refreshPage && !(key == 'translation' || key == 'darkMode' || key == 'commentTranslationChoice')) {
  6859. if (GM_getValue(key) != value) refreshPage = true;
  6860. }
  6861. GM_setValue(key, value);
  6862. }
  6863. for (const [key, value] of Object.entries(tempConfigs)) {
  6864. if (!refreshPage && (JSON.stringify(GM_getValue(key)) != JSON.stringify(value))) refreshPage = true;
  6865. GM_setValue(key, value);
  6866. }
  6867.  
  6868. if (refreshPage) location.reload();
  6869. else {
  6870. // 切换黑暗模式
  6871. if (OJBetter.basic.darkMode != settings.darkMode) {
  6872. OJBetter.basic.darkMode = settings.darkMode;
  6873. // 移除旧的事件监听器
  6874. changeEventListeners.forEach(listener => {
  6875. mediaQueryList.removeEventListener('change', listener);
  6876. });
  6877.  
  6878. if (OJBetter.basic.darkMode == "follow") {
  6879. changeEventListeners.push(handleColorSchemeChange);
  6880. mediaQueryList.addEventListener('change', handleColorSchemeChange);
  6881. $('html').removeAttr('data-theme');
  6882. } else if (OJBetter.basic.darkMode == "dark") {
  6883. $('html').attr('data-theme', 'dark');
  6884. if (OJBetter.monaco.editor) {
  6885. monaco.editor.setTheme('vs-dark');
  6886. }
  6887. } else {
  6888. $('html').attr('data-theme', 'light');
  6889. if (OJBetter.monaco.editor) {
  6890. monaco.editor.setTheme('vs');
  6891. }
  6892. // 移除旧的事件监听器
  6893. const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  6894. window.matchMedia('(prefers-color-scheme: dark)');
  6895. }
  6896. }
  6897. // 更新配置信息
  6898. OJBetter.translation.choice = settings.translation;
  6899. OJBetter.translation.comment.choice = settings.commentTranslationChoice;
  6900. }
  6901. }
  6902. }
  6903. OJB_closeAndRemoveModal(settingMenu);
  6904. $settingBtns.prop("disabled", false).removeClass("open");
  6905. });
  6906. });
  6907. };
  6908.  
  6909. /**
  6910. * 初始化html2markdown转换器
  6911. */
  6912. async function initHTML2MarkDown() {
  6913. OJBetter.common.turndownService = new TurndownService({ bulletListMarker: '-' });
  6914.  
  6915. // 保留原始
  6916. OJBetter.common.turndownService.keep(['del']);
  6917.  
  6918. OJBetter.common.turndownService.addRule('removeByClass', {
  6919. filter: function (node) {
  6920. return node.classList.contains('html2md-panel') ||
  6921. node.classList.contains('div-btn-copy') ||
  6922. node.classList.contains('btn-copy') ||
  6923. node.classList.contains('overlay') ||
  6924. node.classList.contains('monaco-editor') ||
  6925. node.nodeName === 'SCRIPT';
  6926. },
  6927. replacement: function () {
  6928. return '';
  6929. }
  6930. });
  6931.  
  6932. // inline math
  6933. OJBetter.common.turndownService.addRule('inline-math', {
  6934. filter: function (node, options) {
  6935. return node.tagName.toLowerCase() == "span" && node.className == "katex";
  6936. },
  6937. replacement: function (content, node) {
  6938. var latex = $(node).find('annotation').text();
  6939. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  6940. return "$" + latex + "$";
  6941. }
  6942. });
  6943.  
  6944. // block math
  6945. OJBetter.common.turndownService.addRule('block-math', {
  6946. filter: function (node, options) {
  6947. return node.tagName.toLowerCase() == "span" && node.className == "katex-display";
  6948. },
  6949. replacement: function (content, node) {
  6950. var latex = $(node).find('annotation').text();
  6951. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  6952. return "\n$$\n" + latex + "\n$$\n";
  6953. }
  6954. });
  6955.  
  6956. // pre
  6957. OJBetter.common.turndownService.addRule('pre', {
  6958. filter: function (node, options) {
  6959. return node.tagName.toLowerCase() == "pre";
  6960. },
  6961. replacement: function (content, node) {
  6962. if (!node.classList.contains('source-code-for-copy') && !node.classList.contains('prettyprint')) {
  6963. return "```\n" + content + "```\n";
  6964. } else {
  6965. return "";
  6966. }
  6967. }
  6968. });
  6969.  
  6970. // bordertable
  6971. OJBetter.common.turndownService.addRule('bordertable', {
  6972. filter: 'table',
  6973. replacement: function (content, node) {
  6974. if (node.classList.contains('table')) {
  6975. var output = [],
  6976. thead = '',
  6977. trs = node.querySelectorAll('tr');
  6978. if (trs.length > 0) {
  6979. var ths = trs[0].querySelectorAll('th, td');
  6980. if (ths.length > 0) {
  6981. thead = '| ' + Array.from(ths).map(th => OJBetter.common.turndownService.turndown(th.innerHTML.trim())).join(' | ') + ' |\n'
  6982. thead += '| ' + Array.from(ths).map(() => ' --- ').join('|') + ' |\n';
  6983. }
  6984. }
  6985. var rows = node.querySelectorAll('tr');
  6986. Array.from(rows).forEach(function (row, i) {
  6987. if (i > 0) {
  6988. var cells = row.querySelectorAll('td,th');
  6989. var trow = '| ' + Array.from(cells).map(cell => OJBetter.common.turndownService.turndown(cell.innerHTML.trim())).join(' | ') + ' |';
  6990. output.push(trow);
  6991. }
  6992. });
  6993. return thead + output.join('\n');
  6994. } else {
  6995. return content;
  6996. }
  6997. }
  6998. });
  6999. };
  7000.  
  7001. /**
  7002. * 任务队列
  7003. */
  7004. class TaskQueue {
  7005. constructor() {
  7006. this.taskQueues = {};
  7007. this.isProcessing = {}; // 处理状态
  7008. this.delays = {}; // 等待时间(毫秒)
  7009. }
  7010.  
  7011. getDelay(type) {
  7012. if (type === 'openai') {
  7013. return 0;
  7014. } else {
  7015. return OJBetter.translation.waitTime;
  7016. }
  7017. }
  7018.  
  7019. /**
  7020. * 添加任务
  7021. * @param {string} type 任务类型
  7022. * @param {function} fn 任务函数
  7023. * @param {boolean} isNonQueueTask 是否为非队列任务
  7024. */
  7025. addTask(type, fn, isNonQueueTask = false) {
  7026. if (!this.taskQueues[type]) {
  7027. this.taskQueues[type] = [];
  7028. }
  7029.  
  7030. if (isNonQueueTask) {
  7031. fn();
  7032. } else {
  7033. this.taskQueues[type].push(fn);
  7034.  
  7035. if (!this.isProcessing[type]) {
  7036. this.processQueue(type);
  7037. }
  7038. }
  7039. }
  7040.  
  7041. async processQueue(type) {
  7042. this.isProcessing[type] = true;
  7043.  
  7044. while (this.taskQueues[type].length > 0) {
  7045. const task = this.taskQueues[type].shift();
  7046. await task();
  7047.  
  7048. if (this.taskQueues[type].length > 0) {
  7049. await this.wait(this.getDelay(type));
  7050. }
  7051. }
  7052.  
  7053. this.isProcessing[type] = false;
  7054. }
  7055.  
  7056. wait(delay) {
  7057. return new Promise(resolve => {
  7058. setTimeout(resolve, delay);
  7059. });
  7060. }
  7061. }
  7062.  
  7063. /**
  7064. * 检测文本是否可能为代码片段
  7065. * @param {string} text 待检测的文本
  7066. * @returns {boolean} 是否可能为代码片段
  7067. */
  7068. /**
  7069. * 检测为空文本
  7070. * @param {string} text 待检测的文本
  7071. * @returns {boolean} 是否为空文本
  7072. */
  7073. const isEmptyText = text => text.trim() === '';
  7074.  
  7075. /**
  7076. * 加载按钮相关函数
  7077. */
  7078. async function initButtonFunc() {
  7079. // 鼠标悬浮时为目标元素区域添加一个覆盖层
  7080. $.fn.addHoverOverlay = function (target) {
  7081. let position = $(target).css('position');
  7082. let display = $(target).css('display');
  7083.  
  7084. this.hover(() => {
  7085. $(target)
  7086. .addClass('overlay')
  7087. .css('position', 'relative');
  7088. if (display == "inline" || display == "contents") {
  7089. $(target).css('display', 'block');
  7090. }
  7091. }, () => {
  7092. $(target)
  7093. .removeClass('overlay')
  7094. .css('position', position);
  7095. if (display == "inline" || display == "contents") {
  7096. $(target).css('display', display);
  7097. }
  7098. })
  7099. }
  7100.  
  7101. /**
  7102. * 为按钮设置图标
  7103. * @param {string} icon 图标
  7104. * @returns {JQuery<HTMLElement>} 按钮
  7105. */
  7106. $.fn.setButtonIcon = function (icon) {
  7107. let i = this.find("i");
  7108. if (i.length != 0 && i.hasClass("iconfont")) {
  7109. i.html(icon);
  7110. } else {
  7111. i = OJB_safeCreateJQElement(`<i>${icon}</i>`);
  7112. this.prepend(i);
  7113. }
  7114. return this;
  7115. }
  7116.  
  7117. /**
  7118. * 设置按钮为加载等待状态
  7119. */
  7120. $.fn.setButtonLoading = function () {
  7121. this.addClass("loading");
  7122. this.prop("disabled", true);
  7123. return this;
  7124. }
  7125.  
  7126. /**
  7127. * 解除按钮的加载等待状态
  7128. */
  7129. $.fn.setButtonLoaded = function () {
  7130. this.removeClass("loading");
  7131. this.prop("disabled", false);
  7132. return this;
  7133. }
  7134.  
  7135. /**
  7136. * 为按钮设置popover提示文本
  7137. * @param {string} text 文本
  7138. * @returns {JQuery<HTMLElement>} 按钮
  7139. */
  7140. $.fn.setButtonPopover = function (text) {
  7141. // find if has popover_content class element
  7142. let popover_content = this.find(".popover_content");
  7143. if (popover_content.length != 0) {
  7144. popover_content.text(text);
  7145. } else {
  7146. popover_content = OJB_safeCreateJQElement(`<span class="popover_content">${text}</span>`);
  7147. this.append(popover_content);
  7148. }
  7149. return this;
  7150. }
  7151.  
  7152. /**
  7153. * 获取MarkDown
  7154. * @returns {string} MarkDown
  7155. */
  7156. $.fn.getMarkdown = function () {
  7157. const markdown = this.data('markdown');
  7158. if (markdown === undefined) {
  7159. const htmlContent = this.html();
  7160. const newMarkdown = OJBetter.common.turndownService.turndown(htmlContent);
  7161. this.data('markdown', newMarkdown);
  7162. return newMarkdown;
  7163. }
  7164. return markdown;
  7165. }
  7166.  
  7167. // 设置按钮状态
  7168. $.fn.setButtonState = function (state, popoverText = null, disabled = false) {
  7169. this.data('buttonState', state)
  7170. .prop('disabled', disabled)
  7171. .css('cursor', disabled ? 'not-allowed' : 'pointer')
  7172. .removeClass('running success enabled error loading redo');
  7173. if (popoverText) this.setButtonPopover(popoverText);
  7174.  
  7175. if (state !== 'initial') this.addClass(state);
  7176. return this;
  7177. };
  7178.  
  7179. // 为按钮添加鼠标悬浮重试
  7180. $.fn.setHoverRedo = function () {
  7181. this.hover(() => {
  7182. prevState = this.getButtonState();
  7183. if (prevState !== "normal" && prevState !== "running") {
  7184. this.setButtonState('redo');
  7185. }
  7186. }, () => {
  7187. const currentState = this.getButtonState();
  7188. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7189. this.setButtonState(prevState);
  7190. prevState = null;
  7191. }
  7192. });
  7193. };
  7194.  
  7195. // 获取按钮状态
  7196. $.fn.getButtonState = function () {
  7197. return this.data('buttonState') || 'normal';
  7198. };
  7199.  
  7200. // 设置翻译按钮状态
  7201. $.fn.setTransButtonState = function (state, text = null) {
  7202. const popoverText = text || i18next.t(`trans.${state}`, { ns: 'button' });
  7203. const disabled = state === 'running' || state === 'loading';
  7204. this.setButtonState(state, popoverText, disabled);
  7205. return this;
  7206. };
  7207.  
  7208. // 存翻译结果
  7209. $.fn.pushResultToTransButton = function (result) {
  7210. let resultStack = this.data('resultStack');
  7211. if (!resultStack) resultStack = [];
  7212. resultStack.push(result);
  7213. this.data('resultStack', resultStack);
  7214. }
  7215.  
  7216. // 获取翻译结果
  7217. $.fn.getResultFromTransButton = function () {
  7218. return this.data('resultStack');
  7219. }
  7220.  
  7221. // 标记为不自动翻译
  7222. $.fn.setNotAutoTranslate = function () {
  7223. this.data('notAutoTranslate', true);
  7224. }
  7225.  
  7226. // 获取是否为不自动翻译
  7227. $.fn.getNotAutoTranslate = function () {
  7228. return this.data('notAutoTranslate');
  7229. }
  7230.  
  7231. // 判断是否已经翻译
  7232. $.fn.IsTranslated = function () {
  7233. if (this.hasAttr('translated')) {
  7234. return true;
  7235. } else {
  7236. return false;
  7237. }
  7238. }
  7239.  
  7240. // 判断是否为评论区按钮
  7241. $.fn.IsCommentButton = function () {
  7242. let isCommentButton = this.data('isCommentButton');
  7243. if (isCommentButton == undefined) {
  7244. this.parents('.comments').length > 0 ? isCommentButton = true : isCommentButton = false;
  7245. this.data('isCommentButton', isCommentButton);
  7246. }
  7247. return isCommentButton;
  7248. }
  7249.  
  7250. // 按钮点击效果
  7251. $(document).on('mousedown', '.ojb_btn', function () {
  7252. $(this).addClass('active').on('animationend', () => $(this).removeClass('active'));
  7253. });
  7254. }
  7255.  
  7256. /**
  7257. * 添加题目markdown转换/复制/翻译按钮面板
  7258. * @param {HTMLElement} element 需要添加按钮面板的元素
  7259. * @param {string} suffix 按钮面板id后缀
  7260. * @param {string} type 按钮面板添加位置
  7261. * @param {boolean} is_simple 是否是简单模式
  7262. * @returns {object} 返回按钮面板元素
  7263. */
  7264. function addButtonPanel(element, suffix, type, is_simple = false) {
  7265. let text;
  7266. if (OJBetter.translation.comment.transMode == "1") text = i18next.t('trans.segment', { ns: 'button' });
  7267. else if (OJBetter.translation.comment.transMode == "2") text = i18next.t('trans.select', { ns: 'button' });
  7268. else text = i18next.t('trans.normal', { ns: 'button' });
  7269.  
  7270. let panel = OJB_safeCreateJQElement(`<div class='html2md-panel input-output-copier ${is_simple ? 'is_simple' : ''}'></div>`);
  7271. let viewButton = OJB_safeCreateJQElement(`
  7272. <button class='ojb_btn ojb_btn_popover top' id='html2md-view${suffix}'>
  7273. <i class="iconfont">&#xe7e5;</i>
  7274. <span class="popover_content">${i18next.t('md.normal', { ns: 'button' })}</span>
  7275. </button>`);
  7276. let copyButton = OJB_safeCreateJQElement(`
  7277. <button class='ojb_btn ojb_btn_popover top' id='html2md-cb${suffix}'>
  7278. <i class="iconfont">&#xe608;</i>
  7279. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  7280. </button>`);
  7281. let translateButton = OJB_safeCreateJQElement(`
  7282. <button class='ojb_btn translateButton ojb_btn_popover top' id='translateButton${suffix}'>
  7283. <i class="iconfont">&#xe6be;</i>
  7284. <span class="popover_content">${text}</span>
  7285. </button>`);
  7286. if (!is_simple) panel.append(viewButton);
  7287. if (!is_simple) panel.append(copyButton);
  7288. panel.append(translateButton);
  7289. if (type === "this_level") {
  7290. $(element).before(panel);
  7291. } else if (type === "child_level") {
  7292. $(element).prepend(panel);
  7293. }
  7294.  
  7295. return {
  7296. panel: panel,
  7297. viewButton: viewButton,
  7298. copyButton: copyButton,
  7299. translateButton: translateButton
  7300. }
  7301. }
  7302.  
  7303. /**
  7304. * 添加MD视图按钮
  7305. * @param {JQuery<HTMLElement>} button 按钮
  7306. * @param {JQuery<HTMLElement>} element 目标元素
  7307. * @param {string} suffix id后缀
  7308. * @param {string} type 类型
  7309. * @returns {void}
  7310. */
  7311. async function addButtonWithHTML2MD(button, element, suffix, type) {
  7312. /**
  7313. * 改变按钮状态
  7314. * @param {string} state 状态
  7315. */
  7316. const changeButtonState = (state) => {
  7317. if (state == "loading") {
  7318. button.setButtonLoading();
  7319. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7320. } else if (state == "loaded") {
  7321. button.setButtonLoaded();
  7322. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7323. } else if (state == "normal") {
  7324. button.removeClass("enabled");
  7325. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7326. } else if (state == "mdView") {
  7327. button.addClass("enabled");
  7328. button.setButtonPopover(i18next.t('md.reduction', { ns: 'button' }));
  7329. } else if (state == "disabled") {
  7330. button.prop("disabled", true);
  7331. button.setButtonPopover(i18next.t('md.disabled', { ns: 'button' }));
  7332. }
  7333. }
  7334.  
  7335. /**
  7336. * 存放目标元素的 JQueryObject
  7337. */
  7338. const target = (() => {
  7339. if (type = "child_level") {
  7340. return $(element).children(':not(.html2md-panel)');
  7341. } else {
  7342. return $(element);
  7343. }
  7344. })();
  7345.  
  7346. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7347. changeButtonState("disabled");
  7348. return;
  7349. } else {
  7350. changeButtonState("loading");
  7351. await waitForMathJaxIdle();
  7352. changeButtonState("loaded");
  7353. }
  7354.  
  7355. button.click(OJB_debounce(function () {
  7356. /**
  7357. * 检查是否是MarkDown视图
  7358. * @returns {boolean} 是否是MarkDown视图
  7359. */
  7360. function checkViewmd() {
  7361. if ($(element).attr("viewmd") === "true") {
  7362. return true;
  7363. } else {
  7364. return false;
  7365. }
  7366. }
  7367.  
  7368. /**
  7369. * 设置是否是MarkDown视图
  7370. * @param {boolean} value 是否是MarkDown视图
  7371. * @returns {void}
  7372. */
  7373. function setViewmd(value) {
  7374. $(element).attr("viewmd", value);
  7375. if (value) {
  7376. changeButtonState("mdView");
  7377. } else {
  7378. changeButtonState("normal");
  7379. }
  7380. }
  7381.  
  7382. if (checkViewmd()) {
  7383. setViewmd(false);
  7384. target.last().next(".mdViewContent").remove();
  7385. target.show();
  7386. } else {
  7387. setViewmd(true);
  7388. const markdown = $(element).getMarkdown();
  7389. const mdViewContent = OJB_safeCreateJQElement(`<span class="mdViewContent" style="width:auto; height:auto;">${markdown}</span>`);
  7390. target.last().after(mdViewContent);
  7391. target.hide();
  7392. }
  7393. }));
  7394.  
  7395. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7396. button.addHoverOverlay($(element));
  7397. }
  7398. }
  7399.  
  7400. /**
  7401. * 添加复制按钮
  7402. * @param {JQuery<HTMLElement>} button 按钮
  7403. * @param {JQuery<HTMLElement>} element 目标元素
  7404. * @param {string} suffix 后缀
  7405. * @param {string} type 类型
  7406. */
  7407. async function addButtonWithCopy(button, element, suffix, type) {
  7408. /**
  7409. * 改变按钮状态
  7410. * @param {string} state 状态
  7411. */
  7412. function changeButtonState(state) {
  7413. if (state == "loading") {
  7414. button.setButtonLoading();
  7415. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7416. } else if (state == "loaded") {
  7417. button.setButtonLoaded();
  7418. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7419. } else if (state == "normal") {
  7420. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7421. } else if (state == "copied") {
  7422. button.setButtonPopover(i18next.t('copy.copied', { ns: 'button' }));
  7423. } else if (state == "disabled") {
  7424. button.prop("disabled", true);
  7425. button.setButtonPopover(i18next.t('copy.disabled', { ns: 'button' }));
  7426. }
  7427. }
  7428.  
  7429. // 等待MathJax队列完成
  7430. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7431. changeButtonState("disabled");
  7432. return;
  7433. } else {
  7434. changeButtonState("loading");
  7435. await waitForMathJaxIdle();
  7436. changeButtonState("loaded");
  7437. }
  7438.  
  7439. button.click(OJB_debounce(function () {
  7440. var target = $(element).get(0);
  7441.  
  7442. var markdown = $(element).getMarkdown();
  7443.  
  7444. GM_setClipboard(markdown);
  7445.  
  7446. $(this).addClass("success");
  7447. changeButtonState("copied");
  7448.  
  7449.  
  7450. // 更新复制按钮文本
  7451. setTimeout(() => {
  7452. $(this).removeClass("success");
  7453. changeButtonState("normal")
  7454. }, 2000);
  7455. }));
  7456.  
  7457. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7458. button.addHoverOverlay($(element));
  7459. }
  7460. }
  7461.  
  7462. /**
  7463. * 添加翻译按钮
  7464. * @param {JQuery<HTMLElement>} button 按钮
  7465. * @param {JQuery<HTMLElement>} element 目标元素
  7466. * @param {string} suffix 后缀
  7467. * @param {string} type 类型
  7468. * @param {boolean} is_comment 是否是评论
  7469. */
  7470. async function addButtonWithTranslation(button, element, suffix, type, is_comment = false) {
  7471. /**
  7472. * 添加可指定翻译服务的方法调用
  7473. * @param {string} translation 翻译服务
  7474. */
  7475. button.data("translatedItBy", function (translation) {
  7476. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7477. OJBetter.common.taskQueue.addTask(translation, () => transTask(button, element, type, is_comment, translation), translation == 'openai');
  7478. });
  7479.  
  7480. // 等待MathJax队列完成
  7481. button.setButtonLoading();
  7482. await waitForMathJaxIdle();
  7483. button.setButtonLoaded();
  7484.  
  7485. // 标记目标文本区域不自动翻译
  7486. {
  7487. let text;
  7488. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7489. text = $(element).html();
  7490. } else {
  7491. text = $(element).getMarkdown();
  7492. }
  7493. let length = text.length;
  7494. if (length > OJBetter.translation.auto.shortTextLength || isEmptyText(text) || $(element).find('.spoiler').length > 0) {
  7495. button.setNotAutoTranslate();
  7496. }
  7497. // button.after(`<span>${length}</span>`); // 显示字符数
  7498. }
  7499.  
  7500. button.click(OJB_debounce(async function () {
  7501. // 重新翻译
  7502. let resultStack = $(this).getResultFromTransButton();
  7503. if (resultStack) {
  7504. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected");
  7505. for (let item of resultStack) {
  7506. if (OJBetter.translation.retransAction == "0") {
  7507. // 选段翻译不直接移除旧结果
  7508. if (OJBetter.translation.comment.transMode == "2") {
  7509. // 只移除即将要翻译的段的结果
  7510. if (pElements.is(item.translateDiv.getDiv().prev())) {
  7511. item.translateDiv.close();
  7512. }
  7513. } else {
  7514. item.translateDiv.close();
  7515. $($(element)).find(".translate-problem-statement, .translate-problem-statement-panel").remove();
  7516. }
  7517. } else {
  7518. item.translateDiv.fold();
  7519. }
  7520. }
  7521. }
  7522.  
  7523. // 翻译
  7524. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7525. OJBetter.common.taskQueue.addTask(OJBetter.translation.choice, () => transTask(button, element, type, is_comment), OJBetter.translation.choice == 'openai');
  7526. }));
  7527.  
  7528. // 重新翻译提示
  7529. let prevState;
  7530. button.hover(() => {
  7531. prevState = button.getButtonState();
  7532. if (prevState !== "normal" && prevState !== "running") {
  7533. button.setTransButtonState('redo');
  7534. }
  7535. }, () => {
  7536. const currentState = button.getButtonState();
  7537. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7538. button.setTransButtonState(prevState);
  7539. prevState = null;
  7540. }
  7541. });
  7542.  
  7543. // 目标区域指示
  7544. if (OJBetter.preference.hoverTargetAreaDisplay) {
  7545. button.addHoverOverlay($(element));
  7546. }
  7547.  
  7548. // 翻译右键切换菜单
  7549. $(document).on('contextmenu', '#translateButton' + suffix, function (e) {
  7550. e.preventDefault();
  7551.  
  7552. // 是否为评论的翻译
  7553. let is_comment = button.IsCommentButton();
  7554.  
  7555. // 移除旧的
  7556. if (!$(e.target).closest('.OJBetter_contextmenu').length) {
  7557. $('.OJBetter_contextmenu').remove();
  7558. }
  7559.  
  7560. var menu = $('<div class="OJBetter_contextmenu"></div>');
  7561. var translations = [
  7562. { value: 'deepl', name: i18next.t('translation.options.services.deepl', { ns: 'settings' }) },
  7563. { value: 'iflyrec', name: i18next.t('translation.options.services.iflyrec', { ns: 'settings' }) },
  7564. { value: 'youdao', name: i18next.t('translation.options.services.youdao', { ns: 'settings' }) },
  7565. { value: 'google', name: i18next.t('translation.options.services.google', { ns: 'settings' }) },
  7566. { value: 'caiyun', name: i18next.t('translation.options.services.caiyun', { ns: 'settings' }) },
  7567. { value: 'openai', name: i18next.t('translation.options.services.openai.name', { ns: 'settings' }) }
  7568. ];
  7569.  
  7570. // Function to check if the service supports the target language
  7571. function supportsTargetLanguage(service, targetLang) {
  7572. return OJBetter.supportList.translationSupport[service] && OJBetter.supportList.translationSupport[service][targetLang] !== undefined;
  7573. }
  7574.  
  7575. if (is_comment) {
  7576. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="0">
  7577. <span class="OJBetter_contextmenu_label_text">
  7578. ${i18next.t('translation.preference.comment_translation_choice.services.follow', { ns: 'settings' })}
  7579. </span></label>`);
  7580. menu.append(label);
  7581. }
  7582. translations.forEach(function (translation) {
  7583. if (supportsTargetLanguage(translation.value, OJBetter.translation.targetLang)) {
  7584. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="${translation.value}">
  7585. <span class="OJBetter_contextmenu_label_text">${translation.name}</span></label>`);
  7586. menu.append(label);
  7587. }
  7588. });
  7589.  
  7590. // 初始化
  7591. if (is_comment) {
  7592. menu.find(`input[name="translation"][value="${OJBetter.translation.comment.choice}"]`).prop('checked', true);
  7593. } else {
  7594. menu.find(`input[name="translation"][value="${OJBetter.translation.choice}"]`).prop('checked', true);
  7595. }
  7596. menu.css({
  7597. top: e.pageY + 'px',
  7598. left: e.pageX + 'px'
  7599. }).appendTo('body');
  7600.  
  7601. $(document).one('change', 'input[name="translation"]', function () {
  7602. if (is_comment) {
  7603. OJBetter.translation.comment.choice = $('input[name="translation"]:checked').val();
  7604. GM_setValue("commentTranslationChoice", OJBetter.translation.comment.choice);
  7605. } else {
  7606. OJBetter.translation.choice = $('input[name="translation"]:checked').val();
  7607. GM_setValue("translation", OJBetter.translation.choice);
  7608. }
  7609. $('.OJBetter_contextmenu').remove();
  7610. });
  7611.  
  7612. // 点击区域外关闭菜单
  7613. function handleClick(event) {
  7614. if (!$(event.target).closest('.OJBetter_contextmenu').length) {
  7615. $('.OJBetter_contextmenu').remove();
  7616. $(document).off('change', 'input[name="translation"]');
  7617. } else {
  7618. $(document).one('click', handleClick);
  7619. }
  7620. }
  7621. $(document).one('click', handleClick);
  7622. });
  7623. }
  7624.  
  7625. /**
  7626. * 创建翻译任务
  7627. * @param {JQuery<HTMLElement>} button 按钮
  7628. * @param {HTMLElement} element 目标元素
  7629. * @param {string} type 类型
  7630. * @param {boolean} is_comment 是否是评论
  7631. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7632. */
  7633. async function transTask(button, element, type, is_comment, overrideTrans) {
  7634. /** @type {HTMLElement} 目标元素 */
  7635. let target;
  7636. /**
  7637. * 错误计数数据结构
  7638. * @typedef {Object} count
  7639. * @property {number} errerNum 错误数量
  7640. * @property {number} skipNum 跳过数量
  7641. */
  7642. const count = {
  7643. errerNum: 0,
  7644. skipNum: 0
  7645. };
  7646. if (OJBetter.translation.comment.transMode == "1") {
  7647. // 分段翻译
  7648. let pElements = $(element).find("p:not(li p), li, .OJBetter_acmsguru");
  7649. for (let i = 0; i < pElements.length; i++) {
  7650. target = $(pElements[i]).eq(0).clone();
  7651. element_node = pElements[i];
  7652. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7653. }
  7654. } else if (OJBetter.translation.comment.transMode == "2") {
  7655. // 选段翻译
  7656. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected, .OJBetter_acmsguru");
  7657. for (let i = 0; i < pElements.length; i++) {
  7658. target = $(pElements[i]).eq(0).clone();
  7659. element_node = pElements[i];
  7660. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7661. }
  7662. $(element).find("p.block_selected:not(li p), li.block_selected").removeClass('block_selected');
  7663. } else {
  7664. // 普通翻译
  7665. target = $(element).eq(0).clone();
  7666. if (type === "child_level") $(target).children(':first').remove();
  7667. element_node = $($(element)).get(0);
  7668. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7669. }
  7670.  
  7671. // 翻译完成
  7672. if (!count.errerNum && !count.skipNum) {
  7673. button.setTransButtonState('success');
  7674. }
  7675. }
  7676.  
  7677. /**
  7678. * 翻译处理
  7679. * @param {JQuery<HTMLElement>} button 按钮
  7680. * @param {HTMLElement} target 目标元素
  7681. * @param {HTMLElement} element_node 目标节点
  7682. * @param {string} type 类型
  7683. * @param {boolean} is_comment 是否是评论
  7684. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7685. */
  7686. async function process(button, target, element_node, type, is_comment, count, overrideTrans) {
  7687. if (type === "child_level") {
  7688. let div = $("<div>");
  7689. $(element_node).append(div);
  7690. element_node = div.get(0);
  7691. }
  7692.  
  7693. //是否跳过折叠块
  7694. if ($(target).find('.spoiler').length > 0) {
  7695. const shouldSkip = await OJB_createDialog(
  7696. i18next.t('skipFold.title', { ns: 'dialog' }),
  7697. i18next.t('skipFold.content', { ns: 'dialog' }),
  7698. [
  7699. i18next.t('skipFold.buttons.0', { ns: 'dialog' }),
  7700. i18next.t('skipFold.buttons.1', { ns: 'dialog' })
  7701. ],
  7702. true
  7703. ); //跳过折叠块确认
  7704. if (shouldSkip) {
  7705. $(target).find('.spoiler').remove();
  7706. } else {
  7707. $(target).find('.html2md-panel').remove();
  7708. }
  7709. }
  7710.  
  7711. // 等待并获取结果
  7712. button.setTransButtonState('running');
  7713. const result = await blockProcessing(button, target, element_node, is_comment, overrideTrans);
  7714. button.pushResultToTransButton(result);
  7715.  
  7716. if (result.status == "error") count.errerNum += 1;
  7717. else if (result.status == "skip") count.skipNum += 1;
  7718. $(target).remove();
  7719. }
  7720.  
  7721. /**
  7722. * 块处理
  7723. * @param {JQuery<HTMLElement>} button
  7724. * @param {HTMLElement} target 目标元素
  7725. * @param {HTMLElement} element_node 目标节点
  7726. * @param {boolean} is_comment 是否是评论
  7727. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7728. * @returns {TranslateResult} 翻译结果对象
  7729. */
  7730. async function blockProcessing(button, target, element_node, is_comment, overrideTrans) {
  7731. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7732. target.markdown = $(target).html();
  7733. } else if (!target.markdown) {
  7734. target.markdown = OJBetter.common.turndownService.turndown($(target).html());
  7735. }
  7736.  
  7737. const result = await translateProblemStatement(target.markdown, element_node, is_comment, overrideTrans);
  7738. if (result.status == "skip") {
  7739. button.setTransButtonState('error', i18next.t('trans.tooLong', { ns: 'button' }));
  7740. result.translateDiv.close();
  7741. } else if (result.status == "error" || !result.rawData.done) {
  7742. result.translateDiv.setError();
  7743. result.translateDiv.setRawData(result.rawData);
  7744. result.translateDiv.showDebugButton();
  7745. button.setTransButtonState('error', i18next.t('trans.error', { ns: 'button' }));
  7746. $(target).remove();
  7747. }
  7748. return result;
  7749. }
  7750.  
  7751. /**
  7752. * 选段翻译支持
  7753. */
  7754. async function multiChoiceTranslation() {
  7755. GM_addStyle(`
  7756. .topic .content #task-statement {
  7757. overflow: initial;
  7758. }
  7759. `);
  7760.  
  7761. $(document).on('click', 'p, li:not(:has(.comment)), .OJBetter_acmsguru', function (e) {
  7762. let $this = $(this);
  7763. e.stopPropagation();
  7764. if ($this.hasClass('block_selected')) {
  7765. $this.removeClass('block_selected');
  7766. // 移除对应的按钮
  7767. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + $this.attr('OJBetter_p_id'));
  7768. } else {
  7769. let id = OJB_getRandomNumber(8);
  7770. $this.attr('OJBetter_p_id', id);
  7771. $this.addClass('block_selected');
  7772. // 添加按钮
  7773. let menu = OJB_safeCreateJQElement(`<div class="OJBetter_MiniTranslateButton" id='translateButton_selected_${id}'>${translateIcon}</div>`)
  7774. .css({
  7775. left: $($this).outerWidth(true) + $($this).position().left + 10 + 'px',
  7776. });
  7777. $this.before(menu);
  7778.  
  7779. $("#translateButton_selected_" + id).click(async function () {
  7780. // 处理旧的结果
  7781. if ($this.attr('translated')) {
  7782. let result = $this.data("resultData");
  7783. if (OJBetter.translation.retransAction == "0") {
  7784. result.translateDiv.close();
  7785. } else {
  7786. result.translateDiv.fold();
  7787. }
  7788. }
  7789. // 翻译
  7790. let target = $this.eq(0).clone();
  7791. let result = await blockProcessing(OJBetter.translation.choice, target, $this.eq(0), $("#translateButton_selected_" + id), false);
  7792. $this.data("resultData", result);
  7793. $this.removeClass('block_selected');
  7794. // 移除对应的按钮
  7795. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + id);
  7796. $this.attr('translated', '1'); // 标记已翻译
  7797. });
  7798. }
  7799. });
  7800. }
  7801.  
  7802. /**
  7803. * 添加MD/复制/翻译按钮
  7804. */
  7805. async function addConversionButton() {
  7806. // TODO 7
  7807. let promises = []; // 用于收集所有的 Promise
  7808.  
  7809. // 基本添加
  7810. if (!OJBetter.typeOfPage.is_homepage) {
  7811. $('section').each((i, e) => {
  7812. let id = "_" + OJB_getRandomNumber(8);
  7813. let panel = addButtonPanel(e, id, "this_level");
  7814. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7815. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7816. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7817. });
  7818. }
  7819.  
  7820. // 添加按钮到题解部分
  7821. if (OJBetter.typeOfPage.isEditorial) {
  7822. let contestNavTabs = $("#contest-nav-tabs");
  7823. let nextElement = contestNavTabs.next();
  7824. let id = "_editorial_1_" + OJB_getRandomNumber(8);
  7825. let panel = addButtonPanel(nextElement, id, "this_level");
  7826. panel.panel.css('width', '100%'); // 加个样式,不然不显示
  7827. promises.push(addButtonWithHTML2MD(panel.viewButton, nextElement, id, "this_level"));
  7828. promises.push(addButtonWithCopy(panel.copyButton, nextElement, id, "this_level"));
  7829. promises.push(addButtonWithTranslation(panel.translateButton, nextElement, id, "this_level"));
  7830. }
  7831.  
  7832. // 添加按钮到折叠块部分
  7833. $('details').each((i, e) => {
  7834. // 自定义测试样例折叠块不添加
  7835. if ($(e).attr('id') !== "customTestBlock") {
  7836. let id = "_details_" + OJB_getRandomNumber(8);
  7837. let panel = addButtonPanel(e, id, "child_level");
  7838. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "child_level"));
  7839. promises.push(addButtonWithCopy(panel.copyButton, e, id, "child_level"));
  7840. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  7841. }
  7842. });
  7843.  
  7844. // 添加到contest-statement部分
  7845. $('#contest-statement').each((i, e) => {
  7846. let id = "_contest-statement_" + OJB_getRandomNumber(8);
  7847. let panel = addButtonPanel(e, id, "this_level");
  7848. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7849. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7850. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7851. });
  7852.  
  7853. // 添加到blog-post部分
  7854. $('.blog-post').each((i, e) => {
  7855. let id = "_blog-post_" + OJB_getRandomNumber(8);
  7856. let panel = addButtonPanel(e, id, "this_level");
  7857. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  7858. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  7859. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  7860. });
  7861.  
  7862. return Promise.all(promises).catch(error => {
  7863. console.error("One or more of the Add Button operations failed: ", error);
  7864. });
  7865. };
  7866.  
  7867. /**
  7868. * 等待LaTeX渲染队列全部完成
  7869. * @returns {Promise} 完成渲染
  7870. */
  7871. function waitForMathJaxIdle() {
  7872. return true;
  7873. // return new Promise((resolve, reject) => {
  7874. // // 检查MathJax对象是否存在
  7875. // const checkMathJaxExists = () => {
  7876. // if (typeof MathJax === 'undefined') {
  7877. // // 如果MathJax不存在,稍后再次检查
  7878. // OJB_delay(100).then(checkMathJaxExists);
  7879. // } else {
  7880. // // MathJax存在,开始监视渲染队列
  7881. // startMonitoringQueue();
  7882. // }
  7883. // };
  7884.  
  7885. // // 开始监视MathJax渲染队列
  7886. // const startMonitoringQueue = () => {
  7887. // const intervalId = setInterval(() => {
  7888. // const queue = MathJax.Hub.queue;
  7889. // if (queue.pending === 0 && queue.running === 0) {
  7890. // clearInterval(intervalId);
  7891. // resolve();
  7892. // }
  7893. // }, 100);
  7894. // };
  7895.  
  7896. // // 开始检查MathJax对象
  7897. // checkMathJaxExists();
  7898. // });
  7899. }
  7900.  
  7901. /**
  7902. * 翻译结果面板
  7903. */
  7904. class TranslateDiv {
  7905. /**
  7906. * 构造函数
  7907. * @param {string} id 指定翻译框的id
  7908. */
  7909. constructor(id) {
  7910. this.id = id;
  7911. this.div = $('<div>').attr('id', id).addClass('translateDiv bounce-in');
  7912. if (!OJBetter.typeOfPage.is_completeProblemset) {
  7913. this.div.addClass('input-output-copier');
  7914. }
  7915. this.panelDiv = $('<div>').addClass('translate-problem-statement-panel');
  7916. this.div.append(this.panelDiv);
  7917.  
  7918. // 主要信息
  7919. this.mainDiv = $('<div>').addClass('translate-problem-statement');
  7920. this.span = $('<span>');
  7921. this.mainDiv.append(this.span);
  7922. this.div.append(this.mainDiv);
  7923. this.mainDivState = {
  7924. current: 'transHTML',
  7925. transHTML: '',
  7926. rawDataHTML: ''
  7927. };
  7928.  
  7929. // 顶栏信息
  7930. this.topText = $('<div>').addClass('topText');
  7931. this.panelDiv.append(this.topText);
  7932.  
  7933. // 右侧
  7934. this.rightDiv = $('<div>').css('display', 'flex');
  7935. this.panelDiv.append(this.rightDiv);
  7936. this.debugButton = OJB_safeCreateJQElement(`
  7937. <button class='ojb_btn ojb_btn_popover top'>
  7938. <i class="iconfont">&#xe641;</i>
  7939. <span class="popover_content">${i18next.t('rawData.normal', { ns: 'button' })}</span>
  7940. </button>`).hide();
  7941. this.rightDiv.append(this.debugButton);
  7942. this.queryBalanceButton = OJB_safeCreateJQElement(`
  7943. <button class='ojb_btn ojb_btn_popover top'>
  7944. <i class="iconfont">&#xe6ae;</i>
  7945. <span class="popover_content">${i18next.t('queryBalance.normal', { ns: 'button' })}</span>
  7946. </button>`).hide();
  7947. this.rightDiv.append(this.queryBalanceButton);
  7948. this.copyButton = OJB_safeCreateJQElement(`
  7949. <button class='ojb_btn ojb_btn_popover top'>
  7950. <i class="iconfont">&#xe608;</i>
  7951. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  7952. </button>`);
  7953. this.rightDiv.append(this.copyButton);
  7954. this.upButton = OJB_safeCreateJQElement(`
  7955. <button class='ojb_btn ojb_btn_popover top'>
  7956. <i class="iconfont">&#xe601;</i>
  7957. <span class="popover_content">${i18next.t('fold.normal', { ns: 'button' })}</span>
  7958. </button>`);
  7959. this.rightDiv.append(this.upButton);
  7960. this.closeButton = OJB_safeCreateJQElement(`
  7961. <button class='ojb_btn ojb_btn_popover top'>
  7962. <i class="iconfont">&#xe614;</i>
  7963. <span class="popover_content">${i18next.t('close.normal', { ns: 'button' })}</span>
  7964. </button>`);
  7965. this.rightDiv.append(this.closeButton);
  7966. }
  7967.  
  7968. /**
  7969. * 获取翻译框
  7970. * @returns {JQuery<HTMLElement>} 返回翻译框
  7971. */
  7972. getDiv() {
  7973. return this.div;
  7974. }
  7975.  
  7976. /**
  7977. * 设置翻译框顶部的文本
  7978. * @param {string} text 翻译框顶部的文本
  7979. */
  7980. setTopText(text) {
  7981. this.div.attr("data-topText", text);
  7982. this.topText.text(text);
  7983. }
  7984.  
  7985. /**
  7986. * 获取翻译框顶部的文本
  7987. * @returns {string} 返回翻译框顶部的文本
  7988. */
  7989. getTopText() {
  7990. return this.topText.text();
  7991. }
  7992.  
  7993. /**
  7994. * 渲染一个元素内的LaTeX公式
  7995. * @param {*} element
  7996. */
  7997. renderLaTeX(element) {
  7998. const latexRenderOptions = {
  7999. delimiters: [
  8000. { left: "$$", right: "$$", display: true },
  8001. { left: "\$$", right: "\\$$", display: true },
  8002. { left: "$", right: "$", display: false },
  8003. { left: "\$$", right: "\\$$", display: false }
  8004. ]
  8005. };
  8006.  
  8007. if (typeof renderMathInElement === 'function') {
  8008. renderMathInElement(element, latexRenderOptions);
  8009. }
  8010. }
  8011.  
  8012. /**
  8013. * 更新翻译框内容
  8014. * @param {string} text 文本内容
  8015. * @param {boolean} is_escapeHTML 是否转义HTML标签,为true则HTML标签将作为普通文本处理,默认为true
  8016. * @param {boolean} is_renderLaTeX 是否渲染LaTeX,为true则会渲染LaTeX,默认为true
  8017. */
  8018. updateTranslateDiv(text, is_escapeHTML = true, is_renderLaTeX = true,) {
  8019. // 渲染MarkDown
  8020. let md = window.markdownit({
  8021. html: !is_escapeHTML,
  8022. });
  8023. if (!text) text = "";
  8024. let html = md.render(text);
  8025. this.mainDiv.html(html);
  8026.  
  8027. // 渲染Latex
  8028. if (is_renderLaTeX) {
  8029. // MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.mainDiv.get(0)]);
  8030. this.renderLaTeX(this.mainDiv.get(0));
  8031. }
  8032. // 渲染代码块中的公式 (AtCoder)
  8033. this.mainDiv.find('pre code').each((index, element) => {
  8034. const codeText = $(element).text();
  8035. const latexPattern = /\$\$([^]*?)\$\$|\$(\\\$|[^\$])*?\$/;
  8036. if (latexPattern.test(codeText)) {
  8037. this.renderLaTeX(element);
  8038. }
  8039. });
  8040. }
  8041.  
  8042. /**
  8043. * 关闭元素
  8044. */
  8045. close() {
  8046. this.closeButton.click();
  8047. }
  8048.  
  8049. /**
  8050. * 收起元素
  8051. */
  8052. fold() {
  8053. if (!this.upButton.hasClass("reverse")) {
  8054. this.upButton.click();
  8055. }
  8056. }
  8057.  
  8058. /**
  8059. * 注册收起按钮事件
  8060. */
  8061. registerUpButtonEvent() {
  8062. this.upButton.on("click", () => {
  8063. // 如果没有reverse类,说明是展开状态
  8064. if (!this.upButton.hasClass("reverse")) {
  8065. // 执行收起操作
  8066. this.upButton.addClass("reverse");
  8067. this.upButton.setButtonState('initial', i18next.t('fold.unfold', { ns: 'button' }));
  8068. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8069. } else {
  8070. // 执行展开操作
  8071. this.upButton.removeClass("reverse");
  8072. this.upButton.setButtonState('initial', i18next.t('fold.normal', { ns: 'button' }));
  8073. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8074. }
  8075. });
  8076. }
  8077.  
  8078. /**
  8079. * 注册关闭按钮事件
  8080. */
  8081. registerCloseButtonEvent() {
  8082. this.closeButton.on("click", () => {
  8083. $(this.div).remove();
  8084. $(this.panelDiv).remove();
  8085. if (OJBetter.typeOfPage.is_problem && OJBetter.translation.memory.enabled) {
  8086. OJBetter.translation.memory.ttTree.rmTransResultMap(this.id); // 移除ttTree中的数据
  8087. OJBetter.translation.memory.ttTree.refreshNode("#task-statement");
  8088. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新DB中的数据
  8089. }
  8090. });
  8091. }
  8092.  
  8093. /**
  8094. * 注册复制按钮事件
  8095. * @param {string} text 复制的文本
  8096. */
  8097. registerCopyButtonEvent(text) {
  8098. this.copyButton.on("click", () => {
  8099. GM_setClipboard(text);
  8100. this.copyButton.setButtonState('success', i18next.t('copy.copied', { ns: 'button' }));
  8101. // 复制提示
  8102. setTimeout(() => {
  8103. this.copyButton.setButtonState('initial', i18next.t('copy.normal', { ns: 'button' }));
  8104. }, 2000);
  8105. });
  8106. }
  8107.  
  8108. /**
  8109. * 禁用复制按钮
  8110. */
  8111. disableCopyButton() {
  8112. this.copyButton.css({ 'fill': '#ccc' });
  8113. this.copyButton.off("click");
  8114. }
  8115.  
  8116. /**
  8117. * 设置面板为error状态
  8118. */
  8119. setError() {
  8120. this.div.addClass('error');
  8121. this.panelDiv.addClass('error');
  8122. this.mainDiv.addClass('error');
  8123. }
  8124.  
  8125. /**
  8126. * 设置原始数据数据
  8127. * @param {Object} Object 原始数据
  8128. */
  8129. setRawData(Object) {
  8130. this.mainDivState.rawDataHTML = $("<pre>").text(JSON.stringify(Object, null, 4)).get(0);
  8131. if (this.mainDivState.current === 'rawDataHTML') {
  8132. this.renderMainDiv();
  8133. }
  8134. }
  8135.  
  8136. /**
  8137. * 切换结果面板与原始数据面板
  8138. */
  8139. switchMainDiv() {
  8140. // 在切换之前,保存当前内容的状态
  8141. this.mainDivState[this.mainDivState.current] = this.mainDiv.html();
  8142. // 切换当前状态
  8143. this.debugButton.setButtonState(this.mainDivState.current === 'transHTML' ? 'enabled' : 'initial');
  8144. this.mainDivState.current = this.mainDivState.current === 'transHTML' ? 'rawDataHTML' : 'transHTML';
  8145. // 渲染新的当前状态
  8146. this.renderMainDiv();
  8147. }
  8148.  
  8149. // 渲染当前内容到 mainDiv
  8150. renderMainDiv() {
  8151. requestAnimationFrame(() => {
  8152. this.mainDiv.html(this.mainDivState[this.mainDivState.current]);
  8153. });
  8154. }
  8155.  
  8156. /**
  8157. * 注册debug按钮事件
  8158. */
  8159. registerDebugButtonEvent() {
  8160. this.debugButton.on("click", () => {
  8161. this.switchMainDiv();
  8162. });
  8163. }
  8164.  
  8165. /**
  8166. * 显示debug按钮
  8167. */
  8168. showDebugButton() {
  8169. this.debugButton.show();
  8170. this.registerDebugButtonEvent();
  8171. }
  8172.  
  8173. /**
  8174. * 注册查询余额按钮事件
  8175. * @param {function} callback 查询回调函数
  8176. */
  8177. registerQueryBalanceButtonEvent(callback) {
  8178. this.queryBalanceButton.on("click", async () => {
  8179. this.queryBalanceButton.setButtonState('loading', i18next.t('queryBalance.loading', { ns: 'button' }));
  8180. try {
  8181. const balance = await callback();
  8182. this.queryBalanceButton.setButtonState('success', `${i18next.t('queryBalance.success', { ns: 'button' })} ${balance}`);
  8183. } catch (error) {
  8184. this.queryBalanceButton.setButtonState('error', `${i18next.t('queryBalance.error', { ns: 'button' })} ${error.message}`);
  8185. }
  8186. });
  8187. }
  8188.  
  8189. /**
  8190. * 显示余额查询按钮
  8191. * @param {string} server 服务名称
  8192. */
  8193. showQueryBalanceButton(server) {
  8194. if (server == 'deepl') {
  8195. const quotaConfig = OJBetter.deepl.config.quota;
  8196. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8197. this.queryBalanceButton.show();
  8198. this.registerQueryBalanceButtonEvent(() => {
  8199. return queryServerBalance(OJBetter.deepl.config.quota);
  8200. });
  8201. }
  8202. } else if (server == 'openai') {
  8203. const quotaConfig = OJBetter.chatgpt.config.quota;
  8204. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8205. this.queryBalanceButton.show();
  8206. this.registerQueryBalanceButtonEvent(() => {
  8207. return queryServerBalance(OJBetter.chatgpt.config.quota);
  8208. });
  8209. }
  8210. }
  8211. }
  8212. }
  8213.  
  8214. // 元素关系树
  8215. class ElementsTree {
  8216. constructor(elements) {
  8217. this.node = [];
  8218. this.transResultMap = {};
  8219. this.index = 0;
  8220. // this.tagNames = ["DIV", "P", "UL", "LI"]
  8221. this.tagNames = ["DIV", "P", "UL", "LI", "SECTION", "SPAN"]
  8222. this.init($(elements));
  8223. }
  8224.  
  8225. // Iterate through all elements, because there may be multiple ttypography
  8226. init(elements) {
  8227. elements.each((i, e) => {
  8228. this.node.push({}); // add one element
  8229. this.index = 0; // reset index
  8230. this.create(i, $(e));
  8231. });
  8232. }
  8233.  
  8234. // 刷新关系树
  8235. refreshNode(elements) {
  8236. this.node = [];
  8237. this.index = 0;
  8238. this.init($(elements));
  8239. }
  8240.  
  8241. // 创建节点间的关系树
  8242. create(i_, element) {
  8243. var prev = null;
  8244. var node = this.node[i_];
  8245. element.children().each((i, e) => {
  8246. // only add element with tagNames
  8247. if (this.tagNames.includes($(e).prop("tagName"))) {
  8248. prev = this.addNode(i_, prev, e);
  8249. }
  8250. // recursively child element
  8251. if ($(e).children().length > 0 && prev !== null) {
  8252. node[prev].firstChild = this.index;
  8253. this.create(i_, $(e));
  8254. }
  8255. });
  8256. }
  8257.  
  8258. // 向树中添加一个节点
  8259. addNode(i_, prev, e) {
  8260. let node = this.node[i_];
  8261. node[this.index] = {
  8262. prev: prev,
  8263. next: null,
  8264. firstChild: null,
  8265. type: $(e).prop("tagName"),
  8266. isTranslateDiv: $(e).hasClass("translateDiv"),
  8267. topText: $(e).attr("data-topText"),
  8268. id: $(e).attr("id"),
  8269. };
  8270.  
  8271. if (prev !== null) {
  8272. node[prev].next = this.index;
  8273. }
  8274.  
  8275. prev = this.index;
  8276.  
  8277. this.index++;
  8278. return prev;
  8279. }
  8280.  
  8281. getNodeData() {
  8282. return this.node;
  8283. }
  8284.  
  8285. setNodeData(node) {
  8286. this.node = node;
  8287. }
  8288.  
  8289. getTransResultMap() {
  8290. return this.transResultMap;
  8291. }
  8292.  
  8293. setTransResultMap(transResultMap) {
  8294. this.transResultMap = transResultMap;
  8295. }
  8296.  
  8297. rmTransResultMap(id) {
  8298. delete this.transResultMap[id];
  8299. }
  8300.  
  8301. addTransResultMap(id, text) {
  8302. this.transResultMap[id] = text;
  8303. }
  8304.  
  8305. getTranslateDivNum(ttTree) {
  8306. var num = 0;
  8307. for (var i in ttTree) {
  8308. if (ttTree[i].isTranslateDiv) {
  8309. num++;
  8310. }
  8311. }
  8312. return num;
  8313. }
  8314.  
  8315. // 恢复目标元素中的translateDiv
  8316. recover(elements) {
  8317. elements.each((i, e) => {
  8318. var ttTreeNode = this.node[i];
  8319. var missingTranslateDivs = this.getTranslateDivNum(ttTreeNode);
  8320. if (missingTranslateDivs > 0) {
  8321. this.recoverOneElement($(e), ttTreeNode);
  8322. }
  8323. });
  8324. }
  8325.  
  8326. recoverOneElement(element, ttTreeNode) {
  8327. this.recoverOneFork(element.children().eq(0), ttTreeNode, 0);
  8328. }
  8329.  
  8330. // 恢复一个分支
  8331. recoverOneFork(pElement, ttTreeNode, index) {
  8332. do {
  8333. // only recover element with tagNames
  8334. if (!this.tagNames.includes(pElement.prop("tagName"))) {
  8335. if (pElement.next().length > 0) {
  8336. pElement = pElement.next();
  8337. } else {
  8338. return;
  8339. }
  8340. }
  8341. if (!ttTreeNode[index] || pElement.prop("tagName") !== ttTreeNode[index].type) {
  8342. // console.warn(`元素不存在或类型不同, 元素结构可能已经发生了变化: \nindex: ${index}`, pElement);
  8343. return;
  8344. } else {
  8345. // recursively child element
  8346. var node = ttTreeNode[index];
  8347. if (node.firstChild !== null) {
  8348. this.recoverOneFork(
  8349. pElement.children().eq(0),
  8350. ttTreeNode,
  8351. node.firstChild
  8352. );
  8353. }
  8354. // check if next node is translateDiv
  8355. if (node.next !== null) {
  8356. index = node.next;
  8357.  
  8358. var ne_node = ttTreeNode[index];
  8359. if (ne_node.isTranslateDiv) {
  8360. var id = ne_node.id;
  8361. var topText = ne_node.topText;
  8362. var text = this.transResultMap[id];
  8363. // create element after pElement
  8364. this.reCreateTransDiv(pElement, id, text, topText, node.isTranslateDiv); // 如果前面一个也是翻译结果,则该结果折叠
  8365. }
  8366. pElement = pElement.next(); // go to next element
  8367. }
  8368. }
  8369. } while (node.next !== null);
  8370. }
  8371.  
  8372. /**
  8373. * 重新创建translateDiv
  8374. * @param {*} pElement
  8375. * @param {*} id
  8376. * @param {*} translatedText
  8377. * @param {*} topText
  8378. * @param {Boolean} isFold 是否折叠
  8379. */
  8380. reCreateTransDiv(pElement, id, translatedText, topText, isFold) {
  8381. const translateDiv = new TranslateDiv(id);
  8382. pElement.after(translateDiv.getDiv());
  8383. translateDiv.setTopText(topText);
  8384. translateDiv.registerUpButtonEvent();
  8385. translateDiv.registerCloseButtonEvent();
  8386. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8387. translateDiv.registerCopyButtonEvent(translatedText);
  8388. } else {
  8389. translateDiv.disableCopyButton();
  8390. }
  8391. translateDiv.updateTranslateDiv(translatedText, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru));
  8392. // 标记已翻译并添加到翻译按钮的结果栈中
  8393. let transButton = pElement.prev('.html2md-panel').find('.translateButton');
  8394. if (transButton.length == 0) {
  8395. // 如果没有找到,则应该是得在父元素中找到
  8396. transButton = pElement.parent().prev('.html2md-panel').find('.translateButton');
  8397. }
  8398. if (isFold) translateDiv.fold(); // 是否折叠该翻译
  8399. transButton.pushResultToTransButton({
  8400. translateDiv: translateDiv,
  8401. status: 0
  8402. });
  8403. transButton.setTransButtonState('success');
  8404. }
  8405. }
  8406.  
  8407. // 更新TransDB中的翻译数据
  8408. async function updateTransDBData(nodeDate, transResultMap) {
  8409. var url = window.location.href.replace(/#/, "");
  8410. try {
  8411. await OJBetter.common.database.translateData.put({ url, transResultMap, nodeDate });
  8412. return 'translateData saved successfully';
  8413. } catch (error) {
  8414. throw new Error(`Failed to save translateData: ${error}`);
  8415. }
  8416. }
  8417.  
  8418. // 获取TransDB中保存的翻译数据
  8419. async function getTransDBData() {
  8420. var url = window.location.href.replace(/#/, "");
  8421. try {
  8422. const result = await OJBetter.common.database.translateData.get(url);
  8423. return result;
  8424. } catch (error) {
  8425. throw new Error(`Failed to get translateData: ${error}`);
  8426. }
  8427. }
  8428.  
  8429. /**
  8430. * 翻译结果恢复功能初始化
  8431. * @returns
  8432. */
  8433. async function initTransResultsRecover() {
  8434. OJBetter.translation.memory.ttTree = new ElementsTree("#task-statement"); // 初始化当前页面#task-statement元素的结构树
  8435. let result = await getTransDBData();
  8436. if (!result) return;
  8437. OJBetter.translation.memory.ttTree.setNodeData(result.nodeDate);
  8438. OJBetter.translation.memory.ttTree.setTransResultMap(result.transResultMap);
  8439. OJBetter.translation.memory.ttTree.recover($("#task-statement"));
  8440. }
  8441.  
  8442. /**
  8443. * 自动翻译
  8444. */
  8445. async function initTransWhenViewable() {
  8446. await waitForMathJaxIdle();
  8447.  
  8448. // const elements = $('.ttypography, .comments').find('.translateButton');
  8449. const elements = $('#task-statement').find('.translateButton');
  8450. const observers = [];
  8451.  
  8452. // Use a single Intersection Observer for all elements
  8453. const observer = new IntersectionObserver((entries, obs) => {
  8454. entries.forEach((entry) => {
  8455. if (entry.isIntersecting) {
  8456. const button = $(entry.target);
  8457. const state = button.getButtonState();
  8458. const notAutoTranslate = button.getNotAutoTranslate();
  8459. // Check if the button meets the criteria
  8460. if (state === 'normal' && !notAutoTranslate) {
  8461. let trans = OJBetter.translation.choice;
  8462.  
  8463. if (OJBetter.translation.auto.mixTrans.enabled && button.IsCommentButton() && OJBetter.translation.auto.mixTrans.servers.length > 0) {
  8464. const randomIndex = Math.floor(Math.random() * OJBetter.translation.auto.mixTrans.servers.length);
  8465. trans = OJBetter.translation.auto.mixTrans.servers[randomIndex];
  8466. }
  8467. button.data("translatedItBy")(trans);
  8468. }
  8469.  
  8470. // Stop observing the element
  8471. obs.unobserve(entry.target);
  8472. }
  8473. });
  8474. });
  8475.  
  8476. // Observe each element
  8477. elements.each((i, e) => {
  8478. observer.observe(e);
  8479. });
  8480.  
  8481. // Store the observer in case you need to disconnect it later
  8482. observers.push(observer);
  8483. }
  8484.  
  8485. /**
  8486. * 翻译返回结果结构体
  8487. * @typedef {Object} TranslateResult
  8488. * @property {string} status 翻译状态
  8489. * @property {TranslateDiv} translateDiv 翻译结果面板
  8490. * @property {TransRawData} rawData 原始翻译数据
  8491. */
  8492.  
  8493. /**
  8494. * 翻译主方法
  8495. * @param {string} text 待翻译文本
  8496. * @param {HTMLElement} element_node 元素节点
  8497. * @param {Boolean} is_comment 是否为评论区文本
  8498. * @param {string} overrideTrans 覆盖全局翻译服务设定
  8499. * @returns {TranslateResult} 翻译结果对象
  8500. */
  8501. async function translateProblemStatement(text, element_node, is_comment, overrideTrans) {
  8502. /** @type {number} 翻译结果的ID*/
  8503. const id = OJB_getRandomNumber(8);
  8504. /** @type {TextBlockReplacer} 文本块替换/恢复实例*/
  8505. const textBlockReplacer = new TextBlockReplacer();
  8506. /** @type {string} 翻译结果文本*/
  8507. let translatedText = "";
  8508.  
  8509. /** @type {string} 当前实际应用的翻译服务 */
  8510. const realTransServer = overrideTrans ||
  8511. (is_comment && OJBetter.translation.comment.choice != "0" ?
  8512. OJBetter.translation.comment.choice :
  8513. OJBetter.translation.choice);
  8514.  
  8515. /** @type {TranslateResult} 翻译结果对象 */
  8516. const translateResult = {
  8517. status: "ok",
  8518. rawData: {
  8519. done: false
  8520. }
  8521. }
  8522.  
  8523. /**
  8524. * LaTeX替换
  8525. * @param {string} text 待翻译文本
  8526. * @returns {string} 处理后的文本
  8527. */
  8528. const replaceLatex = function (text) {
  8529. if (OJBetter.typeOfPage.is_oldLatex) {
  8530. const regex = /<span\s+class="tex-span">.*?<\/span>/gi;
  8531. text = textBlockReplacer.replace(text, regex);
  8532. text = text.replace(/<p>(.*?)<\/p>/g, "$1\n\n"); // <p/>标签换为换行
  8533. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8534. const regex = /<i>.*?<\/i>|<sub>.*?<\/sub>|<sup>.*?<\/sup>|<pre>.*?<\/pre>/gi;
  8535. text = textBlockReplacer.replace(text, regex);
  8536. } else if (realTransServer != "openai") {
  8537. // 使用GPT翻译时不必替换latex公式
  8538. const regex = /\$\$([^]*?)\$\$|\$(\\\$|[^\$])*?\$/g;
  8539. text = textBlockReplacer.replace(text, regex);
  8540.  
  8541. // 替换行间代码块```
  8542. const regex2 = /```[\s\S]*?```/g;
  8543. text = textBlockReplacer.replace(text, regex2);
  8544. }
  8545. return text;
  8546. }
  8547.  
  8548. /**
  8549. * LaTeX恢复
  8550. * @param {string} text 已翻译的文本
  8551. * @returns {string} 恢复后的文本
  8552. */
  8553. const recoverLatex = function (text) {
  8554. // 两个公式之间加个空格,防止有些LaTeX解析器解析错误
  8555. let resultText = text
  8556. .replace(/】【/g, '】 【')
  8557. .replace(/\]\[/g, '] [')
  8558. .replace(/\}\{/g, '} {');
  8559.  
  8560. if (OJBetter.typeOfPage.is_oldLatex) {
  8561. resultText = resultText.replace(/(.+?)(\n\n|$)/g, "<p>$1</p>"); // 换行符还原为<p/>标签
  8562. resultText = textBlockReplacer.recover(resultText);
  8563. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8564. resultText = textBlockReplacer.recover(resultText);
  8565. } else if (realTransServer != "openai") {
  8566. resultText = textBlockReplacer.recover(resultText);
  8567. }
  8568. return resultText;
  8569. }
  8570.  
  8571. /**
  8572. * 格式化翻译结果
  8573. * @param {string} text
  8574. * @returns {string} 处理后的翻译结果
  8575. */
  8576. const formatText = function (text) {
  8577. // 转义LaTex中的特殊符号
  8578. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8579.  
  8580. // 先替换掉行间代码块
  8581. const replacer = new TextBlockReplacer();
  8582. text = replacer.replace(text, /```[\s\S]*?```/g);
  8583.  
  8584. // 处理LaTeX公式
  8585. const escapeRules = [
  8586. { pattern: /(?<!\\)>(?!\s)/g, replacement: " &gt; " }, // >符号
  8587. { pattern: /(?<!\\)</g, replacement: " &lt; " }, // <符号
  8588. { pattern: /(?<!\\)\*/g, replacement: " &#42; " }, // *符号
  8589. { pattern: /(?<!\\)_/g, replacement: " &#95; " }, // _符号
  8590. { pattern: /(?<!\\)\\\\(?=\s)/g, replacement: "\\\\\\\\" }, // \\符号
  8591. { pattern: /(?<!\\)\\(?![\\a-zA-Z0-9])/g, replacement: "\\\\" }, // \符号
  8592. ];
  8593.  
  8594. let latexMatches = [...text.matchAll(/\$\$([\s\S]*?)\$\$|\$(.*?)\$|\$([\s\S]*?)\$/g)];
  8595. for (const match of latexMatches) {
  8596. const matchedText = match[0];
  8597. let escapedText = matchedText;
  8598.  
  8599. for (const rule of escapeRules) {
  8600. escapedText = escapedText.replaceAll(rule.pattern, rule.replacement);
  8601. }
  8602. escapedText = escapedText.replace(/\$\$/g, "$$$$$$$$");// $$符号(因为后面需要作为replacement,双倍消耗)
  8603. text = text.replace(matchedText, escapedText);
  8604. }
  8605.  
  8606. // 恢复行间代码块
  8607. text = replacer.recover(text);
  8608. }
  8609.  
  8610. // // 使符合mathjx的转换语法
  8611. // const mathjaxRuleMap = [
  8612. // { pattern: /\$/g, replacement: "$$$$$$" }, // $$ 行间
  8613. // ];
  8614. // mathjaxRuleMap.forEach(({ pattern, replacement }) => {
  8615. // text = text.replace(pattern, replacement);
  8616. // });
  8617.  
  8618. // markdown修正
  8619. const mdRuleMap = [
  8620. { pattern: /(\s_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: "$1 $2" }, // 斜体
  8621. { pattern: /(_[\u4e00-\u9fa5]+_\s)([\u4e00-\u9fa5]+)/g, replacement: " $1$2" },
  8622. { pattern: /(_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: " $1 $2" },
  8623. { pattern: /(([\s\S]*?))/g, replacement: "($1)" }, // 中文()
  8624. // { pattern: /:/g, replacement: ":" }, // 中文:
  8625. { pattern: /\*\* (.*?) \*\*/g, replacement: "\*\*$1\*\*" } // 加粗
  8626. ];
  8627. mdRuleMap.forEach(({ pattern, replacement }) => {
  8628. text = text.replace(pattern, replacement);
  8629. });
  8630.  
  8631. return text;
  8632. }
  8633.  
  8634. // 创建翻译结果元素并放在element_node的后面
  8635. translateResult.translateDiv = new TranslateDiv(id);
  8636. $(element_node).after(translateResult.translateDiv.getDiv());
  8637.  
  8638. // 顶栏左侧信息
  8639. translateResult.translateDiv.setTopText(i18next.t('servers.' + realTransServer, { ns: 'translator' }) +
  8640. i18next.t('translateDiv.topTextSuffix', { ns: 'translator' }));
  8641.  
  8642. // 注册按钮
  8643. translateResult.translateDiv.registerUpButtonEvent();
  8644. translateResult.translateDiv.registerCloseButtonEvent();
  8645. if (OJBetter.translation.choice == 'openai' || OJBetter.translation.choice == 'deepl') {
  8646. translateResult.translateDiv.showQueryBalanceButton(OJBetter.translation.choice); // 显示额度查询
  8647. }
  8648.  
  8649. // 翻译内容是否可能为代码片段
  8650. if (isEmptyText(text)) {
  8651. const shouldContinue = await OJB_createDialog(
  8652. i18next.t('isEmptyText.title', { ns: 'dialog' }),
  8653. i18next.t('isEmptyText.content', { ns: 'dialog' }),
  8654. [
  8655. i18next.t('isEmptyText.buttons.0', { ns: 'dialog' }),
  8656. i18next.t('isEmptyText.buttons.1', { ns: 'dialog' })
  8657. ],
  8658. true
  8659. );
  8660. if (shouldContinue) {
  8661. translateResult.status = "skip";
  8662. return translateResult;
  8663. }
  8664. }
  8665.  
  8666. // 替换latex公式
  8667. text = replaceLatex(text);
  8668.  
  8669. // 过滤**号
  8670. if (OJBetter.translation.filterTextWithoutEmphasis && GM_getValue("translation") !== "openai") { // TODO
  8671. text = text.replace(/\*\*/g, "");
  8672. }
  8673.  
  8674. // 字符数上限
  8675. const translationLimits = {
  8676. deepl: 5000,
  8677. iflyrec: 2000,
  8678. youdao: 5000,
  8679. google: 5000,
  8680. caiyun: 5000
  8681. };
  8682. if (translationLimits.hasOwnProperty(realTransServer) && text.length > translationLimits[realTransServer]) {
  8683. let textLength = translationLimits[realTransServer];
  8684. let realTextLength = text.length;
  8685. const shouldContinue = await OJB_createDialog(
  8686. i18next.t('transTextLimits.title', { ns: 'dialog' }),
  8687. i18next.t('transTextLimits.content', { ns: 'dialog', textLength: textLength, realTextLength: realTextLength }),
  8688. [
  8689. i18next.t('transTextLimits.buttons.0', { ns: 'dialog' }),
  8690. i18next.t('transTextLimits.buttons.1', { ns: 'dialog' })
  8691. ],
  8692. true
  8693. ); // 字数超限确认
  8694. if (shouldContinue) {
  8695. translateResult.status = "skip";
  8696. return translateResult;
  8697. }
  8698. }
  8699.  
  8700. /**
  8701. * 调用各个翻译服务
  8702. * @param {string} transServer 翻译服务
  8703. * @returns {TransRawData} 原始翻译数据
  8704. */
  8705. async function translate(transServer) {
  8706. const is_renderLaTeX = !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru);
  8707. const servername = i18next.t('servers.' + realTransServer, { ns: 'translator' });
  8708. /** @type {TransRawData} 原始翻译数据*/
  8709. let rawData = {};
  8710. try {
  8711. if (transServer == "deepl") {
  8712. if (OJBetter.deepl.config.type == 'free') {
  8713. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8714. rawData = await translate_deepl(text);
  8715. } else if (OJBetter.deepl.config.type == 'api') {
  8716. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.deeplApi', { ns: 'translator', deepl_configName: OJBetter.deepl.config.name })}`, is_renderLaTeX);
  8717. if (OJBetter.deepl.config.apiGenre == 'deeplx') {
  8718. rawData = await translate_deeplx(text);
  8719. } else {
  8720. if (OJBetter.deepl.enableEmphasisProtection) text = convertBoldMarkdownToHTML(text);
  8721. if (OJBetter.deepl.enableLinkProtection) text = convertLinksMarkdownToHTML(text);
  8722. if (OJBetter.deepl.config.apiGenre == 'api-free') {
  8723. rawData = await translate_deepl_api_free(text);
  8724. } else if (OJBetter.deepl.config.apiGenre == 'api-pro') {
  8725. rawData = await translate_deepl_api_pro(text);
  8726. }
  8727. if (OJBetter.deepl.enableEmphasisProtection) rawData.text = convertBoldHTMLToMarkdown(rawData.text);
  8728. if (OJBetter.deepl.enableLinkProtection) rawData.text = convertLinksHTMLToMarkdown(rawData.text);
  8729. }
  8730. }
  8731. } else if (transServer == "iflyrec") {
  8732. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8733. rawData = await translate_iflyrec(text);
  8734. } else if (transServer == "youdao") {
  8735. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8736. rawData = await translate_youdao_mobile(text);
  8737. } else if (transServer == "google") {
  8738. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8739. rawData = await translate_gg(text);
  8740. } else if (transServer == "caiyun") {
  8741. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8742. rawData = await translate_caiyun(text);
  8743. } else if (transServer == "openai") {
  8744. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.openai', { ns: 'translator', openai_name: OJBetter.chatgpt.config.name })}${!OJBetter.chatgpt.isStream
  8745. ? i18next.t('transingTip.openai_isStream', { ns: 'translator' }) : ""}`,
  8746. is_renderLaTeX);
  8747. if (OJBetter.chatgpt.isStream) {
  8748. // 流式传输
  8749. rawData = await translate_openai_stream(text, translateResult.translateDiv);
  8750. } else {
  8751. // 普通模式
  8752. rawData = await translate_openai(text);
  8753. }
  8754. }
  8755. translatedText = rawData.text;
  8756. if (!rawData.done) {
  8757. translateResult.status = "error";
  8758. }
  8759. } catch (e) {
  8760. translateResult.status = "error";
  8761. rawData.message = i18next.t('error.unexpected', { ns: 'translator' });
  8762. console.warn(e);
  8763. }
  8764. return rawData;
  8765. }
  8766. translateResult.rawData = await translate(realTransServer);
  8767.  
  8768. if (translateResult.status == "error") {
  8769. translateResult.translateDiv.updateTranslateDiv(translateResult.rawData.message);
  8770. return translateResult;
  8771. }
  8772.  
  8773. // 还原latex公式
  8774. translatedText = recoverLatex(translatedText);
  8775.  
  8776. // 注册结果复制按钮
  8777. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8778. translateResult.translateDiv.registerCopyButtonEvent(translatedText);
  8779. } else {
  8780. translateResult.translateDiv.disableCopyButton();
  8781. }
  8782.  
  8783. // 翻译结果格式化
  8784. translatedText = formatText(translatedText);
  8785.  
  8786. // 保存翻译结果
  8787. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  8788. // OJBetter.translation.memory.ttTree.refreshNode(".ttypography"); // 刷新当前页面.ttypography元素的结构树实例
  8789. OJBetter.translation.memory.ttTree.refreshNode("#task-statement"); // 刷新当前页面.ttypography元素的结构树实例
  8790. OJBetter.translation.memory.ttTree.addTransResultMap(id, translatedText);
  8791. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新翻译结果到transDB
  8792. }
  8793.  
  8794. // 翻译结果面板更新
  8795. translateResult.translateDiv.updateTranslateDiv(translatedText, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru));
  8796.  
  8797. return translateResult;
  8798. }
  8799.  
  8800. //弹窗翻译
  8801. function alertZh() {
  8802. // var _alert = window.alert;
  8803. // window.alert = async function (msg) {
  8804. // _alert(msg + "\n=========翻译=========\n" + await translate_deepl(msg));
  8805. // return true;
  8806. // }
  8807. };
  8808.  
  8809. /**
  8810. * 折叠块展开
  8811. */
  8812. function ExpandFoldingblocks() {
  8813. $('.spoiler').addClass('spoiler-open');
  8814. $('.spoiler-content').attr('style', '');
  8815. };
  8816.  
  8817. /**
  8818. * 折叠块渲染优化
  8819. */
  8820. function RenderPerfOpt() {
  8821. GM_addStyle(`
  8822. .spoiler-content {
  8823. contain: layout style;
  8824. }
  8825. `);
  8826. }
  8827.  
  8828. /**
  8829. * 下拉选择框性能优化
  8830. */
  8831. async function SelectElementPerfOpt() {
  8832. // TODO 10
  8833. // 加载库资源
  8834. await OJB_LoadJS("https://aowuucdn.oss-accelerate.aliyuncs.com/js/selectpage.min.js", "sha512-HhBheWc9nbTuTG0oVYtY9c3nkJAAiuk899lycOtB8NALvp20CNOjlYdTAYbRy9/0zXnLl0LZpiwhfLZurvK1XQ==");
  8835. /**
  8836. * 将一个<select>元素转换为SelectPage控件
  8837. * @param {HTMLElement|string} selector - 要转换的<select>元素或其选择器
  8838. */
  8839. const OJB_transformSelectToSelectPage = (selector) => {
  8840. const $select = $(selector);
  8841. if ($select.length === 0 || !$select.is('select')) {
  8842. console.error('Invalid select element provided.');
  8843. return;
  8844. }
  8845.  
  8846. // 隐藏原生的<select>元素
  8847. $select.hide();
  8848.  
  8849. // 创建一个新的<input>元素用于SelectPage控件
  8850. const $inputForSelectPage = $('<input>', {
  8851. type: 'text',
  8852. class: 'selectpage-input',
  8853. autocomplete: 'off'
  8854. });
  8855. $select.after($inputForSelectPage);
  8856.  
  8857. // 准备SelectPage所需的数据格式
  8858. const data = $select.find('option').map((_, option) => ({
  8859. id: option.value,
  8860. text: option.text
  8861. })).get();
  8862.  
  8863. // 初始化SelectPage
  8864. $inputForSelectPage.selectPage({
  8865. showField: 'text',
  8866. keyField: 'id',
  8867. data,
  8868. lang: 'en',
  8869. // 当选中一个选项时,更新隐藏的<select>元素的值
  8870. eSelect: (data) => {
  8871. $select.val(data.id).trigger('change');
  8872. },
  8873. // 初始化时根据<select>的当前值设置SelectPage
  8874. initRecord: $select.val()
  8875. });
  8876. };
  8877.  
  8878. // 遍历页面上的所有select
  8879. $('select').each((_, select) => {
  8880. // 选项大于500才优化
  8881. if ($(select).find('option').length > 500) {
  8882. OJB_transformSelectToSelectPage(select);
  8883. }
  8884. });
  8885. }
  8886.  
  8887. /**
  8888. * 题目页相关链接栏
  8889. */
  8890. class ProblemPageLinkbar {
  8891. constructor() {
  8892. this.containerElement = this.createToolbar();
  8893. this.commandInvoker = new CommandInvoker();
  8894. }
  8895.  
  8896. /**
  8897. * 创建工具栏
  8898. */
  8899. createToolbar() {
  8900. // const toolbarElement = $("<div>").attr("id", "problemToolbar").insertBefore($(".problemindexholder"));
  8901. const toolbarElement = $("<div>").attr("id", "problemToolbar").insertBefore($(".h2"));
  8902. return new DOMContainer(toolbarElement);
  8903. }
  8904.  
  8905. /**
  8906. * 添加按钮
  8907. * @param {string} id 按钮id
  8908. * @param {string} url 按钮链接
  8909. * @param {string} text 按钮文字
  8910. * @param {JQueryObject} icon 按钮图标
  8911. * @param {string} iconHeight 图标高度
  8912. * @returns {object} 按钮对象
  8913. */
  8914. addLinkButton(id, url, text, icon = $('<div>'), iconHeight = "22px") {
  8915. const linkElement = $("<a>")
  8916. .attr("href", url)
  8917. .attr("target", "_blank")
  8918. .addClass("ojb_btn")
  8919. .attr("id", id);
  8920.  
  8921. linkElement.append(icon);
  8922. icon.css("height", iconHeight);
  8923.  
  8924. const textSpan = $("<span>").html(text);
  8925. linkElement.append(textSpan);
  8926.  
  8927. this.commandInvoker.execute(new AddElementCommand(this.containerElement, linkElement));
  8928. return {
  8929. element: linkElement,
  8930. text: textSpan,
  8931. icon: icon
  8932. };
  8933. }
  8934.  
  8935. /**
  8936. * 更新链接
  8937. * @param {object} button 按钮对象
  8938. * @param {string} url 按钮链接
  8939. */
  8940. updateUrl(button, url) {
  8941. button.element.attr("href", url);
  8942. }
  8943.  
  8944. /**
  8945. * 更新文字
  8946. * @param {object} button 按钮对象
  8947. * @param {string} text 按钮文字
  8948. */
  8949. updateText(button, text) {
  8950. button.text.html(text);
  8951. }
  8952.  
  8953. /**
  8954. * 设置文字为粗体
  8955. * @param {object} button 按钮对象
  8956. */
  8957. setBold(button) {
  8958. button.text.css("font-weight", "bold");
  8959. }
  8960.  
  8961. /**
  8962. * 更新图标
  8963. * @param {object} button 按钮对象
  8964. * @param {JQueryObject} icon 按钮图标
  8965. * @param {string} iconHeight 图标高度
  8966. */
  8967. updateIcon(button, icon, iconHeight = "16px") {
  8968. button.icon.remove();
  8969. button.text.prepend(icon);
  8970. icon.css("height", iconHeight);
  8971. button.icon = icon;
  8972. }
  8973.  
  8974. /**
  8975. * 添加类
  8976. * @param {object} button 按钮对象
  8977. * @param {string} className 类名
  8978. */
  8979. addClass(button, className) {
  8980. button.element.addClass(className);
  8981. }
  8982.  
  8983. /**
  8984. * 禁用链接按钮
  8985. * @param {object} button 按钮对象
  8986. */
  8987. disableButton(button) {
  8988. button.element.addClass("disabled");
  8989. }
  8990.  
  8991. /**
  8992. * 启用链接按钮
  8993. * @param {object} button 按钮对象
  8994. */
  8995. enableButton(button) {
  8996. button.element.removeClass("disabled");
  8997. }
  8998. }
  8999.  
  9000. /**
  9001. * 获取题目的id
  9002. * @param {String} url 题目的链接
  9003. * @returns 题目的id,形如2000A
  9004. */
  9005. function getProblemId(url) {
  9006. const regex = /\/contests\/([A-Za-z\d\-]+)\/tasks\/([A-Za-z\d\_]+)/;
  9007. const matchResult = url.match(regex);
  9008. return matchResult && matchResult.length >= 3
  9009. ? `${matchResult[2]}`
  9010. : '';
  9011. };
  9012.  
  9013. /**
  9014. * 跳转到洛谷
  9015. * @param {ProblemPageLinkbar} problemToolbar
  9016. */
  9017. async function CF2luogu(problemToolbar) {
  9018. const url = window.location.href;
  9019. const problemId = getProblemId(url);
  9020. const luoguButton = problemToolbar.addLinkButton(
  9021. "luoguButton",
  9022. "https://www.luogu.com.cn/",
  9023. i18next.t('state.loading', { ns: 'button' }),
  9024. $("<img>").attr("src", "https://cdn.luogu.com.cn/fe/logo.png")
  9025. );
  9026.  
  9027. const checkLinkExistence = async (url) => {
  9028. return OJB_promiseRetryWrapper(async () => {
  9029. const response = await OJB_GMRequest({
  9030. method: "GET",
  9031. url
  9032. });
  9033. return !response.responseText.match(/出错了/g);
  9034. }, {
  9035. maxRetries: 3,
  9036. retryInterval: 1000
  9037. });
  9038. };
  9039.  
  9040. const LuoguUrl = `https://www.luogu.com.cn/problem/AT_${problemId}`;
  9041. try {
  9042. const result = await checkLinkExistence(LuoguUrl);
  9043. if (problemId && result) {
  9044. problemToolbar.updateText(luoguButton, "");
  9045. problemToolbar.updateUrl(luoguButton, LuoguUrl);
  9046. } else {
  9047. problemToolbar.updateText(luoguButton, i18next.t('state.404', { ns: 'button' }));
  9048. problemToolbar.disableButton(luoguButton);
  9049. }
  9050. } catch (error) {
  9051. if (error instanceof OJB_GMError && error.type == "error") {
  9052. problemToolbar.updateText(luoguButton, i18next.t('state.netError', { ns: 'button' }));
  9053. problemToolbar.disableButton(luoguButton);
  9054. }
  9055. }
  9056. }
  9057.  
  9058. /**
  9059. * 跳转到 Virtual Judge
  9060. * @param {ProblemPageLinkbar} problemToolbar
  9061. */
  9062. async function CF2vjudge(problemToolbar) {
  9063. const url = window.location.href;
  9064. const problemId = getProblemId(url);
  9065. const vjudgeButton = problemToolbar.addLinkButton(
  9066. "vjudgeButton",
  9067. "https://vjudge.net/",
  9068. i18next.t('state.loading', { ns: 'button' }),
  9069. $("<img>").attr("src", "https://aowuucdn.oss-accelerate.aliyuncs.com/vjudge.ico")
  9070. );
  9071.  
  9072. const checkLinkExistence = async (url) => {
  9073. return OJB_promiseRetryWrapper(async () => {
  9074. const response = await OJB_GMRequest({
  9075. method: "HEAD",
  9076. url: url,
  9077. });
  9078. if (response.status >= 200 && response.status < 300) return true;
  9079. else if (response.status == 404) return false;
  9080. else throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9081. }, {
  9082. maxRetries: 3,
  9083. retryInterval: 1000
  9084. });
  9085. };
  9086.  
  9087. const VjudgeUrl = `https://vjudge.net/problem/AtCoder-${problemId}`;
  9088. try {
  9089. const result = await checkLinkExistence(VjudgeUrl);
  9090. if (problemId && result) {
  9091. problemToolbar.updateText(vjudgeButton, "VJudge");
  9092. problemToolbar.updateUrl(vjudgeButton, VjudgeUrl);
  9093. } else {
  9094. problemToolbar.updateText(vjudgeButton, i18next.t('state.404', { ns: 'button' }));
  9095. problemToolbar.disableButton(vjudgeButton);
  9096. }
  9097. } catch (error) {
  9098. if (error instanceof OJB_GMError && error.type == "error") {
  9099. problemToolbar.updateText(vjudgeButton, i18next.t('state.netError', { ns: 'button' }));
  9100. problemToolbar.disableButton(vjudgeButton);
  9101. }
  9102. }
  9103. }
  9104.  
  9105. // RatingClass
  9106. const ratingClassMap = {
  9107. NaN: "rating_by_clist_colorNaN",
  9108. 0: "rating_by_clist_color0",
  9109. 400: "rating_by_clist_color1",
  9110. 800: "rating_by_clist_color2",
  9111. 1200: "rating_by_clist_color3",
  9112. 1600: "rating_by_clist_color4",
  9113. 2000: "rating_by_clist_color5",
  9114. 2400: "rating_by_clist_color6",
  9115. 2800: "rating_by_clist_color7",
  9116. 2800: "rating_by_clist_color8",
  9117. 2800: "rating_by_clist_color9"
  9118. };
  9119. const cssMap = {
  9120. "rating_by_clist_colorNaN": "#cccccc",
  9121. "rating_by_clist_color0": "#808080",
  9122. "rating_by_clist_color1": "#804000",
  9123. "rating_by_clist_color2": "#008000",
  9124. "rating_by_clist_color3": "#00c0c0",
  9125. "rating_by_clist_color4": "#0000ff",
  9126. "rating_by_clist_color5": "#c0c000",
  9127. "rating_by_clist_color6": "#fb7e00",
  9128. "rating_by_clist_color7": "#ff0000",
  9129. "rating_by_clist_color8": "#ff0000",
  9130. "rating_by_clist_color9": "#ff0000"
  9131. };
  9132. // TODO 7
  9133. /**
  9134. * clist 访问有效性检查
  9135. * @param {boolean} onlyCookie 是否只检查Cookie
  9136. * @returns {Promise<boolean>} 是否有效
  9137. */
  9138. async function validateClistConnection(onlyCookie = false) {
  9139. const clistApiUrl = "https://clist.by:443/api/v4/contest/?limit=1&resource_id=1";
  9140. const requestOptions = {
  9141. method: "GET",
  9142. url: clistApiUrl,
  9143. timeout: 5000,
  9144. };
  9145.  
  9146. // 尝试发送请求
  9147. async function tryRequest(options) {
  9148. try {
  9149. const response = await OJB_GMRequest(options);
  9150. if (response.status === 200) {
  9151. return { ok: true };
  9152. } else if (response.status === 401) {
  9153. throw new Error('unauthorized');
  9154. } else if (response.status === 404) {
  9155. throw new Error('not_found');
  9156. } else {
  9157. throw new Error('other_error');
  9158. }
  9159. } catch (error) {
  9160. console.warn(`Error accessing clist.by: ${error.message}`);
  9161. return { ok: false, error: error.message };
  9162. }
  9163. }
  9164.  
  9165. // 尝试携带Key发送请求
  9166. let result = await tryRequest(requestOptions);
  9167. if (!onlyCookie && !result.ok) {
  9168. requestOptions.headers = { "Authorization": OJBetter.clist.authorization };
  9169. result = await tryRequest(requestOptions);
  9170. }
  9171.  
  9172. // 根据结果显示错误信息
  9173. if (!result.ok) {
  9174. let errorType = result.error;
  9175. const loadingMessage = new LoadingMessage();
  9176. let state;
  9177. if (errorType === 'not_found') {
  9178. state = i18next.t('error.clist.404', { ns: 'alert' });
  9179. } else if (errorType === 'unauthorized') {
  9180. state = i18next.t('error.clist.cookie', { ns: 'alert' });
  9181. } else {
  9182. state = i18next.t('error.clist.other', { ns: 'alert' });
  9183. }
  9184. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${state}`, 'error');
  9185. }
  9186. return result.ok;
  9187. }
  9188.  
  9189. /**
  9190. * 创建Rating相关css
  9191. * @param {boolean} [hasBorder=true] 是否有边框
  9192. */
  9193. function creatRatingCss(hasBorder = true) {
  9194. const defaultBorderColor = '#dcdfe6';
  9195. let dynamicCss = "";
  9196. let hoverSelector = OJBetter.clist.ratingHidden ? ":hover" : "";
  9197. for (let cssClass in cssMap) {
  9198. dynamicCss += `a.${cssClass}${hoverSelector}, a.${cssClass}${hoverSelector}:link {\n`;
  9199. let borderColor = hasBorder ? cssMap[cssClass] : defaultBorderColor;
  9200. dynamicCss += ` color: ${cssMap[cssClass]} ${OJBetter.clist.ratingHidden ? "!important" : ""};\n`;
  9201. dynamicCss += `}\n`;
  9202. }
  9203. GM_addStyle(dynamicCss);
  9204. if (OJBetter.clist.ratingHidden) {
  9205. GM_addStyle(`
  9206. #clistButton {
  9207. color: #ffffff00;
  9208. }
  9209. `);
  9210. }
  9211. }
  9212.  
  9213. /**
  9214. * 模拟clist网页访问获取rating
  9215. * @param {string} problem 题目名称
  9216. * @param {string} problem_url 题目链接
  9217. * @param {string} contest 比赛名称
  9218. * @returns {Promise<{rating: number, problem: string}>} 题目难度
  9219. */
  9220. async function getRatingFromHTML(problem, problem_url, contest = null) {
  9221. // 去除题目名称中的括号,以及首尾的空白符
  9222. problem = problem.replace(/\([\s\S]*?\)/g, '').trim();
  9223.  
  9224. return OJB_promiseRetryWrapper(async () => {
  9225. const queryString = `search=${encodeURIComponent(problem)}&resource=1`;
  9226. const response = await OJB_GMRequest({
  9227. method: 'GET',
  9228. url: `https://clist.by/problems/?${queryString}`,
  9229. });
  9230.  
  9231. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9232. const html = response.responseText;
  9233. const cleanedHtml = html.replace(/src=(.|\s)*?"/g, '');
  9234. const parser = new DOMParser();
  9235. const doc = parser.parseFromString(cleanedHtml, 'text/html');
  9236. const trs = doc.querySelectorAll('table tbody tr');
  9237.  
  9238. for (let tr of trs) {
  9239. const rating = tr.querySelector('.problem-rating-column').textContent.trim();
  9240. const link = OJB_cleanLink(tr.querySelector('.problem-name-column a:nth-of-type(2)')?.href);
  9241.  
  9242. if (link === problem_url || link === problem_url + '/') {
  9243. return {
  9244. rating: parseInt(rating),
  9245. problem: problem
  9246. };
  9247. } else if (contest !== null) {
  9248. const contestTitles = [...tr.querySelectorAll('.problem-name-column .pull-right a[title], .problem-name-column .pull-right span[title]')].map(el => el.title);
  9249. if (contestTitles.includes(contest)) {
  9250. return {
  9251. rating: parseInt(rating),
  9252. problem: problem
  9253. };
  9254. }
  9255. }
  9256. }
  9257. console.warn(`No data found for the question: ${problem}`);
  9258. }, {
  9259. maxRetries: 3,
  9260. retryInterval: 500
  9261. });
  9262. }
  9263.  
  9264. /**
  9265. * 从clist API获取题目的rating
  9266. * @param {string} problem_name 题目名
  9267. * @param {string} problem_url 题目链接
  9268. * @returns {Promise<number>} 题目rating
  9269. *
  9270. * 使用两个Map对象来存储和快速访问题目信息:
  9271. * - problemsMap: 通过题目的URL作为键来存储题目信息。
  9272. * - nameMap: 通过题目的名称作为键来存储题目信息。
  9273. *
  9274. * 每个题目信息是一个对象,包含以下属性:
  9275. * @typedef {Object} ProblemInfo
  9276. * @property {string} name 题目名称
  9277. * @property {string} url 题目URL
  9278. * @property {number} rating 题目评分,如果没有评分信息则为NaN
  9279. */
  9280. async function getRatingFromApi_problem(problem_name, problem_url) {
  9281. return OJB_promiseRetryWrapper(async () => {
  9282. const response = await OJB_GMRequest({
  9283. method: "GET",
  9284. // url: `https://clist.by:443/api/v4/problem/?name=${encodeURIComponent(problem_name)}&resource__regex=codeforces.com`,
  9285. url: `https://clist.by:443/api/v4/problem/?url__regex=${encodeURIComponent(problem_name)}&resource__regex=atcoder.jp`,
  9286. headers: { "Authorization": OJBetter.clist.authorization }
  9287. });
  9288.  
  9289. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9290. let data = JSON.parse(response.responseText);
  9291. /**
  9292. * 使用题目的URL作为键来存储题目信息。
  9293. * @type {Map<string, ProblemInfo>}
  9294. */
  9295. let problemsMap = new Map();
  9296.  
  9297. /**
  9298. * 使用题目的名称作为键来存储题目信息。
  9299. * @type {Map<string, ProblemInfo>}
  9300. */
  9301. let nameMap = new Map();
  9302.  
  9303. data.objects.forEach(problem => {
  9304. /** @type {ProblemInfo} 题目信息*/
  9305. let problemInfo = {
  9306. name: problem.name,
  9307. url: problem.url,
  9308. rating: problem.rating ? problem.rating : NaN
  9309. };
  9310. problemsMap.set(OJB_cleanLink(problem.url), problemInfo);
  9311. nameMap.set(problem.name, problemInfo);
  9312. });
  9313.  
  9314. if (problemsMap.has(problem_url)) {
  9315. return problemsMap.get(problem_url).rating;
  9316. } else if (nameMap.has(problem_name)) {
  9317. return nameMap.get(problem_name).rating;
  9318. } else {
  9319. console.warn('Problem not found in the response');
  9320. }
  9321. }, {
  9322. maxRetries: 5,
  9323. retryInterval: 1000
  9324. });
  9325. }
  9326.  
  9327. /**
  9328. * 获取字符串中的关键词列表
  9329. * @param {string} text 字符串文本
  9330. * @returns {array<string>} 返回关键词列表
  9331. */
  9332. function getKeywords(text) {
  9333. // 定义要过滤掉的高频词
  9334. const highFrequencyWords = ['Educational', 'Codeforces', 'Round', 'Div'];
  9335.  
  9336. // 使用正则表达式替换掉特殊符号(保留空格以便分词)
  9337. const sanitizedText = text.replace(/[^\w\s]|_/g, '').replace(/\s+/g, ' ');
  9338.  
  9339. // 将字符串拆分为单词数组
  9340. const words = sanitizedText.split(' ');
  9341.  
  9342. // 过滤掉高频词和空字符串
  9343. const filteredWords = words.filter(word => {
  9344. return word && highFrequencyWords.indexOf(word) === -1;
  9345. });
  9346.  
  9347. // 返回关键词列表
  9348. return filteredWords;
  9349. }
  9350.  
  9351. /**
  9352. * 根据关键词从 Clist API 中获取实际比赛名称
  9353. * @param {string} contestName 比赛名
  9354. * @param {string} contestUrl 比赛链接
  9355. * @returns {string|null} 该比赛在Clist中的实际名字
  9356. */
  9357. async function getContestNameFromApi(contestName, contestUrl) {
  9358. return OJB_promiseRetryWrapper(async () => {
  9359. const options = {
  9360. method: "GET",
  9361. // url: `https://clist.by:443/api/v4/contest/?resource_id=1&event__regex=${encodeURIComponent(contestName)}`,
  9362. url: `https://clist.by:443/api/v4/contest/?resource_id=93&event__regex=${encodeURIComponent(contestName)}`,
  9363. headers: {
  9364. "Authorization": OJBetter.clist.authorization
  9365. }
  9366. };
  9367.  
  9368. let response = await OJB_GMRequest(options);
  9369.  
  9370. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9371.  
  9372. let data = JSON.parse(response.responseText);
  9373. let objects = data.objects;
  9374.  
  9375. if (objects.length > 0) {
  9376. for (const contest of objects) {
  9377. // const href = contest.href.replace(/\/contests\//i, '/contest/'); // 链接可能是contests而不是contest,换回来
  9378. const href = contest.href;
  9379. if (OJB_cleanLink(href) == contestUrl) {
  9380. return contest.event;
  9381. }
  9382. }
  9383. }
  9384. return null;
  9385. }, {
  9386. maxRetries: 5,
  9387. retryInterval: 1000
  9388. });
  9389. }
  9390.  
  9391. /**
  9392. * 获取在clist中的实际比赛名称
  9393. * @param {string} contestName 待搜索的比赛名称
  9394. * @param {string} contestUrl 比赛的url
  9395. * @returns {Promise<string|null>} 在clist中的实际比赛名称,如果没有找到,则返回null
  9396. */
  9397. async function getActualContestName(contestName, contestUrl) {
  9398. // 首先尝试使用完整的比赛名称进行搜索
  9399. let actualContestName = await getContestNameFromApi(contestName, contestUrl);
  9400. if (actualContestName) {
  9401. return actualContestName;
  9402. }
  9403.  
  9404. // 如果使用完整名称没有找到,则尝试使用关键词进行搜索
  9405. const keywords = getKeywords(contestName);
  9406. const maxKeywordAttempts = 1; // 最多尝试到第几个关键词(因为Clist API有频率限制)
  9407. for (let i = 0; i < Math.min(keywords.length, maxKeywordAttempts); i++) {
  9408. actualContestName = await getContestNameFromApi(keywords[i], contestUrl);
  9409. if (actualContestName) {
  9410. return actualContestName;
  9411. }
  9412. }
  9413.  
  9414. // 如果全部尝试后仍没有找到,返回null
  9415. return null;
  9416. }
  9417.  
  9418. /**
  9419. * 从clist API获取比赛题目集的rating
  9420. * @param {string} contestName 比赛名
  9421. * @returns {Promise<Map<string, number>>} 题目rating
  9422. */
  9423. async function getRatingFromApi_contest(contestName, contestUrl) {
  9424. const actualContestName = await getActualContestName(contestName, contestUrl);
  9425. return OJB_promiseRetryWrapper(async () => {
  9426. const options = {
  9427. method: "GET",
  9428. url: `https://clist.by:443/api/v4/contest/?resource_id=93&with_problems=true&event=${encodeURIComponent(actualContestName)}`,
  9429. headers: {
  9430. "Authorization": OJBetter.clist.authorization
  9431. }
  9432. };
  9433.  
  9434. let response = await OJB_GMRequest(options);
  9435.  
  9436. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9437.  
  9438. let data = JSON.parse(response.responseText);
  9439. let objects = data.objects;
  9440. let problemsMap = new Map();
  9441.  
  9442. if (objects.length > 0 && objects[0].problems) {
  9443. objects[0].problems.forEach(problem => {
  9444. problemsMap.set(OJB_cleanLink(problem.url), problem.rating ? problem.rating : NaN);
  9445. });
  9446. }
  9447.  
  9448. return problemsMap;
  9449. }, {
  9450. maxRetries: 5,
  9451. retryInterval: 1000
  9452. });
  9453. }
  9454.  
  9455. /**
  9456. * 根据rating获取对应的颜色class名
  9457. * @param {number} rating 题目rating
  9458. * @returns {string} 颜色class名
  9459. */
  9460. function getClassNameByRating(rating) {
  9461. let className = "rating_by_clist_color9";
  9462. if (Number.isNaN(rating)) {
  9463. className = "rating_by_clist_colorNaN";
  9464. } else {
  9465. let keys = Object.keys(ratingClassMap);
  9466. for (let i = 0; i < keys.length; i++) {
  9467. if (rating < keys[i]) {
  9468. className = ratingClassMap[keys[i - 1]];
  9469. break;
  9470. }
  9471. }
  9472. }
  9473. return className;
  9474. }
  9475.  
  9476. /**
  9477. * problem题目页显示Rating
  9478. * @param {ProblemPageLinkbar} problemToolbar
  9479. * @returns {Promise<void>}
  9480. */
  9481. async function showRatingByClist_problem(problemToolbar) {
  9482. // 题目名
  9483. // const problem = $('.header .title').eq(0).text().replace(/[\s\S]*?. /, '');
  9484. // if (OJBetter.typeOfPage.is_acmsguru) problem = $('h4').eq(0).text().replace(/[\s\S]*?. /, '');
  9485. const url = window.location.href;
  9486. const problemId = getProblemId(url);
  9487.  
  9488. // 创建Rating按钮元素
  9489. creatRatingCss(false);
  9490. // TODO
  9491. // const clistButton = problemToolbar.addLinkButton(
  9492. // 'clistButton',
  9493. // `https://clist.by/problems/?search=${problem}&resource=1`,
  9494. // i18next.t('state.wait', { ns: 'button' }),
  9495. // $("<img>").attr("src", "https://clist.by/static/img/logo-48.png"),
  9496. // "15px"
  9497. // );
  9498. const clistButton = problemToolbar.addLinkButton(
  9499. 'clistButton',
  9500. `https://clist.by/problems/?search=${problemId}&resource=93`,
  9501. i18next.t('state.wait', { ns: 'button' }),
  9502. $("<img>").attr("src", "https://clist.by/static/img/logo-48.png"),
  9503. "15px"
  9504. );
  9505.  
  9506. // 检测clist连接
  9507. if (!await validateClistConnection()) {
  9508. problemToolbar.updateText(clistButton, i18next.t('state.netError', { ns: 'button' }));
  9509. return;
  9510. }
  9511.  
  9512. // 题目链接
  9513. let problem_url = window.location.href;
  9514. if (problem_url.includes('/contest/')) {
  9515. problem_url = problem_url.replace(/\/contest\/(\d+)\/problem\/(\w+)[^\w]*/, '/contest/$1/problem/$2');
  9516. } else {
  9517. problem_url = problem_url.replace(/\/problemset\/problem\/(\d+)\/(\w+)/, '/contest/$1/problem/$2');
  9518. }
  9519. if (OJBetter.typeOfPage.is_mSite) problem_url = problem_url.replace(/\/\/(\w+).codeforces.com/, '//codeforces.com'); // 轻量站
  9520.  
  9521. // 比赛名
  9522. // let contest = $('#sidebar').children().first().find('.rtable th').first().text();
  9523.  
  9524. // rating
  9525. problemToolbar.updateText(clistButton, i18next.t('state.loading', { ns: 'button' }));
  9526. let rating = await getRatingFromApi_problem(problemId, problem_url);
  9527. if (rating) {
  9528. let className = getClassNameByRating(rating);
  9529. problemToolbar.updateText(clistButton, rating);
  9530. problemToolbar.setBold(clistButton);
  9531. problemToolbar.addClass(clistButton, className);
  9532. } else {
  9533. problemToolbar.updateText(clistButton, i18next.t('state.404', { ns: 'button' }));
  9534. problemToolbar.disableButton(clistButton);
  9535. }
  9536. }
  9537.  
  9538. /**
  9539. * contest页显示Rating
  9540. * @returns {Promise<void>}
  9541. */
  9542. async function showRatingByClist_contest() {
  9543. // 创建Rating显示框
  9544. creatRatingCss();
  9545. let ratingBadges = {};
  9546. // $('.datatable .id.left').each(function () {
  9547. // let href = 'https://codeforces.com' + $(this).find('a').attr('href');
  9548. // let badge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge">${i18next.t('state.wait', { ns: 'button' })}</a>`);
  9549. // $(this).find('a').after(badge);
  9550. // ratingBadges[href] = badge;
  9551. // });
  9552. $('table tbody tr').each(function () {
  9553. let href = 'https://atcoder.jp' + $(this).find('a').attr('href');
  9554. let badge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge">${i18next.t('state.wait', { ns: 'button' })}</a>`);
  9555. $(this).find('a:first').after(badge);
  9556. ratingBadges[href] = badge;
  9557. });
  9558.  
  9559. // 检测clist连接
  9560. if (!await validateClistConnection()) {
  9561. for (let href in ratingBadges) {
  9562. ratingBadges[href].text('error').addClass('ratingBadge_error');
  9563. }
  9564. return;
  9565. }
  9566.  
  9567. // 显示loading
  9568. for (let href in ratingBadges) {
  9569. ratingBadges[href].text(i18next.t('state.loading', { ns: 'button' })).addClass('ratingBadge_loading');
  9570. }
  9571.  
  9572. // 获取Rating
  9573. // let contestName = $('#sidebar').children().first().find('.rtable th').first().text();
  9574. let contestName = window.location.href.match(/\/contests\/[^\/]*?(\d+)\/tasks/)?.[1];
  9575. // let contestUrl = OJB_cleanLink(window.location.href);
  9576. let contestUrl = OJB_cleanLink(window.location.href.replace(/\/tasks\/?.*$/, ''));
  9577. try {
  9578. let problemsMap = await getRatingFromApi_contest(contestName, contestUrl);
  9579.  
  9580. // 填充数据
  9581. for (let href in ratingBadges) {
  9582. if (problemsMap.has(href)) {
  9583. let rating = problemsMap.get(href);
  9584. let className = getClassNameByRating(rating);
  9585. ratingBadges[href].text(rating).addClass(className);
  9586. } else {
  9587. ratingBadges[href].text(i18next.t('state.404', { ns: 'button' })).addClass('ratingBadge_no');
  9588. }
  9589. }
  9590. } catch (error) {
  9591. // 填充数据
  9592. for (let href in ratingBadges) {
  9593. ratingBadges[href].text(i18next.t('state.netError', { ns: 'button' })).addClass('ratingBadge_no');
  9594. }
  9595. console.warn(error);
  9596. }
  9597. }
  9598.  
  9599. /**
  9600. * problemset页显示Rating
  9601. * @returns {Promise<void>}
  9602. */
  9603. async function showRatingByClist_problemset() {
  9604. creatRatingCss();
  9605. let ratingBadges = [];
  9606. const $problems = $('.problems');
  9607. const $trs = $problems.find('tbody tr:gt(0)');
  9608.  
  9609. // 先创建Rating显示框,并将关系存进数组ratingBadges
  9610. for (let i = 0; i < $trs.length; i++) {
  9611. const $tds = $($trs[i]).find('td');
  9612. const $firstDiv = $($tds[1]).find('div:first');
  9613. let problem = $firstDiv.text();
  9614. let problem_url = $firstDiv.find('a').attr('href');
  9615. problem_url = problem_url.replace(/^\/problemset\/problem\/(\d+)\/(\w+)/, 'https://codeforces.com/contest/$1/problem/$2');
  9616.  
  9617. const ratingBadge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge"></a>`);
  9618. const rating = OJB_safeCreateJQElement(`<span class="rating">${i18next.t('state.wait', { ns: 'button' })}</span>`);
  9619. ratingBadge.append(rating);
  9620. $($tds[0]).find('a').after(ratingBadge);
  9621. ratingBadges.push({ ratingBadge, rating, problem, problem_url });
  9622. }
  9623.  
  9624. // 检测clist连接
  9625. if (!await validateClistConnection()) {
  9626. for (let i = 0; i < rating.length; i++) {
  9627. ratingBadges[i].rating.text(i18next.t('state.netError', { ns: 'button' }));
  9628. }
  9629. return;
  9630. }
  9631.  
  9632. // 每次只获取3个rating
  9633. for (let i = 0; i < ratingBadges.length; i += 3) {
  9634. const promises = [];
  9635. const endIndex = Math.min(i + 3, ratingBadges.length);
  9636.  
  9637. for (let j = i; j < endIndex; j++) {
  9638. const ratingBadge = ratingBadges[j];
  9639. // 显示请求中
  9640. ratingBadge.rating.text(i18next.t('state.loading', { ns: 'button' }));
  9641. promises.push(getRatingFromHTML(ratingBadge.problem, ratingBadge.problem_url).catch(error => console.warn(error)));
  9642. }
  9643.  
  9644. const results = await Promise.all(promises);
  9645.  
  9646. for (let j = i; j < endIndex; j++) {
  9647. const result = results[j - i];
  9648. const ratingBadge = ratingBadges[j];
  9649. if (result) {
  9650. let className = getClassNameByRating(result.rating);
  9651. ratingBadge.ratingBadge.addClass(className);
  9652. ratingBadge.rating.text(result.rating);
  9653. } else {
  9654. ratingBadge.rating.text(i18next.t('state.404', { ns: 'button' }));
  9655. }
  9656. }
  9657. }
  9658. }
  9659.  
  9660. /**
  9661. * 存放编辑器语言select的值与Monaco语言对应关系的map.
  9662. * @type {Object.<string, string>}
  9663. */
  9664. // const value_monacoLanguageMap = {
  9665. // "4": "pascal", "6": "php", "7": "python", "9": "csharp", "13": "perl", "20": "scala", "31": "python",
  9666. // "32": "go", "34": "javascript", "36": "java", "40": "python", "41": "python", "43": "cpp",
  9667. // "50": "cpp", "51": "pascal", "52": "cpp", "54": "cpp", "55": "javascript", "59": "cpp", "60": "java",
  9668. // "61": "cpp", "65": "csharp", "67": "ruby", "70": "python", "73": "cpp", "74": "java", "75": "rust",
  9669. // "77": "kotlin", "79": "csharp", "80": "cpp", "83": "kotlin", "87": "java"
  9670. // };
  9671. const value_monacoLanguageMap = {
  9672. "5001": "cpp", "5002": "go", "5003": "csharp", "5004": "kotlin", "5005": "java",
  9673. "5006": "nim", "5007": "text", "5008": "text", "5009": "javascript", "5010": "javascript",
  9674. "5011": "r", "5012": "d", "5013": "d", "5014": "swift", "5015": "dart",
  9675. "5016": "php", "5017": "cpp", "5018": "ruby", "5019": "crystal", "5020": "text",
  9676. "5021": "fsharp", "5022": "julia", "5023": "sh", "5024": "text", "5025": "haskell",
  9677. "5026": "fortran", "5027": "lua", "5028": "cpp", "5029": "lisp", "5030": "cobol",
  9678. "5031": "cpp", "5032": "sh", "5033": "python", "5034": "sh", "5035": "text",
  9679. "5036": "text", "5037": "perl", "5038": "sh", "5039": "text", "5040": "text",
  9680. "5041": "pascal", "5042": "csharp", "5043": "lua", "5044": "prolog", "5045": "sh",
  9681. "5046": "scheme", "5047": "scala", "5048": "vbscript", "5049": "text", "5050": "clojure",
  9682. "5051": "erlang", "5052": "typescript", "5053": "cpp", "5054": "rust", "5055": "python",
  9683. "5056": "scala", "5057": "text", "5058": "typescript", "5059": "ocaml", "5060": "raku",
  9684. "5061": "text", "5062": "lisp", "5063": "python", "5064": "clojure", "5065": "text",
  9685. "5066": "text", "5067": "text", "5068": "ada", "5069": "text", "5070": "text",
  9686. "5071": "clojure", "5072": "cpp", "5073": "cpp", "5074": "text", "5075": "lisp",
  9687. "5076": "text", "5077": "d", "5078": "python", "5079": "text", "5080": "text",
  9688. "5081": "ocaml", "5082": "python", "5083": "matlab", "5084": "haxe", "5085": "elixir",
  9689. "5086": "text", "5087": "text", "5088": "lisp", "5089": "text", "5090": "cobol"
  9690. };
  9691.  
  9692. /**
  9693. * 更新代码提交页的HTML
  9694. * @param {string} submitUrl 提交页面的URL
  9695. * @param {string} cacheKey 本地缓存的键名
  9696. * @returns {Promise<jQuery<HTMLElement>>} 返回 jQuery 包装的 HTML 元素
  9697. */
  9698. async function CloneOriginalHTML(submitUrl, cacheKey) {
  9699. return OJB_promiseRetryWrapper(async () => {
  9700. const response = await OJB_GMRequest({
  9701. method: 'GET',
  9702. url: submitUrl
  9703. });
  9704. const html = response.responseText;
  9705. const parser = new DOMParser();
  9706. const doc = parser.parseFromString(html, 'text/html');
  9707. const cloneHTML = $(doc.body).html();
  9708. localStorage.setItem(cacheKey, html);
  9709. return $(cloneHTML);
  9710. }, {
  9711. maxRetries: 5,
  9712. retryInterval: 1000,
  9713. errorHandler: (err) => {
  9714. console.error('A network error occurred while retrieving the HTML for the code submission page.', submitUrl);
  9715. }
  9716. });
  9717. }
  9718.  
  9719. /**
  9720. * 获取代码提交页的HTML元素
  9721. * @param {string} submitUrl
  9722. * @returns {Promise<jQuery>}
  9723. */
  9724. async function getSubmitHTML(submitUrl) {
  9725. const cacheKey = 'OJBetter_CloneOriginalHTML';
  9726. const cookieKey = 'OJBetter_CloneOriginalHTML_time';
  9727. if (OJB_getCookie(cookieKey) === '1') {
  9728. // 存在缓存
  9729. CloneOriginalHTML(submitUrl, cacheKey);
  9730. // 校验
  9731. let cloneHTML = $(localStorage.getItem(cacheKey));
  9732. if (cloneHTML.find('form.submit-form').length > 0) {
  9733. return cloneHTML;
  9734. } else {
  9735. // 存在错误,更新缓存
  9736. console.warn(`Cache error detected!\nattempting to update, cache destination submitUrl:\n${submitUrl}`);
  9737. return await CloneOriginalHTML(submitUrl, cacheKey);
  9738. }
  9739.  
  9740. } else {
  9741. // 没有缓存,更新
  9742. document.cookie = `${cookieKey}=1; path=/`;
  9743. return await CloneOriginalHTML(submitUrl, cacheKey);
  9744. }
  9745. }
  9746.  
  9747. // 代码自动保存
  9748. async function saveCode(url, code) {
  9749. try {
  9750. await OJBetter.common.database.editorCode.put({ url, code });
  9751. return 'Code saved successfully';
  9752. } catch (error) {
  9753. throw new Error('Failed to save code');
  9754. }
  9755. }
  9756.  
  9757. async function getCode(url) {
  9758. try {
  9759. const result = await OJBetter.common.database.editorCode.get(url);
  9760. return result ? result.code : null;
  9761. } catch (error) {
  9762. throw new Error('Failed to get code');
  9763. }
  9764. }
  9765.  
  9766. // 创建代码编辑调试表单元素
  9767. // async function createCodeEditorForm(submitUrl, cloneHTML) {
  9768. async function createCodeEditorForm(submitUrl) {
  9769. // 表单
  9770. let formDiv = $('<form method="post" id="OJBetter_SubmitForm" class="input-output-copier"></form>');
  9771. // $('.ttypography').after(formDiv);
  9772. $('#task-statement').after(formDiv);
  9773. // formDiv.attr('action', submitUrl + "?csrf_token=" + OJBetter.common.at_csrf_token);
  9774. formDiv.attr('action', submitUrl);
  9775. formDiv.attr('method', 'POST');
  9776.  
  9777. // 顶部区域
  9778. let topDiv = OJB_safeCreateJQElement(`<div class="topDiv"></div>`);
  9779. let selectLang = $('#select-lang').clone(); // 语言选择
  9780. // selectLang.css({ 'margin': '10px 0px' }).attr('id', 'programTypeId');
  9781. topDiv.append(selectLang);
  9782. let topRightDiv = OJB_safeCreateJQElement(`<div class="topRightDiv"></div>`);
  9783. topDiv.append(topRightDiv);
  9784. formDiv.append(topDiv);
  9785.  
  9786. // 问题选择/编号
  9787. // let selectProblem = $('<input name="submittedProblemIndex" style="display:none;"></input>');
  9788. // let problemCode;
  9789. // if (OJBetter.typeOfPage.is_acmsguru) {
  9790. // problemCode = $('h4').eq(0).text();
  9791. // let matchResult = problemCode.match(/([A-Z0-9]+)/);
  9792. // problemCode = matchResult[0];
  9793. // } else if (OJBetter.typeOfPage.is_problemset_problem) {
  9794. // let match = window.location.href.match(/\/problem\/([0-9]+?)\/([A-Za-z0-9]+?)(?!=[A-Za-z0-9])/);
  9795. // problemCode = match[1] + match[2];
  9796. // selectProblem.attr('name', 'submittedProblemCode');
  9797. // } else {
  9798. // problemCode = $('.header .title').eq(0).text();
  9799. // let matchResult = problemCode.match(/([A-Z0-9]+)/);
  9800. // problemCode = matchResult[0];
  9801. // }
  9802. // selectProblem.val(problemCode);
  9803. let selectProblem = $('input[name="data.TaskScreenName"]').clone();
  9804. formDiv.append(selectProblem);
  9805.  
  9806. // 隐藏的代码记录
  9807. // let sourceDiv = $('<textarea id="sourceCodeTextarea" name="source" style="display: none;"></textarea>');
  9808. let sourceDiv = $('<textarea id="plain-textarea" name="sourceCode" style="display: none;"></textarea>');
  9809. formDiv.append(sourceDiv);
  9810.  
  9811. // 隐藏的crsf token
  9812. let csrfDiv = $(`<input type="hidden" name="csrf_token" value=${OJBetter.common.at_csrf_token}>`);
  9813. formDiv.append(csrfDiv);
  9814.  
  9815. // 代码编辑器
  9816. let editorDiv = $('<div id="OJBetter_editor"></div>');
  9817. formDiv.append(editorDiv);
  9818.  
  9819. // monaco
  9820. let monaco = $('<div id="OJBetter_monaco"></div>');
  9821. editorDiv.append(monaco);
  9822.  
  9823. // 自定义调试
  9824. let customTestDiv = OJB_safeCreateJQElement(`
  9825. <details id="customTestBlock">
  9826. <summary >${i18next.t('customTestBlock.title', { ns: 'codeEditor' })}</summary>
  9827. <div id="customTests" style="min-height: 30px;"></div>
  9828. <div id="control" style="display:flex;">
  9829. <div style="display: flex;margin: 5px;">
  9830. <input type="checkbox" id="onlyCustomTest"}><label for="onlyCustomTest">
  9831. ${i18next.t('customTestBlock.onlyCustom', { ns: 'codeEditor' })}
  9832. </label>
  9833. </div>
  9834. <div style="display: flex;margin: 5px;">
  9835. <input type="checkbox" id="DontShowDiff"}>
  9836. <label for="DontShowDiff">
  9837. ${i18next.t('customTestBlock.DontShowDiff', { ns: 'codeEditor' })}
  9838. </label>
  9839. </div>
  9840. <button type="button" id="addCustomTest">${i18next.t('customTestBlock.add', { ns: 'codeEditor' })}</button>
  9841. </div>
  9842. </details>
  9843. `)
  9844. formDiv.append(customTestDiv);
  9845.  
  9846. // 调试/提交
  9847. let submitDiv = $('<div id="OJBetter_submitDiv"></div>');
  9848. let CompilerArgsInput = $('<input type="text" id="CompilerArgsInput">');
  9849. submitDiv.append(CompilerArgsInput);
  9850.  
  9851. let runButton = OJB_safeCreateJQElement(`
  9852. <button type="button" id="RunTestButton" class="ojb_btn ojb_btn_popover top">
  9853. <i class="iconfont">&#xe6c1;</i>
  9854. <span class="popover_content">${i18next.t('runTestButton.initial', { ns: 'codeEditor' })}</span>
  9855. </button>
  9856. `);
  9857. let submitButton = OJB_safeCreateJQElement(`
  9858. <button id="SubmitButton" class="ojb_btn ojb_btn_popover top" type="submit">
  9859. <i class="iconfont">&#xe633;</i>
  9860. <span class="popover_content">${i18next.t('submitButton', { ns: 'codeEditor' })}</span>
  9861. </button>
  9862. `);
  9863. if (OJBetter.monaco.setting.submitButtonPosition == "bottom") {
  9864. // 添加测试/提交按钮到底部
  9865. submitDiv.append(runButton);
  9866. submitDiv.append(submitButton);
  9867. }
  9868.  
  9869. formDiv.append(submitDiv);
  9870. let CompilerSetting = OJB_safeCreateJQElement(`
  9871. <div id="CompilerSetting"></div>
  9872. `);
  9873. formDiv.append(CompilerSetting);
  9874. let statePanel = OJB_safeCreateJQElement(`
  9875. <div id="statePanel"></div>
  9876. `);
  9877. formDiv.append(statePanel);
  9878.  
  9879. //==================================
  9880. // 去除原有的编辑器
  9881. //==================================
  9882. $('.form-code-submit').remove();
  9883.  
  9884. let from = {
  9885. formDiv: formDiv,
  9886. selectLang: selectLang.find('select:first'),
  9887. topRightDiv: topRightDiv,
  9888. sourceDiv: sourceDiv,
  9889. editorDiv: editorDiv,
  9890. monaco: monaco,
  9891. runButton: runButton,
  9892. submitButton: submitButton,
  9893. submitDiv: submitDiv,
  9894. CompilerSetting: CompilerSetting,
  9895. statePanel: statePanel
  9896. };
  9897. return from;
  9898. }
  9899.  
  9900. // 解析ace格式的补全规则(acwing)
  9901. function parseAceCompleter(rules, range) {
  9902. const suggestions = [];
  9903. if (rules && rules.templates && rules.templates.items) {
  9904. const items = rules.templates.items;
  9905. for (let i = 0; i < items.length; i++) {
  9906. const item = items[i];
  9907. const parts = item.caption.split(' ');
  9908. for (let i = 0; i < parts.length; i++) {
  9909. if (item.value.startsWith(parts[i])) {
  9910. item.value = item.value.replace(parts[i], parts.slice(0, i + 1).join(' '));
  9911. break;
  9912. }
  9913. }
  9914. const completionItem = {
  9915. label: item.caption,
  9916. kind: monaco.languages.CompletionItemKind.Function,
  9917. insertText: item.value,
  9918. range: range
  9919. };
  9920. suggestions.push(completionItem);
  9921. }
  9922. }
  9923. return { suggestions };
  9924. }
  9925.  
  9926. // 解析monaco格式的补全规则
  9927. function parseMonacoCompleter(rules, range) {
  9928. const suggestions_ = [];
  9929. if (rules && rules.suggestions) {
  9930. const suggestion = rules.suggestions;
  9931. for (let i = 0; i < rules.suggestions.length; i++) {
  9932. const item = suggestion[i];
  9933. const completionItem = {
  9934. ...item,
  9935. range: range
  9936. };
  9937. suggestions_.push(completionItem);
  9938. }
  9939. }
  9940. return { suggestions: suggestions_ };
  9941. }
  9942.  
  9943. /**
  9944. * 创建monaco编辑器的一个实例
  9945. */
  9946. async function createMonacoEditor(language, form, support) {
  9947. // 判断monacoLoader是否加载完毕
  9948. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  9949.  
  9950. /**
  9951. * 通用参数
  9952. */
  9953. var id = 0; // 协议中的id标识
  9954. var workspace = language + "_workspace";
  9955. var rootUri = OJBetter.monaco.lsp.workUri + "/" + workspace;
  9956. // 文件名
  9957. var InstanceID = OJB_getRandomNumber(8).toString();
  9958. var filename = language == "java" ? "hello/src/" + InstanceID : InstanceID;
  9959. // 后缀名
  9960. var fileExtension =
  9961. language === "cpp"
  9962. ? ".cpp"
  9963. : language === "python"
  9964. ? ".py"
  9965. : language === "java"
  9966. ? ".java"
  9967. : "";
  9968. var uri = rootUri + "/" + filename + fileExtension;
  9969. var initialized = false; // 是否已初始化
  9970. var serverInfo; // 服务器返回的支持信息
  9971. var model; // model
  9972. var OJBetter_monaco = {};
  9973. window.OJBetter_monaco = OJBetter_monaco; // 全局方法
  9974.  
  9975. /**
  9976. * 一些工具函数
  9977. */
  9978. // 将lsp格式的rang转换为Monaco格式
  9979. OJBetter_monaco.lspRangeToMonacoRange = function (range) {
  9980. const { start, end } = range;
  9981. return new monaco.Range(
  9982. start.line + 1,
  9983. start.character + 1,
  9984. end.line + 1,
  9985. end.character + 1
  9986. );
  9987. };
  9988. // 将Monaco格式的rang转为lsp格式
  9989. OJBetter_monaco.MonacoRangeTolspRange = function (range) {
  9990. return {
  9991. start: {
  9992. line: range.startLineNumber - 1,
  9993. character: range.startColumn - 1,
  9994. },
  9995. end: {
  9996. line: range.endLineNumber - 1,
  9997. character: range.endColumn - 1,
  9998. },
  9999. };
  10000. };
  10001. // 将Monaco格式的position转为lsp格式的
  10002. OJBetter_monaco.MonacoPositionTolspPosition = function (position) {
  10003. return {
  10004. line: position.lineNumber - 1,
  10005. character: position.column - 1,
  10006. };
  10007. };
  10008. // 将Monaco格式的severity转为lsp格式的
  10009. OJBetter_monaco.MonacoSeverityTolspSeverity = function (severity) {
  10010. switch (severity) {
  10011. case 8:
  10012. return 1;
  10013. case 1:
  10014. return 4;
  10015. case 2:
  10016. return 3;
  10017. case 4:
  10018. return 2;
  10019. default:
  10020. return severity;
  10021. }
  10022. };
  10023. // 将lsp格式的severity转为Monaco格式的
  10024. OJBetter_monaco.lspSeverityToMonacoSeverity = function (severity) {
  10025. switch (severity) {
  10026. case 1:
  10027. return 8;
  10028. case 4:
  10029. return 1;
  10030. case 3:
  10031. return 2;
  10032. case 2:
  10033. return 4;
  10034. default:
  10035. return severity;
  10036. }
  10037. };
  10038. // 收集Monaco数据中的rang数据
  10039. OJBetter_monaco.CollectRange = function (item) {
  10040. return {
  10041. startLineNumber: item.startLineNumber,
  10042. startColumn: item.startColumn,
  10043. endLineNumber: item.endLineNumber,
  10044. endColumn: item.endColumn,
  10045. };
  10046. };
  10047. // 收集Monaco position数据中的rang数据
  10048. OJBetter_monaco.CollectRangeByPosition = function (item) {
  10049. var word = model.getWordUntilPosition(item);
  10050. return {
  10051. startLineNumber: item.lineNumber,
  10052. endLineNumber: item.lineNumber,
  10053. startColumn: word.startColumn,
  10054. endColumn: word.endColumn,
  10055. };
  10056. };
  10057. // 将lsp格式的Edit转换为Monaco格式
  10058. OJBetter_monaco.lspEditToMonacoEdit = function (edit) {
  10059. const edits = [];
  10060.  
  10061. if (language == "python") {
  10062. for (const item1 of edit.documentChanges) {
  10063. for (const item2 of item1.edits) {
  10064. const newElement = {
  10065. textEdit: {
  10066. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10067. text: item2.newText,
  10068. },
  10069. resource: monaco.Uri.parse(item1.textDocument.uri),
  10070. versionId: model.getVersionId(),
  10071. };
  10072. edits.push(newElement);
  10073. }
  10074. }
  10075. } else if (language == "java") {
  10076. for (const item1 in edit.changes) {
  10077. edit.changes[item1].forEach((item2) => {
  10078. const newElement = {
  10079. textEdit: {
  10080. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10081. text: item2.newText,
  10082. },
  10083. resource: uri,
  10084. versionId: model.getVersionId(),
  10085. };
  10086. edits.push(newElement);
  10087. });
  10088. }
  10089. } else {
  10090. for (const key in edit.changes) {
  10091. const arr = edit.changes[key];
  10092. for (const item of arr) {
  10093. const newElement = {
  10094. textEdit: {
  10095. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  10096. text: item.newText,
  10097. },
  10098. resource: monaco.Uri.parse(key),
  10099. versionId: model.getVersionId(),
  10100. };
  10101. edits.push(newElement);
  10102. }
  10103. }
  10104. }
  10105. return { edits: edits };
  10106. };
  10107.  
  10108. /**
  10109. * 实例化一个editor
  10110. */
  10111. uri = monaco.Uri.file(uri);
  10112. model = monaco.editor.createModel('', language, uri);
  10113. OJBetter.monaco.editor = monaco.editor.create(document.getElementById("OJBetter_monaco"), {
  10114. model: model,
  10115. rootUri: rootUri,
  10116. fontSize: 15,
  10117. tabSize: 4,
  10118. theme: OJBetter.basic.darkMode == "dark" ? "vs-dark" : "vs",
  10119. bracketPairColorization: {
  10120. enabled: true,
  10121. independentColorPoolPerBracketType: true,
  10122. },
  10123. automaticLayout: true,
  10124. lineNumbersMinChars: 3,
  10125. matchOnWordStartOnly: false,
  10126. wordWrap: "on",
  10127. wrappingIndent: "same",
  10128. glyphMargin: true,
  10129. formatOnType: true,
  10130. scrollbar: {
  10131. verticalScrollbarSize: 10,
  10132. horizontalScrollbarSize: 10,
  10133. alwaysConsumeMouseWheel: OJBetter.monaco.setting.alwaysConsumeMouseWheel
  10134. },
  10135. suggest: {
  10136. selectionMode: 'never' // 代码建议不自动选择
  10137. }
  10138. });
  10139.  
  10140. /**
  10141. * 添加快捷功能
  10142. */
  10143. (OJBetter_monaco.addShortCuts = async () => {
  10144. // 从配置信息更新字体大小
  10145. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(OJBetter.monaco.setting.fontsize) });
  10146.  
  10147. // 调整字体大小
  10148. let changeSize = OJB_safeCreateJQElement(`
  10149. <div class="ojb_btn ojb_btn_popover top">
  10150. <input type="number" id="fontSizeInput" value="${OJBetter.monaco.setting.fontsize}">
  10151. <span class="popover_content">${i18next.t('fontSizeInput', { ns: 'codeEditor' })}</span>
  10152. </div>`)
  10153. form.topRightDiv.append(changeSize);
  10154. changeSize.find('input#fontSizeInput').on('input', function () {
  10155. var size = $(this).val();
  10156. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(size) });
  10157. GM_setValue('editorFontSize', size);
  10158. });
  10159.  
  10160. // 全屏按钮
  10161. let fullscreenButton = OJB_safeCreateJQElement(`
  10162. <button type="button" class="ojb_btn ojb_btn_popover top">
  10163. <i class="iconfont">&#xe606;</i>
  10164. <span class="popover_content">${i18next.t('fullscreenButton', { ns: 'codeEditor' })}</span>
  10165. </button>
  10166. `);
  10167. form.topRightDiv.append(fullscreenButton);
  10168. fullscreenButton.on('click', enterFullscreen);
  10169.  
  10170. // 固定到底部按钮
  10171. let fixToBottomButton = OJB_safeCreateJQElement(`
  10172. <button type="button" class="ojb_btn ojb_btn_popover top">
  10173. <i class="iconfont">&#xe607;</i>
  10174. <span class="popover_content">${i18next.t('fixToBottomButton', { ns: 'codeEditor' })}</span>
  10175. </button>
  10176. `);
  10177. form.topRightDiv.append(fixToBottomButton);
  10178. fixToBottomButton.on('click', fixToBottom);
  10179.  
  10180. // 固定到右侧按钮
  10181. let fixToRightButton = OJB_safeCreateJQElement(`
  10182. <button type="button" class="ojb_btn ojb_btn_popover top">
  10183. <i class="iconfont">&#xe605;</i>
  10184. <span class="popover_content">${i18next.t('fixToRightButton', { ns: 'codeEditor' })}</span>
  10185. </button>
  10186. `);
  10187. // form.topRightDiv.append(fixToRightButton);
  10188. fixToRightButton.on('click', fixToRight);
  10189.  
  10190. // 添加测试/提交按钮到顶部
  10191. if (OJBetter.monaco.setting.submitButtonPosition == "top") {
  10192. form.topRightDiv.append(form.runButton);
  10193. form.topRightDiv.append(form.submitButton);
  10194. }
  10195.  
  10196. // 选择记忆
  10197. if (!OJBetter.monaco.setting.position_initialized) {
  10198. OJBetter.monaco.setting.position_initialized = true; // 标记是否已经初始化过
  10199. if (OJBetter.monaco.setting.position == "full") {
  10200. fullscreenButton.click();
  10201. } else if (OJBetter.monaco.setting.position == "bottom") {
  10202. fixToBottomButton.click();
  10203. } else if (OJBetter.monaco.setting.position == "right") {
  10204. fixToRightButton.click();
  10205. }
  10206. }
  10207.  
  10208. // 禁用按钮
  10209. function disableButtons() {
  10210. fullscreenButton.prop("disabled", true);
  10211. fixToBottomButton.prop("disabled", true);
  10212. fixToRightButton.prop("disabled", true);
  10213. }
  10214.  
  10215. // 启用按钮
  10216. function enableButtons() {
  10217. fullscreenButton.prop("disabled", false);
  10218. fixToBottomButton.prop("disabled", false);
  10219. fixToRightButton.prop("disabled", false);
  10220. }
  10221.  
  10222. // 进入全屏
  10223. function enterFullscreen() {
  10224. let editor = $('#OJBetter_editor');
  10225. editor.addClass('fullscreen');
  10226.  
  10227. // 取消按钮
  10228. let cancelButton = OJB_safeCreateJQElement(`
  10229. <button type="button" class="ojb_btn ojb_btn_popover top primary exit_button_bottom">
  10230. <i class="iconfont">&#xe60b;</i>
  10231. <span class="popover_content">${i18next.t('exitFullscreenButton', { ns: 'codeEditor' })}</span>
  10232. </button>
  10233. `).on('click', () => exitFullscreen(cancelButton));
  10234. $('body').append(cancelButton);
  10235.  
  10236. disableButtons();
  10237. GM_setValue("monacoEditor_position", "full");
  10238. }
  10239.  
  10240. // 退出全屏
  10241. const exitFullscreen = (cancelButton) => {
  10242. let editor = $('#OJBetter_editor');
  10243. editor.removeClass('fullscreen');
  10244. cancelButton.remove();
  10245. enableButtons();
  10246. GM_setValue("monacoEditor_position", "initial");
  10247. };
  10248.  
  10249. // 固定到底部
  10250. function fixToBottom() {
  10251. let editor = $('#OJBetter_editor');
  10252. editor.addClass('bottom');
  10253.  
  10254. let halfHeight = $(window).height() * 0.5;
  10255. let blankSpace = $('<div>', {
  10256. 'class': 'blank-space',
  10257. 'style': 'height: ' + (halfHeight + 30) + 'px;'
  10258. });
  10259. $('body').append(blankSpace);
  10260.  
  10261. let cancelButton = OJB_safeCreateJQElement(`
  10262. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10263. <i class="iconfont">&#xe625;</i>
  10264. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10265. </button>
  10266. `).on('click', () => cancelFixingToBottom(cancelButton, blankSpace));
  10267. $('body').append(cancelButton);
  10268.  
  10269. disableButtons();
  10270. GM_setValue("monacoEditor_position", "bottom");
  10271. }
  10272.  
  10273. // 取消固定到底部
  10274. const cancelFixingToBottom = (cancelButton, blankSpace) => {
  10275. let editor = $('#OJBetter_editor');
  10276. editor.removeClass('bottom');
  10277. cancelButton.remove();
  10278. blankSpace.remove();
  10279. enableButtons();
  10280. GM_setValue("monacoEditor_position", "initial");
  10281. };
  10282.  
  10283. // 固定到右侧边栏
  10284. function fixToRight() {
  10285. const sidebar = $('#sidebar').hide();
  10286.  
  10287. // 添加样式
  10288. const styleElement = GM_addStyle(`
  10289. #body {
  10290. min-width: 50vw;
  10291. max-width: 50vw;
  10292. max-height: 100vh;
  10293. overflow-x: hidden;
  10294. overflow-y: auto;
  10295. padding: 1rem;
  10296. box-sizing: border-box;
  10297. }
  10298. body {
  10299. margin: 0px;
  10300. }
  10301. .content-with-sidebar {
  10302. margin-right: 0px !important;
  10303. }
  10304. .menu-list li {
  10305. margin-right: 0.5em;
  10306. }
  10307. .menu-list li a {
  10308. font-size: 1.4rem;
  10309. }
  10310. #OJBetter_editor{
  10311. height: 100vh;
  10312. width: 50vw;
  10313. }
  10314. `);
  10315.  
  10316. // 包装一层div
  10317. $('#body').wrap('<div id="right-side-wrapper" style="display:flex; max-width: 100vw; overflow: hidden;"></div>');
  10318. const blankSpace = $('<div>').appendTo('#right-side-wrapper');
  10319.  
  10320. // 移动编辑器
  10321. const editor = $('#OJBetter_editor').prependTo(blankSpace).addClass('right-side');
  10322.  
  10323. // 取消按钮
  10324. const cancelButton = OJB_safeCreateJQElement(`
  10325. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10326. <i class="iconfont">&#xe625;</i>
  10327. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10328. </button>
  10329. `).on('click', () => cancelFixingToRight(sidebar, styleElement, editor, cancelButton)).appendTo('body');
  10330.  
  10331. disableButtons();
  10332. GM_setValue("monacoEditor_position", "right");
  10333.  
  10334. // 补丁:修复固定到右侧导致的样例元素.sample-test相关代码重复执行的问题(具体原因未查)
  10335. $('.sample-test').find('.title').each((i, e) => {
  10336. if ($(e).find('.input-output-copier').length > 1) {
  10337. $(e).find('.input-output-copier').first().remove();
  10338. }
  10339. });
  10340. darkModeStyleAdjustment();
  10341. }
  10342.  
  10343. const cancelFixingToRight = (sidebar, styleElement, editor, cancelButton) => {
  10344. sidebar.show();
  10345. // 移回来
  10346. editor.insertAfter(form.sourceDiv).removeClass('right-side');
  10347.  
  10348. // 移除包装
  10349. $('#body').unwrap();
  10350. cancelButton.remove();
  10351. styleElement.remove(); // 移除添加的样式
  10352.  
  10353. enableButtons();
  10354. GM_setValue("monacoEditor_position", "initial");
  10355. }
  10356.  
  10357. // 代码同步与保存
  10358. if (OJBetter.monaco.setting.autoMemoryCode) {
  10359. let nowUrl = window.location.href;
  10360. nowUrl = nowUrl.replace(/#/, ""); // 当页面存在更改时url会多出一个#,去掉
  10361. const code = await getCode(nowUrl);
  10362. if (code) {
  10363. OJBetter.monaco.editor.setValue(code); // 恢复代码
  10364. form.sourceDiv.val(code);
  10365. }
  10366. OJBetter.monaco.editor.onDidChangeModelContent(async () => {
  10367. // 将monaco editor的内容同步到sourceDiv
  10368. const code = OJBetter.monaco.editor.getValue();
  10369. form.sourceDiv.val(code);
  10370. await saveCode(nowUrl, code);
  10371. });
  10372. }
  10373. })();
  10374.  
  10375. /**
  10376. * 注册本地自动补全
  10377. */
  10378. (OJBetter_monaco.RegisterLocalComplet = async () => {
  10379. // 补全器注册函数
  10380. function registMyCompletionItemProvider(language, genre, rule) {
  10381. if (genre == "monaco") {
  10382. monaco.languages.registerCompletionItemProvider(language, {
  10383. provideCompletionItems: function (model, position) {
  10384. return parseMonacoCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10385. }
  10386. })
  10387. } else if (genre == "ace") {
  10388. monaco.languages.registerCompletionItemProvider(language, {
  10389. provideCompletionItems: function (model, position) {
  10390. return parseAceCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10391. }
  10392. })
  10393. }
  10394. }
  10395.  
  10396. // 注册acwing cpp 模板
  10397. if (language == "cpp" && OJBetter.monaco.complet.cppCodeTemplate) {
  10398. try {
  10399. var acwing_cpp_code_completer = JSON.parse(GM_getResourceText("acwing_cpp_code_completer"));
  10400. registMyCompletionItemProvider('cpp', 'ace', acwing_cpp_code_completer);
  10401. } catch (error) {
  10402. console.error("Error registering acwing cpp template:", error);
  10403. }
  10404. }
  10405.  
  10406. // 注册自定义的补全
  10407. let complet_length = OJBetter.monaco.complet.customConfig.configurations.length;
  10408. if (complet_length > 0) {
  10409. for (let i = 0; i < complet_length; i++) {
  10410. let item = OJBetter.monaco.complet.customConfig.configurations[i];
  10411. if (item.isChoose && item.language == language) {
  10412. try {
  10413. let rule = await OJB_getExternalJSON(item.jsonUrl);
  10414. registMyCompletionItemProvider(item.language, item.genre, rule);
  10415. } catch (error) {
  10416. console.error(`Error registering custom completer for ${item.language}:`, error);
  10417. }
  10418. }
  10419. }
  10420. }
  10421. })();
  10422.  
  10423. if (!support || !OJBetter.monaco.lsp.enabled) { return; } // 如果不支持lsp,则到此为止
  10424.  
  10425. /**
  10426. * LSP连接状态指示
  10427. */
  10428. const lspStateButton = OJB_safeCreateJQElement(`
  10429. <div id="lspStateDiv" class="ojb_btn ojb_btn_popover top loading">
  10430. <i class="iconfont">&#xe658;</i>
  10431. <span class="popover_content">${i18next.t('lsp.connect', { ns: 'codeEditor' })}</span>
  10432. </div>
  10433. `).on('click', () => {
  10434. OJB_showModal(LSPLogDiv);
  10435. LSPLogDiv.show();
  10436. });
  10437. form.topRightDiv.prepend(lspStateButton);
  10438.  
  10439. const LSPLogDiv = OJB_safeCreateJQElement(`
  10440. <dialog id="LSPLog" style="display: none;">
  10441. <button class="ojb_btn">${i18next.t('close', { ns: 'common' })}</button>
  10442. <div id="LSPLogList" style="overflow: auto;"></div>
  10443. <dialog>`);
  10444. $('body').append(LSPLogDiv);
  10445.  
  10446. const LSPLogList = $('<ul></ul>');
  10447. $('#LSPLogList').append(LSPLogList);
  10448.  
  10449. const closeButton = LSPLogDiv.find('button');
  10450. closeButton.on('click', function () {
  10451. OJB_closeModal(LSPLogDiv);
  10452. });
  10453.  
  10454. /**
  10455. * 推送新的消息到LSP日志中
  10456. * @param {'error' | 'warn' | 'info'} status
  10457. * @param {string} msg
  10458. * @param {boolean} data
  10459. */
  10460. function pushLSPLogMessage(status, msg, data) {
  10461. var li = $('<li>').text('[' + new Date().toLocaleString() + '] ' + msg);
  10462. if (status === 'error') {
  10463. li.attr('style', 'color: #f44336;');
  10464. } else if (status === 'warn') {
  10465. li.attr('style', 'color: #ff9800;');
  10466. } else if (status === 'info') {
  10467. li.attr('style', 'color: #616161;');
  10468. }
  10469. if (data) {
  10470. var jsonText = JSON.stringify(data, null, 2);
  10471. var details = $('<details>');
  10472. var summary = $('<summary>').text('Data');
  10473. var pre = $('<pre>').text(jsonText);
  10474. details.append(summary, pre);
  10475. li.append(details);
  10476. }
  10477. LSPLogList.append(li);
  10478. }
  10479.  
  10480. /**
  10481. * 添加状态底栏
  10482. */
  10483. var statusBar = $('<div id="OJBetter_statusBar">');
  10484. form.editorDiv.append(statusBar);
  10485.  
  10486. /**
  10487. * languageSocket
  10488. */
  10489. var url = OJBetter.monaco.lsp.socketUrl;
  10490. var languageSocket = new WebSocket(url + language);
  10491. OJBetter.monaco.lsp.socket.push(languageSocket);
  10492. var languageSocketState = false;
  10493. var responseHandlers = new Map(); // 映射表,需要等待返回数据的请求 -> 对应的事件触发函数
  10494.  
  10495. languageSocket.onopen = () => {
  10496. languageSocketState = true;
  10497. lspStateButton.setButtonPopover(i18next.t('lsp.waitingAnswer', { ns: 'codeEditor' }));
  10498. pushLSPLogMessage("info", `languageSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10499. };
  10500. languageSocket.onmessage = (event) => {
  10501. const message = JSON.parse(event.data);
  10502. if (message.id === 0 && message.result) {
  10503. // 初始化完成
  10504. lspStateButton.setButtonState('success', i18next.t('lsp.connected', { ns: 'codeEditor' }));
  10505. pushLSPLogMessage("info", `Initialization ${i18next.t('lsp.state.finished', { ns: 'logMessage' })}`);
  10506. serverInfo = message.result; // 存下服务器支持信息
  10507. OJBetter_monaco.openDocRequest(); // 打开文档
  10508. if (!OJBetter.monaco.setting.language.includes(language)) {
  10509. OJBetter.monaco.setting.language.push(language);
  10510. OJBetter_monaco.RegistrationAfterInit(); // 注册语言及功能
  10511. } else {
  10512. location.reload(); // 这里有问题,先贴个补丁
  10513. }
  10514. OJBetter_monaco.PassiveReceiveHandler(); // 注册被动接收函数
  10515. } else if (message.id === 0 && message.error) {
  10516. pushLSPLogMessage("warn", `Initialization ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`);
  10517. } else if (message.id !== undefined && responseHandlers.has(message.id)) {
  10518. // 如果收到带有id字段的消息,则回传给对应的事件触发函数
  10519. const handler = responseHandlers.get(message.id);
  10520. if (handler) {
  10521. handler(message);
  10522. responseHandlers.delete(message.id); // 删除已处理的事件触发函数
  10523. }
  10524. } else if (message.method == "textDocument/publishDiagnostics") {
  10525. // 接收代码诊断推送
  10526. OJBetter_monaco.updateMarkers(message);
  10527. } else if (message.method == "workspace/applyEdit") {
  10528. // 应用服务器推送的更改
  10529. OJBetter_monaco.applyEdit(message);
  10530. }
  10531. };
  10532. languageSocket.onerror = (error) => {
  10533. pushLSPLogMessage("error", `languageSocket ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`, error);
  10534. console.warn(`Error connecting to languageSocket: ${error}`)
  10535. };
  10536. languageSocket.onclose = (event) => {
  10537. languageSocketState = false;
  10538. lspStateButton.setButtonState('error', i18next.t('lsp.error', { ns: 'codeEditor' }));
  10539. pushLSPLogMessage("warn", `languageSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`);
  10540. };
  10541.  
  10542. /**
  10543. * 等待LanguageSocketState
  10544. */
  10545. async function waitForLanguageSocketState() {
  10546. return new Promise((resolve) => {
  10547. const checkInitialized = () => {
  10548. if (languageSocketState) {
  10549. resolve();
  10550. } else {
  10551. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10552. }
  10553. };
  10554. checkInitialized();
  10555. });
  10556. }
  10557.  
  10558. // 等待lsp响应初始化结果
  10559. async function waitForInitialized() {
  10560. return new Promise((resolve) => {
  10561. const checkInitialized = () => {
  10562. if (initialized) {
  10563. resolve();
  10564. } else {
  10565. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10566. }
  10567. };
  10568. checkInitialized();
  10569. });
  10570. }
  10571.  
  10572. /**
  10573. * 与languageSocket通信的包装方法
  10574. */
  10575. async function sendMessage(data, requiresResponse, callback) {
  10576. if (!initialized) {
  10577. await waitForInitialized(); // 等待initialized为真
  10578. }
  10579. if (requiresResponse) {
  10580. responseHandlers.set(data.id, callback) // 将事件触发函数与id关联起来
  10581. }
  10582. if (!languageSocketState) await waitForLanguageSocketState();
  10583. languageSocket.send(JSON.stringify(data));
  10584. }
  10585. // 发送消息并等待返回结果
  10586. function fetchData(params, callback) {
  10587. sendMessage(params, true, callback);
  10588. }
  10589. // 发送消息,不需要等待返回结果
  10590. function sendData(data) {
  10591. sendMessage(data, false);
  10592. }
  10593.  
  10594. /**
  10595. * 代码文件更新fileWebSocket
  10596. */
  10597. var fileWebSocket = new WebSocket(url + "file");
  10598. var fileWebSocketState = false;
  10599. OJBetter.monaco.lsp.socket.push(fileWebSocket);
  10600. fileWebSocket.onopen = () => {
  10601. fileWebSocketState = true;
  10602. pushLSPLogMessage("info", `fileWebSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10603. };
  10604. fileWebSocket.onclose = (ev) => {
  10605. fileWebSocketState = false;
  10606. pushLSPLogMessage("warn", `fileWebSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`, ev);
  10607. };
  10608. fileWebSocket.onmessage = (ev) => {
  10609. let message = JSON.parse(ev.data);
  10610. if (message.result !== "ok")
  10611. pushLSPLogMessage("error", `update file failed: ${ev}`);
  10612. };
  10613. fileWebSocket.onerror = (error) => {
  10614. console.warn(`Error connecting to fileWebSocket: ${error}`);
  10615. };
  10616. async function updateFile(workspace, filename, fileExtension, code) {
  10617. async function waitForfileWebSocketState() {
  10618. return new Promise((resolve) => {
  10619. const checkInitialized = () => {
  10620. if (fileWebSocketState) {
  10621. resolve();
  10622. } else {
  10623. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10624. }
  10625. };
  10626. checkInitialized();
  10627. });
  10628. }
  10629. if (!fileWebSocketState) await waitForfileWebSocketState();
  10630. fileWebSocket.send(
  10631. JSON.stringify({
  10632. type: "update",
  10633. workspace,
  10634. filename,
  10635. fileExtension,
  10636. code,
  10637. })
  10638. );
  10639. }
  10640.  
  10641. /**
  10642. * 发送初始化请求
  10643. */
  10644. OJBetter_monaco.Initialize = () => {
  10645. //初始化initialize
  10646. const capabilities = {
  10647. workspace: {
  10648. applyEdit: true,
  10649. },
  10650. textDocument: {
  10651. publishDiagnostics: {
  10652. relatedInformation: true,
  10653. versionSupport: false,
  10654. tagSupport: {
  10655. valueSet: [1, 2],
  10656. },
  10657. codeDescriptionSupport: true,
  10658. },
  10659. completion: {
  10660. contextSupport: true,
  10661. completionItem: {
  10662. snippetSupport: true,
  10663. commitCharactersSupport: true,
  10664. documentationFormat: ["markdown", "plaintext"],
  10665. deprecatedSupport: true,
  10666. preselectSupport: true,
  10667. tagSupport: {
  10668. valueSet: [1],
  10669. },
  10670. insertReplaceSupport: true,
  10671. resolveSupport: {
  10672. properties: [
  10673. "documentation",
  10674. "detail",
  10675. "additionalTextEdits",
  10676. ],
  10677. },
  10678. insertTextModeSupport: {
  10679. valueSet: [1, 2],
  10680. },
  10681. },
  10682. },
  10683. hover: {
  10684. dynamicRegistration: true,
  10685. contentFormat: ["markdown", "plaintext"],
  10686. },
  10687. signatureHelp: {
  10688. signatureInformation: {
  10689. documentationFormat: ["markdown", "plaintext"],
  10690. parameterInformation: {
  10691. labelOffsetSupport: true,
  10692. },
  10693. activeParameterSupport: true,
  10694. },
  10695. contextSupport: true,
  10696. },
  10697. definition: {
  10698. dynamicRegistration: true,
  10699. linkSupport: true,
  10700. },
  10701. references: {
  10702. dynamicRegistration: true,
  10703. },
  10704. documentHighlight: {
  10705. dynamicRegistration: true,
  10706. },
  10707. codeAction: {
  10708. codeActionLiteralSupport: {
  10709. codeActionKind: {
  10710. valueSet:
  10711. language == "java"
  10712. ? []
  10713. : [
  10714. "",
  10715. "quickfix",
  10716. "refactor",
  10717. "refactor.extract",
  10718. "refactor.inline",
  10719. "refactor.rewrite",
  10720. "source",
  10721. "source.organizeImports",
  10722. ],
  10723. },
  10724. },
  10725. },
  10726. rename: {
  10727. dynamicRegistration: true,
  10728. prepareSupport: true,
  10729. prepareSupportDefaultBehavior: 1,
  10730. honorsChangeAnnotations: true,
  10731. },
  10732. documentLink: {
  10733. tooltipSupport: true,
  10734. },
  10735. typeDefinition: {
  10736. dynamicRegistration: true,
  10737. linkSupport: true,
  10738. },
  10739. implementation: {
  10740. dynamicRegistration: true,
  10741. linkSupport: true,
  10742. },
  10743. colorProvider: {
  10744. dynamicRegistration: true,
  10745. },
  10746. foldingRange: {
  10747. dynamicRegistration: true,
  10748. rangeLimit: 5000,
  10749. lineFoldingOnly: true,
  10750. },
  10751. declaration: {
  10752. dynamicRegistration: true,
  10753. linkSupport: true,
  10754. },
  10755. semanticTokens: {
  10756. dynamicRegistration: true,
  10757. tokenTypes: [
  10758. "namespace",
  10759. "type",
  10760. "class",
  10761. "enum",
  10762. "interface",
  10763. "struct",
  10764. "typeParameter",
  10765. "parameter",
  10766. "variable",
  10767. "property",
  10768. "enumMember",
  10769. "event",
  10770. "function",
  10771. "method",
  10772. "macro",
  10773. "keyword",
  10774. "modifier",
  10775. "comment",
  10776. "string",
  10777. "number",
  10778. "regexp",
  10779. "operator",
  10780. ],
  10781. tokenModifiers: [
  10782. "declaration",
  10783. "definition",
  10784. "readonly",
  10785. "static",
  10786. "deprecated",
  10787. "abstract",
  10788. "async",
  10789. "modification",
  10790. "documentation",
  10791. "defaultLibrary",
  10792. ],
  10793. formats: ["relative"],
  10794. requests: {
  10795. range: true,
  10796. full: {
  10797. delta: true,
  10798. },
  10799. },
  10800. multilineTokenSupport: false,
  10801. overlappingTokenSupport: false,
  10802. },
  10803. callHierarchy: {
  10804. dynamicRegistration: true,
  10805. },
  10806. },
  10807. window: {
  10808. showMessage: {
  10809. messageActionItem: {
  10810. additionalPropertiesSupport: true,
  10811. },
  10812. },
  10813. showDocument: {
  10814. support: true,
  10815. },
  10816. workDoneProgress: true,
  10817. },
  10818. general: {
  10819. regularExpressions: {
  10820. engine: "ECMAScript",
  10821. version: "ES2020",
  10822. },
  10823. markdown: {
  10824. parser: "marked",
  10825. version: "1.1.0",
  10826. },
  10827. },
  10828. };
  10829.  
  10830. const initializeRequest = {
  10831. id: id++,
  10832. jsonrpc: "2.0",
  10833. method: "initialize",
  10834. params: {
  10835. processId: null,
  10836. clientInfo: {
  10837. name: "CFMonaco" + InstanceID,
  10838. },
  10839. locale: "zh-CN",
  10840. rootPath: null,
  10841. rootUri: null,
  10842. capabilities: capabilities,
  10843. trace: "off",
  10844. workspaceFolders: [
  10845. {
  10846. uri:
  10847. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  10848. name:
  10849. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  10850. },
  10851. ],
  10852. },
  10853. };
  10854. languageSocket.send(JSON.stringify(initializeRequest));
  10855.  
  10856. // 打开文档函数
  10857. OJBetter_monaco.openDocRequest = function () {
  10858. const initializ = {
  10859. jsonrpc: "2.0",
  10860. method: "initialized",
  10861. params: {},
  10862. };
  10863. languageSocket.send(JSON.stringify(initializ));
  10864. const openDocRequest = {
  10865. jsonrpc: "2.0",
  10866. method: "textDocument/didOpen",
  10867. params: {
  10868. textDocument: {
  10869. uri: model.uri.toString(),
  10870. languageId: language,
  10871. version: model.getVersionId(),
  10872. text: model.getValue(),
  10873. },
  10874. },
  10875. };
  10876. languageSocket.send(JSON.stringify(openDocRequest));
  10877. initialized = true; // 初始化完成,这里确认逻辑待完善
  10878. };
  10879.  
  10880. // 初始化更新文件
  10881. updateFile(workspace, filename, fileExtension, model.getValue());
  10882. }
  10883.  
  10884. /**
  10885. * 注册语言及功能
  10886. */
  10887. OJBetter_monaco.RegistrationAfterInit = () => {
  10888. // 注册语言
  10889. monaco.languages.register({ id: language });
  10890.  
  10891. // 注册"Command"
  10892. (function registerCommand() {
  10893. serverInfo.capabilities.executeCommandProvider.commands.forEach(
  10894. (item) => {
  10895. pushLSPLogMessage("info", `${i18next.t('lsp.server.regist', { ns: 'logMessage' })}`, item);
  10896. monaco.editor.registerCommand(item, (accessor, ...args) => {
  10897. sendData({
  10898. jsonrpc: "2.0",
  10899. id: id++,
  10900. method: "workspace/executeCommand",
  10901. params: {
  10902. command: item,
  10903. arguments: args,
  10904. },
  10905. });
  10906. });
  10907. }
  10908. );
  10909. })();
  10910.  
  10911. // 注册"增量更新"
  10912. model.onDidChangeContent((event) => {
  10913. updateFile(workspace, filename, fileExtension, model.getValue()); // 更新文件
  10914. const changeDocRequest = {
  10915. jsonrpc: "2.0",
  10916. method: "textDocument/didChange",
  10917. params: {
  10918. textDocument: {
  10919. uri: model.uri.toString(),
  10920. version: model.getVersionId(),
  10921. },
  10922. contentChanges: event.changes.map((change) => ({
  10923. range: OJBetter_monaco.MonacoRangeTolspRange(change.range),
  10924. rangeLength: change.rangeLength,
  10925. text: change.text,
  10926. })),
  10927. },
  10928. };
  10929. sendData(changeDocRequest);
  10930. });
  10931.  
  10932. //注册"自动补全"
  10933. monaco.languages.registerCompletionItemProvider(language, {
  10934. provideCompletionItems: (model, position, context) => {
  10935. const request = {
  10936. jsonrpc: "2.0",
  10937. id: id++,
  10938. method: "textDocument/completion",
  10939. params: {
  10940. textDocument: {
  10941. uri: model.uri.toString(),
  10942. },
  10943. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  10944. context: {
  10945. triggerKind: context.triggerKind + 1, // 这里要+1,两边的定义不一样。。。
  10946. triggerCharacter: context.triggerCharacter,
  10947. },
  10948. },
  10949. };
  10950. return new Promise((resolve, reject) => {
  10951. fetchData(request, (response) => {
  10952. const result = response.result;
  10953. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  10954. if (!result) return resolve(null);
  10955. const CompletionItems = {
  10956. suggestions: result.items.map(
  10957. ({
  10958. label,
  10959. kind,
  10960. filterText,
  10961. insertText,
  10962. insertTextFormat,
  10963. sortText,
  10964. textEdit,
  10965. documentation,
  10966. additionalTextEdits,
  10967. }) => ({
  10968. additionalTextEdits: additionalTextEdits
  10969. ? additionalTextEdits.map(({ newText, range }) => ({
  10970. text: newText,
  10971. range: OJBetter_monaco.lspRangeToMonacoRange(range),
  10972. }))
  10973. : [],
  10974. documentation: documentation ? documentation.value : "",
  10975. filterText,
  10976. insertText: insertText ? insertText : textEdit.newText,
  10977. insertTextRules:
  10978. insertTextFormat === 2
  10979. ? monaco.languages.CompletionItemInsertTextRule
  10980. .InsertAsSnippet
  10981. : monaco.languages.CompletionItemInsertTextRule
  10982. .KeepWhitespace,
  10983. kind,
  10984. label,
  10985. sortText,
  10986. range: textEdit
  10987. ? textEdit.range
  10988. ? OJBetter_monaco.lspRangeToMonacoRange(textEdit.range)
  10989. : OJBetter_monaco.lspRangeToMonacoRange(textEdit.insert)
  10990. : null,
  10991. })
  10992. ),
  10993. };
  10994. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, CompletionItems);
  10995. resolve(CompletionItems);
  10996. });
  10997. });
  10998. },
  10999. });
  11000.  
  11001. // 注册"代码修复"
  11002. monaco.languages.registerCodeActionProvider(language, {
  11003. provideCodeActions: (model, range, context) => {
  11004. const request = {
  11005. id: id++,
  11006. jsonrpc: "2.0",
  11007. method: "textDocument/codeAction",
  11008. params: {
  11009. textDocument: {
  11010. uri: model.uri.toString(),
  11011. },
  11012. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11013. context: {
  11014. diagnostics: context.markers.map((item) => ({
  11015. range: OJBetter_monaco.MonacoRangeTolspRange({
  11016. startLineNumber: item.startLineNumber,
  11017. startColumn: item.startColumn,
  11018. endLineNumber: item.endLineNumber,
  11019. endColumn: item.endColumn,
  11020. }),
  11021. severity: OJBetter_monaco.MonacoSeverityTolspSeverity(
  11022. item.severity
  11023. ),
  11024. code: item.code,
  11025. source: item.source,
  11026. message: item.message,
  11027. tags: item.tags,
  11028. relatedInformation: item.relatedInformation
  11029. ? item.relatedInformation.map((item) => ({
  11030. location: {
  11031. uri: item.resource.toString(),
  11032. range: OJBetter_monaco.MonacoRangeTolspRange({
  11033. startLineNumber: item.startLineNumber,
  11034. startColumn: item.startColumn,
  11035. endLineNumber: item.endLineNumber,
  11036. endColumn: item.endColumn,
  11037. }),
  11038. },
  11039. message: item.message,
  11040. }))
  11041. : null,
  11042. })),
  11043. only: context.only ? [context.only] : [],
  11044. triggerKind: context.trigger,
  11045. },
  11046. },
  11047. };
  11048.  
  11049. return new Promise((resolve, reject) => {
  11050. fetchData(request, (response) => {
  11051. const result = response.result;
  11052. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11053. if (!result) return resolve(null);
  11054. const codeAction = {
  11055. actions: result.map((item) => ({
  11056. title: item.title,
  11057. kind: item.kind ? item.kind : "quickfix",
  11058. command: item.command
  11059. ? item.command.command
  11060. ? {
  11061. id: item.command.command,
  11062. arguments: item.command.arguments,
  11063. title: item.command.title,
  11064. }
  11065. : null
  11066. : null,
  11067. diagnostics: item.diagnostics
  11068. ? item.diagnostics.map((item) => ({
  11069. code: item.code,
  11070. message: item.message,
  11071. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11072. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(
  11073. item.severity
  11074. ),
  11075. source: item.source,
  11076. }))
  11077. : null,
  11078. edit: item.edit
  11079. ? OJBetter_monaco.lspEditToMonacoEdit(item.edit)
  11080. : item.arguments
  11081. ? {
  11082. edits: item.arguments.flatMap(
  11083. (item1) => OJBetter_monaco.lspEditToMonacoEdit(item1).edits
  11084. ),
  11085. }
  11086. : null,
  11087. })),
  11088. dispose: () => { },
  11089. };
  11090. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, codeAction);
  11091.  
  11092. resolve(codeAction);
  11093. });
  11094. });
  11095. },
  11096. });
  11097.  
  11098. // 注册"hover提示"
  11099. monaco.languages.registerHoverProvider(language, {
  11100. provideHover: (model, position) => {
  11101. const request = {
  11102. jsonrpc: "2.0",
  11103. id: id++,
  11104. method: "textDocument/hover",
  11105. params: {
  11106. textDocument: {
  11107. uri: model.uri.toString(),
  11108. },
  11109. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11110. },
  11111. };
  11112.  
  11113. return new Promise((resolve, reject) => {
  11114. fetchData(request, (response) => {
  11115. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11116. const result = response.result;
  11117.  
  11118. if (!result) return resolve(null);
  11119. const Hover = {
  11120. range: result.range
  11121. ? OJBetter_monaco.lspRangeToMonacoRange(result.range)
  11122. : new monaco.Range(
  11123. position.lineNumber,
  11124. position.column,
  11125. position.lineNumber,
  11126. position.column
  11127. ),
  11128. contents: Array.isArray(result.contents)
  11129. ? result.contents.map((item) => ({
  11130. value: item.value ? item.value : item,
  11131. }))
  11132. : [
  11133. {
  11134. value: result.contents.value,
  11135. },
  11136. ],
  11137. };
  11138. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, Hover);
  11139. resolve(Hover);
  11140. });
  11141. });
  11142. },
  11143. });
  11144.  
  11145. // 注册"inlay提示"
  11146. if (language == "cpp" || language == "java")
  11147. monaco.languages.registerInlayHintsProvider(language, {
  11148. provideInlayHints: (model, range, token) => {
  11149. return new Promise((resolve, reject) => {
  11150. const request = {
  11151. jsonrpc: "2.0",
  11152. id: id++,
  11153. method: "textDocument/inlayHint",
  11154. params: {
  11155. textDocument: {
  11156. uri: model.uri.toString(),
  11157. },
  11158. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11159. },
  11160. };
  11161.  
  11162. fetchData(request, (response) => {
  11163. const result = response.result;
  11164. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11165.  
  11166. if (!result) return resolve(null);
  11167.  
  11168. const inlayHints = {
  11169. hints: result.map((item) => {
  11170. return {
  11171. kind: item.kind,
  11172. label: item.label,
  11173. paddingLeft: item.paddingLeft,
  11174. paddingRight: item.paddingRight,
  11175. position: {
  11176. lineNumber: item.position.line + 1,
  11177. column: item.position.character + 1,
  11178. },
  11179. };
  11180. }),
  11181. };
  11182. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, inlayHints);
  11183.  
  11184. resolve(inlayHints);
  11185. });
  11186. });
  11187. },
  11188. });
  11189.  
  11190. // 注册"转到定义"
  11191. monaco.languages.registerDefinitionProvider(language, {
  11192. provideDefinition: (model, position) => {
  11193. const request = {
  11194. jsonrpc: "2.0",
  11195. id: id++,
  11196. method: "textDocument/definition",
  11197. params: {
  11198. textDocument: {
  11199. uri: model.uri.toString(),
  11200. },
  11201. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11202. },
  11203. };
  11204.  
  11205. return new Promise((resolve, reject) => {
  11206. fetchData(request, (response) => {
  11207. const result = response.result;
  11208. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11209.  
  11210. if (result.length == 0) return resolve(null);
  11211. const definition = result.map((item) => ({
  11212. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11213. uri: monaco.Uri.parse(item.uri), //
  11214. }));
  11215. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, definition);
  11216.  
  11217. resolve(definition);
  11218. });
  11219. });
  11220.  
  11221. },
  11222. });
  11223.  
  11224. // 注册"转到引用"
  11225. monaco.languages.registerReferenceProvider(language, {
  11226. provideReferences: (model, position, context) => {
  11227. const request = {
  11228. jsonrpc: "2.0",
  11229. id: id++,
  11230. method: "textDocument/references",
  11231. params: {
  11232. context: context,
  11233. textDocument: {
  11234. uri: model.uri.toString(),
  11235. },
  11236. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11237. },
  11238. };
  11239.  
  11240. return new Promise((resolve, reject) => {
  11241. fetchData(request, (response) => {
  11242. const result = response.result;
  11243. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11244.  
  11245. if (result.length == 0) return resolve([]);
  11246.  
  11247. const references = result.map((item) => ({
  11248. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11249. uri: monaco.Uri.parse(item.uri), //
  11250. }));
  11251. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, references);
  11252. resolve(references);
  11253. });
  11254. });
  11255. },
  11256. });
  11257.  
  11258. // 注册"符号引用点击高亮"
  11259. monaco.languages.registerDocumentHighlightProvider(language, {
  11260. provideDocumentHighlights: (model, position) => {
  11261. const request = {
  11262. jsonrpc: "2.0",
  11263. id: id++,
  11264. method: "textDocument/documentHighlight",
  11265. params: {
  11266. textDocument: {
  11267. uri: model.uri.toString(),
  11268. },
  11269. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11270. },
  11271. };
  11272.  
  11273. return new Promise((resolve, reject) => {
  11274. fetchData(request, (response) => {
  11275. const result = response.result;
  11276. pushLSPLogMessage("info", `documentHighlight ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11277.  
  11278. if (!result || result.length == 0) return resolve([]);
  11279. const highlights = result.map((item) => ({
  11280. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11281. kind: item.kind,
  11282. }));
  11283. pushLSPLogMessage("info",
  11284. `documentHighlight ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11285. highlights
  11286. );
  11287.  
  11288. resolve(highlights);
  11289. });
  11290. });
  11291. },
  11292. });
  11293.  
  11294. // 注册"文件链接"
  11295. if (language == "cpp" || language == "java")
  11296. monaco.languages.registerLinkProvider(language, {
  11297. provideLinks: (model) => {
  11298. const request = {
  11299. jsonrpc: "2.0",
  11300. id: id++,
  11301. method: "textDocument/documentLink",
  11302. params: {
  11303. textDocument: {
  11304. uri: model.uri.toString(),
  11305. },
  11306. },
  11307. };
  11308.  
  11309. return new Promise((resolve, reject) => {
  11310. fetchData(request, (response) => {
  11311. const result = response.result;
  11312. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11313.  
  11314. if (!result) return resolve(null);
  11315. const links = {
  11316. links: result.map((item) => ({
  11317. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11318. url: item.target.toString(),
  11319. tooltip: item.tooltip ? item.tooltip : null,
  11320. })),
  11321. };
  11322. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, links);
  11323. resolve(links);
  11324. });
  11325. });
  11326. },
  11327. });
  11328.  
  11329. // 注册"格式化"
  11330. monaco.languages.registerDocumentFormattingEditProvider(language, {
  11331. provideDocumentFormattingEdits: (model, options, token) => {
  11332. const request = {
  11333. jsonrpc: "2.0",
  11334. id: id++,
  11335. method: "textDocument/formatting",
  11336. params: {
  11337. textDocument: {
  11338. uri: model.uri.toString(),
  11339. },
  11340. options: options,
  11341. },
  11342. };
  11343.  
  11344. return new Promise((resolve, reject) => {
  11345. fetchData(request, (response) => {
  11346. const result = response.result;
  11347. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11348.  
  11349. const TextEdit = result.map((edit) => ({
  11350. range: OJBetter_monaco.lspRangeToMonacoRange(edit.range),
  11351. text: edit.newText,
  11352. }));
  11353. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, TextEdit);
  11354. resolve(TextEdit);
  11355. });
  11356. });
  11357. },
  11358. });
  11359.  
  11360. // 注册"部分格式化"
  11361. monaco.languages.registerDocumentRangeFormattingEditProvider(language, {
  11362. provideDocumentRangeFormattingEdits: (model, range, options) => {
  11363. const request = {
  11364. jsonrpc: "2.0",
  11365. id: id++,
  11366. method: "textDocument/rangeFormatting",
  11367. params: {
  11368. textDocument: {
  11369. uri: model.uri.toString(),
  11370. },
  11371. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11372. options,
  11373. },
  11374. };
  11375.  
  11376. return new Promise((resolve, reject) => {
  11377. fetchData(request, (response) => {
  11378. const result = response.result;
  11379. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11380.  
  11381. if (!result || result.length == 0) return resolve([]);
  11382. const edits = result.map((item) => ({
  11383. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11384. text: item.newText,
  11385. }));
  11386. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11387. resolve(edits);
  11388. });
  11389. });
  11390. },
  11391. });
  11392.  
  11393. // 注册"重命名"
  11394. monaco.languages.registerRenameProvider(language, {
  11395. provideRenameEdits: (model, position, newName, token) => {
  11396. const request = {
  11397. jsonrpc: "2.0",
  11398. id: id++,
  11399. method: "textDocument/rename",
  11400. params: {
  11401. textDocument: {
  11402. uri: model.uri.toString(),
  11403. },
  11404. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11405. newName: newName,
  11406. },
  11407. };
  11408.  
  11409. return new Promise((resolve, reject) => {
  11410. fetchData(request, (response) => {
  11411. const result = response.result;
  11412. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11413.  
  11414. const rename = OJBetter_monaco.lspEditToMonacoEdit(result);
  11415. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, rename);
  11416. resolve(rename);
  11417. });
  11418. });
  11419. },
  11420. });
  11421.  
  11422. // 注册"折叠范围分析"
  11423. monaco.languages.registerFoldingRangeProvider(language, {
  11424. provideFoldingRanges: (model) => {
  11425. const request = {
  11426. jsonrpc: "2.0",
  11427. id: id++,
  11428. method: "textDocument/foldingRange",
  11429. params: {
  11430. textDocument: {
  11431. uri: model.uri.toString(),
  11432. },
  11433. },
  11434. };
  11435.  
  11436. return new Promise((resolve, reject) => {
  11437. fetchData(request, (response) => {
  11438. const result = response.result;
  11439. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11440.  
  11441. if (!result) return resolve([]);
  11442. const foldingRanges = result.map((item) => ({
  11443. start: item.startLine + 1,
  11444. end: item.endLine + 1,
  11445. kind: monaco.languages.FoldingRangeKind.fromValue(item.kind),
  11446. }));
  11447. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, foldingRanges);
  11448. resolve(foldingRanges);
  11449. });
  11450. });
  11451. },
  11452. });
  11453.  
  11454. // 注册"方法签名提示"
  11455. monaco.languages.registerSignatureHelpProvider(language, {
  11456. signatureHelpTriggerCharacters:
  11457. serverInfo.capabilities.signatureHelpProvider.triggerCharacters,
  11458. provideSignatureHelp: (model, position, token, context) => {
  11459. const request = {
  11460. jsonrpc: "2.0",
  11461. id: id++,
  11462. method: "textDocument/signatureHelp",
  11463. params: {
  11464. textDocument: {
  11465. uri: model.uri.toString(),
  11466. },
  11467. position: {
  11468. line: position.lineNumber - 1,
  11469. character: position.column - 1,
  11470. },
  11471. context: {
  11472. triggerKind: context.triggerKind,
  11473. triggerCharacter: context.triggerCharacter,
  11474. isRetrigger: context.isRetrigger,
  11475. activeSignatureHelp: context.activeSignatureHelp,
  11476. },
  11477. },
  11478. };
  11479.  
  11480. return new Promise((resolve, reject) => {
  11481. fetchData(request, (response) => {
  11482. const result = response.result;
  11483.  
  11484. pushLSPLogMessage("info", `signatureHelp ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11485.  
  11486. if (!result) return resolve(null);
  11487. const SignatureHelpResult = {
  11488. value: {
  11489. activeParameter: result.activeParameter,
  11490. activeSignature: result.activeSignature,
  11491. signatures: result.signatures,
  11492. },
  11493. dispose: () => { },
  11494. };
  11495.  
  11496. pushLSPLogMessage("info",
  11497. `signatureHelp ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11498. SignatureHelpResult
  11499. );
  11500. resolve(SignatureHelpResult);
  11501. });
  11502. });
  11503. },
  11504. });
  11505.  
  11506. // 注册"渐进式自动格式化" 如果server有这个
  11507. if (serverInfo.capabilities.documentOnTypeFormattingProvider)
  11508. monaco.languages.registerOnTypeFormattingEditProvider(language, {
  11509. autoFormatTriggerCharacters: [
  11510. serverInfo.capabilities.documentOnTypeFormattingProvider
  11511. .firstTriggerCharacter,
  11512. ],
  11513. provideOnTypeFormattingEdits: (model, position, ch, options) => {
  11514. const request = {
  11515. jsonrpc: "2.0",
  11516. id: id++,
  11517. method: "textDocument/onTypeFormatting",
  11518. params: {
  11519. textDocument: {
  11520. uri: model.uri.toString(),
  11521. },
  11522. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11523. ch,
  11524. options,
  11525. },
  11526. };
  11527.  
  11528. return new Promise((resolve, reject) => {
  11529. fetchData(request, (response) => {
  11530. const result = response.result;
  11531. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11532.  
  11533. if (!result || result.length == 0) return resolve([]);
  11534.  
  11535. const edits = result.map((item) => ({
  11536. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11537. text: item.newText,
  11538. }));
  11539. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11540. resolve(edits);
  11541. });
  11542. });
  11543. },
  11544. });
  11545. };
  11546.  
  11547. /**
  11548. * 被动式接收处理
  11549. */
  11550. OJBetter_monaco.PassiveReceiveHandler = () => {
  11551.  
  11552. // "实时代码诊断"
  11553. OJBetter_monaco.updateMarkers = function (message) {
  11554. const params = message.params;
  11555. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11556.  
  11557. if (!params) return;
  11558. const markers = params.diagnostics.map((item1) => ({
  11559. code: item1.code,
  11560. message: item1.message,
  11561. ...OJBetter_monaco.lspRangeToMonacoRange(item1.range),
  11562. relatedInformation: item1.relatedInformation
  11563. ? item1.relatedInformation.map((item2) => ({
  11564. ...(item2.location.range
  11565. ? OJBetter_monaco.lspRangeToMonacoRange(item2.location.range)
  11566. : OJBetter_monaco.lspRangeToMonacoRange(item2.location)),
  11567. message: item2.message,
  11568. resource: monaco.Uri.parse(item2.location.uri),
  11569. }))
  11570. : null,
  11571. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(item1.severity),
  11572. source: item1.source,
  11573. }));
  11574.  
  11575. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, markers);
  11576. monaco.editor.setModelMarkers(model, "eslint", markers);
  11577.  
  11578. // 更新状态底栏信息
  11579. const nowMarks = monaco.editor.getModelMarkers();
  11580. warningCount = 0;
  11581. errorCount = 0;
  11582. for (const marker of nowMarks) {
  11583. if (marker.severity === monaco.MarkerSeverity.Warning) {
  11584. warningCount++;
  11585. } else if (marker.severity === monaco.MarkerSeverity.Error) {
  11586. errorCount++;
  11587. }
  11588. }
  11589. $('#OJBetter_statusBar').text(`Warnings: ${warningCount}, Errors: ${errorCount}`);
  11590. };
  11591.  
  11592. // "应用服务器推送的更改"(代码修复)
  11593. OJBetter_monaco.applyEdit = function (message) {
  11594. const params = message.params;
  11595. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11596.  
  11597. if (!params) return;
  11598. const operations = Object.values(params.edit.changes)
  11599. .flat()
  11600. .map((item) => ({
  11601. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11602. text: item.newText,
  11603. }));
  11604.  
  11605. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, operations);
  11606. model.pushEditOperations([], operations, () => null); // 入栈编辑操作
  11607. };
  11608. }
  11609.  
  11610. if (!languageSocketState) await waitForLanguageSocketState();
  11611. OJBetter_monaco.Initialize();
  11612. }
  11613.  
  11614. // 语言更改
  11615. function changeMonacoLanguage(form) {
  11616. let nowSelect = form.selectLang.val();
  11617.  
  11618. // 这里是因为在Chrome上Select2会莫名其妙触发一次不会改变值的change事件,而在其他浏览器中没有,所以贴个补丁
  11619. if (nowSelect === OJBetter.monaco.nowLangSelect) return;
  11620. else OJBetter.monaco.nowLangSelect = nowSelect;
  11621.  
  11622. // 记忆更改
  11623. GM_setValue('compilerSelection', nowSelect);
  11624. // 销毁旧的编辑器
  11625. try {
  11626. if (OJBetter.monaco.editor) OJBetter.monaco.editor.dispose();
  11627. } catch (error) {
  11628. console.warn("Encountered an error while attempting to delete the old editor, but it should not affect your regular usage.", error)
  11629. }
  11630. // 关闭旧的socket
  11631. OJBetter.monaco.lsp.socket.forEach(socket => {
  11632. socket.close();
  11633. });
  11634. // 移除相关元素
  11635. form.topRightDiv.empty();
  11636. $('#LSPLog').remove();
  11637. $('#OJBetter_statusBar').remove();
  11638. // 创建新的编辑器
  11639. if (nowSelect in value_monacoLanguageMap) {
  11640. let language = value_monacoLanguageMap[nowSelect];
  11641. if (language == "python" || language == "cpp") {
  11642. createMonacoEditor(language, form, true);
  11643. } else {
  11644. createMonacoEditor(language, form, false);
  11645. }
  11646. } else {
  11647. createMonacoEditor(null, form, false);
  11648. }
  11649. // 更新在线编译器参数
  11650. changeCompilerArgs(nowSelect);
  11651. }
  11652.  
  11653. // 收集样例数据
  11654. function getTestData() {
  11655. let testData = {};
  11656.  
  11657. /**
  11658. * 从pre中获取文本信息
  11659. * @param {JQuery<HTMLElement>} node 元素
  11660. * @returns {string} 文本信息
  11661. */
  11662. function getTextFromPre(node) {
  11663. let text;
  11664. if (node.find("br").length > 0) {
  11665. text = node.html().replace(/<br>/g, "\n"); // <br>作换行符的情况
  11666. } else {
  11667. text = node.text();
  11668. }
  11669. return text;
  11670. }
  11671.  
  11672. // $('.input').each(function (index) {
  11673. // var inputText = '';
  11674. // if ($(this).find('pre').find('div').length > 0) {
  11675. // $(this).find('pre').find('div').each(function () {
  11676. // inputText += getTextFromPre($(this)) + '\n';
  11677. // });
  11678. // } else {
  11679. // inputText = getTextFromPre($(this).find('pre'));
  11680. // }
  11681. // var outputText = '';
  11682. // if ($('.output').eq(index).find('pre').find('div').length > 0) {
  11683. // $('.output').eq(index).find('pre').find('div').each(function () {
  11684. // inputText += getTextFromPre($(this)) + '\n';
  11685. // });
  11686. // } else {
  11687. // outputText = getTextFromPre($('.output').eq(index).find('pre'));
  11688. // }
  11689.  
  11690. // testData[index + 1] = {
  11691. // input: inputText.trim(),
  11692. // output: outputText.trim()
  11693. // };
  11694. // });
  11695.  
  11696. // 需要过滤重复的样例(因为有日文英文两个页面,样例元素重复了两遍)
  11697. let uniqueTestData = [];
  11698. const filteredPreElements = $('pre').clone().filter((index, element) => element.id.startsWith('pre-sample'));
  11699. for (let i = 0; i < filteredPreElements.length; i += 2) {
  11700. if (i + 1 < filteredPreElements.length) {
  11701. const inputElement = $(filteredPreElements[i]);
  11702. const outputElement = $(filteredPreElements[i + 1]);
  11703.  
  11704. const inputText = getTextFromPre(inputElement).trim();
  11705. const outputText = getTextFromPre(outputElement).trim();
  11706.  
  11707. // 检查是否已经存在相同的样例
  11708. let isDuplicate = uniqueTestData.some(testCase => testCase.input === inputText && testCase.output === outputText);
  11709. if (!isDuplicate) {
  11710. uniqueTestData.push({
  11711. input: inputText,
  11712. output: outputText
  11713. });
  11714. }
  11715. }
  11716. }
  11717.  
  11718. // 把唯一的测试数据赋值给testData,保持原有的索引风格
  11719. uniqueTestData.forEach((testCase, index) => {
  11720. testData[index + 1] = testCase;
  11721. });
  11722.  
  11723. return testData;
  11724. }
  11725.  
  11726. // 初始化自定义测试数据面板
  11727. function CustomTestInit() {
  11728. const url = window.location.href;
  11729.  
  11730. restoreText();
  11731.  
  11732. // 添加
  11733. $('#addCustomTest').click(function () {
  11734. var sampleDiv = $('<div class="sampleDiv">');
  11735. var inputTextarea = $('<p style="padding: 0px 5px;">input</p><textarea class="dynamicTextarea inputTextarea"></textarea>');
  11736. var outputTextarea = $('<p style="padding: 0px 5px;">output</p><textarea class="dynamicTextarea outputTextarea"></textarea>');
  11737. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  11738. sampleDiv.append(deleteCustomTest);
  11739. sampleDiv.append(inputTextarea);
  11740. sampleDiv.append(outputTextarea);
  11741. $('#customTests').append(sampleDiv);
  11742. });
  11743.  
  11744. // 实时保存文本内容到 IndexedDB 中
  11745. $(document).on('input', '.inputTextarea, .outputTextarea', function () {
  11746. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  11747. var objectStore = OJBetter.common.database.samplesData;
  11748. var samples = {
  11749. url: url,
  11750. samples: []
  11751. };
  11752. var index = 0;
  11753. $('.sampleDiv').each(function () {
  11754. var $sampleDiv = $(this);
  11755. var inputTextarea = $sampleDiv.find('.inputTextarea');
  11756. var outputTextarea = $sampleDiv.find('.outputTextarea');
  11757. $sampleDiv.attr('data-index', index);
  11758. inputTextarea.attr('id', 'input' + index);
  11759. outputTextarea.attr('id', 'output' + index);
  11760. var sample = {
  11761. id: index,
  11762. input: inputTextarea.val(),
  11763. output: outputTextarea.val()
  11764. };
  11765. samples.samples.push(sample);
  11766. index++;
  11767. });
  11768. objectStore.put(samples);
  11769. });
  11770. });
  11771.  
  11772. // 删除
  11773. $(document).on('click', '.deleteCustomTest', function () {
  11774. var $sampleDiv = $(this).closest('.sampleDiv');
  11775. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  11776. var objectStore = OJBetter.common.database.samplesData;
  11777. var index = parseInt($sampleDiv.attr('data-index'));
  11778. if (!isNaN(index)) {
  11779. objectStore.get(url).then(row => {
  11780. let samples = row.samples;
  11781. samples.splice(index, 1); // 移除第index个元素
  11782. objectStore.put({
  11783. url: url,
  11784. samples: samples
  11785. });
  11786. })
  11787. }
  11788. $sampleDiv.remove();
  11789. });
  11790. });
  11791.  
  11792. // 恢复保存的内容
  11793. function restoreText() {
  11794. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  11795. return OJBetter.common.database.samplesData.get(url);
  11796. }).then(function (data) {
  11797. if (data.samples && data.samples.length > 0) {
  11798. data.samples.forEach(function (item, index) {
  11799. var sampleDiv = $('<div class="sampleDiv">');
  11800. var inputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">input</p><textarea id="input${index}" class="dynamicTextarea inputTextarea"></textarea>`);
  11801. var outputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">output</p><textarea id="output${index}" class="dynamicTextarea outputTextarea"></textarea>`);
  11802. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  11803.  
  11804. inputTextarea.val(item.input);
  11805. outputTextarea.val(item.output);
  11806.  
  11807. sampleDiv.append(deleteCustomTest);
  11808. sampleDiv.append(inputTextarea);
  11809. sampleDiv.append(outputTextarea);
  11810. sampleDiv.attr('data-index', index)
  11811. $('#customTests').append(sampleDiv);
  11812. });
  11813. }
  11814. });
  11815. }
  11816. }
  11817.  
  11818. // 获取自定义测试数据
  11819. function getCustomTestData() {
  11820. const url = window.location.href;
  11821.  
  11822. return new Promise(function (resolve) {
  11823. var customTestData = {};
  11824. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  11825. return OJBetter.common.database.samplesData.get(url);
  11826. }).then(function (data) {
  11827. if (!data) resolve(customTestData);
  11828. if (data.samples && data.samples.length > 0) {
  11829. data.samples.forEach(function (item, index) {
  11830. customTestData[index + 1] = {
  11831. input: item.input,
  11832. output: item.output
  11833. };
  11834. });
  11835. }
  11836. resolve(customTestData);
  11837. });
  11838. });
  11839. }
  11840.  
  11841. // codeforces编译器参数列表
  11842. let officialLanguage = "";
  11843. function officialCompilerArgsChange(nowSelect) {
  11844. officialLanguage = nowSelect;
  11845. $('#CompilerArgsInput').prop("disabled", true);
  11846. }
  11847.  
  11848. // codeforces编译器通信
  11849. async function officialCompiler(code, input) {
  11850. // const data = new FormData();
  11851. // data.append('csrf_token', OJBetter.common.cf_csrf_token);
  11852. const data = new URLSearchParams();
  11853. data.append('csrf_token', OJBetter.common.at_csrf_token);
  11854. // data.append('source', code);
  11855. // data.append('tabSize', '4');
  11856. // data.append('programTypeId', officialLanguage);
  11857. data.append('data.LanguageId', officialLanguage);
  11858. data.append('input', input);
  11859. // data.append('output', '');
  11860. // data.append('communityCode', '');
  11861. // data.append('action', 'submitSourceCode');
  11862. // data.append('programTypeId', officialLanguage);
  11863. data.append('sourceCode', code);
  11864.  
  11865. const requestOptions = {
  11866. method: 'POST',
  11867. // url: `${OJBetter.common.hostAddress}/data/customtest`,
  11868. url: `${OJBetter.common.hostAddress}/contests/arc172/custom_test/submit/json`,
  11869. data: data,
  11870. headers: {
  11871. // 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  11872. 'Content-Type': 'application/x-www-form-urlencoded'
  11873. }
  11874. };
  11875.  
  11876. const result = {
  11877. Errors: '',
  11878. Result: '',
  11879. Stats: ''
  11880. };
  11881.  
  11882. try {
  11883. const submitResponse = await OJB_GMRequest(requestOptions);
  11884. // if (submitResponse.status !== 200 || !submitResponse.response) {
  11885. // result.Errors = `${i18next.t('compiler.official.pushError', { ns: 'codeEditor' })}`;
  11886. // return result;
  11887. // }
  11888. if (submitResponse.status !== 200) {
  11889. result.Errors = `${i18next.t('compiler.official.pushError', { ns: 'codeEditor' })}`;
  11890. return result;
  11891. }
  11892.  
  11893. // const submitResult = JSON.parse(submitResponse.response);
  11894. // const customTestSubmitId = submitResult.customTestSubmitId;
  11895.  
  11896. const verdictResponse = await OJB_promiseRetryWrapper(
  11897. getOfficialCompilerVerdict,
  11898. {
  11899. maxRetries: 10,
  11900. retryInterval: 500
  11901. },
  11902. // customTestSubmitId
  11903. );
  11904. return verdictResponse;
  11905. } catch (error) {
  11906. result.Errors = error.message;
  11907. return result;
  11908. }
  11909. }
  11910.  
  11911. // 获取codeforces编译器的执行结果
  11912. // async function getOfficialCompilerVerdict(customTestSubmitId) {
  11913. async function getOfficialCompilerVerdict() {
  11914. // const newdata = new FormData();
  11915. // newdata.append('csrf_token', OJBetter.common.cf_csrf_token);
  11916. // newdata.append('action', 'getVerdict');
  11917. // newdata.append('customTestSubmitId', customTestSubmitId);
  11918.  
  11919. // const requestOptions = {
  11920. // method: 'POST',
  11921. // url: `${OJBetter.common.hostAddress}/data/customtest`,
  11922. // data: newdata,
  11923. // headers: {
  11924. // 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  11925. // }
  11926. // };
  11927. const requestOptions = {
  11928. method: 'GET',
  11929. url: `https://atcoder.jp/contests/arc172/custom_test/json?reload=true`,
  11930. };
  11931.  
  11932. const responseDetails = await OJB_GMRequest(requestOptions);
  11933. if (responseDetails.status !== 200 || !responseDetails.response) {
  11934. throw new Error(`${i18next.t('compiler.official.getResultError', { ns: 'codeEditor' })}`);
  11935. }
  11936.  
  11937. const response = JSON.parse(responseDetails.response);
  11938. // if (!response.stat) {
  11939. // throw new Error('Verdict not ready, retrying...');
  11940. // }
  11941. if (response.Interval) {
  11942. throw new Error('Verdict not ready, retrying...');
  11943. }
  11944.  
  11945. // return {
  11946. // Errors: response.verdict === "OK" ? null : response.verdict + '<br>' + response.output,
  11947. // Result: response.output.replace(/\r\n/g, "\n"),
  11948. // Stats: `Status: ${response.stat}`
  11949. // };
  11950. return {
  11951. Errors: response.Result.ExitCode === "0" ? null : response.Stderr,
  11952. Result: response.Stdout.replace(/\r\n/g, "\n"),
  11953. Stats: `Status: ExitCode: ${response.Result.ExitCode} Exec Time: ${response.Result.TimeConsumption} ms Memory: ${response.Result.MemoryConsumption} KB`
  11954. };
  11955. }
  11956.  
  11957. // rextester编译器参数列表
  11958. let rextesterLanguage = "";
  11959. function rextesterCompilerArgsChange(nowSelect) {
  11960. // let LanguageChoiceList = {
  11961. // "4": "9", "6": "8", "7": "5", "9": "1", "13": "13", "19": "42", "20": "21", "28": "30", "31": "24", "32": "20",
  11962. // "34": "17", "36": "4", "43": "6", "45": "7", "46": "4", "50": "7", "51": "9", "52": "27", "54": "7", "55": "23", "60": "4",
  11963. // "61": "7", "65": "1", "67": "12", "70": "5", "73": "7", "74": "4", "75": "46", "77": "43", "79": "1", "80": "27", "83": "43", "87": "4"
  11964. // }
  11965. let LanguageChoiceList = {
  11966. "5001": "7", "5002": "20", "5003": "1", "5005": "4", "5009": "17", "5016": "8", "5037": "13", "5041": "9",
  11967. "5047": "21", "5055": "5", "5058": "17", "5063": "5", "5078": "5", "5082": "5"
  11968. }
  11969.  
  11970. let CompilerArgsList = {
  11971. "6": "-Wall -std=gnu99 -O2 -o a.out source_file.c",
  11972. "7": "-Wall -std=c++14 -O2 -o a.out source_file.cpp",
  11973. "20": "-o a.out source_file.go",
  11974. "27": "-Wall -std=c++14 -stdlib=libc++ -O2 -o a.out source_file.cpp",
  11975. "30": "source_file.d -ofa.out"
  11976. }
  11977. if (nowSelect in LanguageChoiceList) {
  11978. $('#RunTestButton').prop("disabled", false);
  11979. rextesterLanguage = LanguageChoiceList[nowSelect];
  11980. } else {
  11981. $('#RunTestButton').prop("disabled", true);
  11982. }
  11983. if (rextesterLanguage in CompilerArgsList) {
  11984. const rextesterCompilerArgs = CompilerArgsList[rextesterLanguage];
  11985. $('#CompilerArgsInput').val(rextesterCompilerArgs);
  11986. } else {
  11987. $('#CompilerArgsInput').val("");
  11988. }
  11989. }
  11990.  
  11991. // rextester编译器通信
  11992. async function rextesterCompiler(code, input) {
  11993. try {
  11994. const result = await OJB_promiseRetryWrapper(rextesterCompilerRequest, {
  11995. maxRetries: 5,
  11996. retryInterval: 500,
  11997. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  11998. }, code, input);
  11999. return result;
  12000. } catch (error) {
  12001. return { Errors: error.message, Result: '', Stats: '' };
  12002. }
  12003. }
  12004.  
  12005. // rextester编译器请求方法
  12006. async function rextesterCompilerRequest(code, input) {
  12007. const data = new FormData();
  12008. data.append('LanguageChoiceWrapper', rextesterLanguage);
  12009. data.append('EditorChoiceWrapper', '1');
  12010. data.append('LayoutChoiceWrapper', '1');
  12011. data.append('Program', code);
  12012. data.append('CompilerArgs', $('#CompilerArgsInput').val());
  12013. data.append('Input', input);
  12014. data.append('ShowWarnings', 'false');
  12015. data.append('IsInEditMode', 'false');
  12016. data.append('IsLive', 'false');
  12017.  
  12018. const responseDetails = await OJB_GMRequest({
  12019. method: 'POST',
  12020. url: 'https://rextester.com/rundotnet/Run',
  12021. data: data
  12022. });
  12023.  
  12024. if (responseDetails.status !== 200 || !responseDetails.response) {
  12025. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12026. }
  12027.  
  12028. const response = JSON.parse(responseDetails.response);
  12029. return {
  12030. Errors: response.Errors || '',
  12031. Result: response.Result || '',
  12032. Stats: response.Stats || ''
  12033. };
  12034. }
  12035.  
  12036. // wandbox编译器参数列表
  12037. var wandboxlist = JSON.parse(GM_getResourceText("wandboxlist"));
  12038. function wandboxCompilerArgsChange(nowSelect) {
  12039. // let LanguageChoiceList = {
  12040. // "6": "PHP", "7": "Python", "9": "C#", "12": "Haskell", "13": "Perl", "19": "OCaml",
  12041. // "20": "Scala", "28": "D", "31": "Python", "32": "Go", "34": "JavaScript", "36": "Java", "40": "Python", "41": "Python",
  12042. // "43": "C++", "50": "C++", "51": "Pascal", "52": "C++", "54": "C++", "60": "Java", "61": "C++", "65": "C#", "67": "Ruby",
  12043. // "70": "Python", "73": "C++", "74": "Java", "75": "Rust", "79": "C#", "80": "C++", "87": "Java"
  12044. // }
  12045. let LanguageChoiceList = {
  12046. "5001": "C++", "5002": "Go", "5003": "C#", "5005": "Java", "5009": "JavaScript", "5010": "JavaScript",
  12047. "5012": "D", "5013": "D", "5016": "PHP", "5017": "C++", "5018": "Ruby", "5025": "Haskell",
  12048. "5027": "Lua", "5028": "C++", "5031": "C++", "5037": "Perl", "5041": "Pascal", "5042": "C#",
  12049. "5043": "Lua", "5047": "Scala", "5055": "Python", "5056": "Scala", "5059": "OCaml", "5062": "Lisp",
  12050. "5063": "Python", "5077": "D", "5078": "Python", "5081": "OCaml", "5082": "Python"
  12051. }
  12052.  
  12053. // 移除旧的
  12054. $('#CompilerChange').remove();
  12055.  
  12056. if (nowSelect in LanguageChoiceList) {
  12057. $('#RunTestButton').prop("disabled", false);
  12058. const Languagefiltered = wandboxlist.filter(obj => obj.language === LanguageChoiceList[nowSelect]);
  12059.  
  12060. // 创建编译器下拉框
  12061. let CompilerChange = $('<select id="CompilerChange" style="width: 100%;"></select>');
  12062.  
  12063. $('#CompilerSetting').show().append(CompilerChange);
  12064. for (let i = 0; i < Languagefiltered.length; i++) {
  12065. let Compiler = Languagefiltered[i];
  12066. let op = $("<option></option>")
  12067. .val(Compiler.name)
  12068. .text(Compiler["display-name"] + " " + Compiler.version);
  12069. $("#CompilerChange").append(op);
  12070. }
  12071.  
  12072. // 编译器参数刷新
  12073. function refreshCompilerArgs() {
  12074. var flags = '';
  12075. $("#CompilerBox").find("*").each(function () {
  12076. if ($(this).is("input[type='checkbox']")) {
  12077. let flag = $(this).prop("checked") ? $(this).val() : '';
  12078. flags += flag + (flag ? ' ' : '');
  12079. } else if ($(this).is("select") || $(this).is("input") || $(this).is("textarea")) {
  12080. let flag = $(this).val();
  12081. flags += flag + (flag ? ' ' : '');
  12082. }
  12083. });
  12084. $("#CompilerArgsInput").val(flags);
  12085. $("#CompilerArgsInput").prop("readonly", true); // 只读
  12086. }
  12087.  
  12088. // 编译器切换监听
  12089. CompilerChange.change(function () {
  12090. let selectedName = CompilerChange.val();
  12091. let Compiler = Languagefiltered.find(
  12092. (obj) => obj.name === selectedName
  12093. );
  12094.  
  12095. $("#CompilerArgsInput").val(); // 初始化编译器输入框
  12096.  
  12097. $("#CompilerBox").remove();
  12098. let div = $("<div id='CompilerBox'></div>");
  12099.  
  12100. let display_compile_command = OJB_safeCreateJQElement(`<input id='${Compiler.name}' value='${Compiler['display-compile-command']}' style="display:none;"}></input>`);
  12101. div.append(display_compile_command);
  12102.  
  12103. let switches = Compiler.switches;
  12104. for (let i = 0; i < switches.length; i++) {
  12105. let switche = switches[i];
  12106.  
  12107. if (switche.type == "single") {
  12108. let single = OJB_safeCreateJQElement(`
  12109. <div>
  12110. <input type='checkbox' id='${switche.name}' value='${switche['display-flags']}' ${switche.default ? 'checked' : ''}></input>
  12111. <label for='${switche.name}'>${switche['display-name']}</label>
  12112. </div>
  12113. `);
  12114. div.append(single);
  12115. single.find("input").change(function () {
  12116. refreshCompilerArgs();
  12117. });
  12118. } else if (switche.type == "select") {
  12119. let select = OJB_safeCreateJQElement(`<select id='${switche.name}'></select>`);
  12120. select.data('previousValue', switche.options[0]['display-flags']);
  12121. div.append(select);
  12122. for (let i = 0; i < switche.options.length; i++) {
  12123. let option = switche.options[i];
  12124. let op = $("<option></option>")
  12125. .val(option['display-flags'])
  12126. .text(option['display-name']);
  12127. select.append(op);
  12128. }
  12129. select.change(function () {
  12130. refreshCompilerArgs();
  12131. });
  12132. }
  12133. }
  12134.  
  12135. if (Compiler['compiler-option-raw'] == true) {
  12136. let textarea = OJB_safeCreateJQElement(`<textarea id="compiler_option_raw" placeholder="Raw compiler options" style="resize: vertical;"></textarea>`);
  12137. div.append(textarea);
  12138. textarea.on('input', function () {
  12139. refreshCompilerArgs();
  12140. });
  12141. }
  12142. if (Compiler['runtime-option-raw'] == true) {
  12143. let textarea = OJB_safeCreateJQElement(`<textarea id="runtime_option_raw" placeholder="Raw runtime options" style="resize: vertical;"></textarea>`);
  12144. div.append(textarea);
  12145. textarea.on('input', function () {
  12146. refreshCompilerArgs();
  12147. });
  12148. }
  12149. $("#CompilerSetting").append(div);
  12150.  
  12151. refreshCompilerArgs(); // 初始化
  12152. });
  12153.  
  12154. CompilerChange.trigger("change"); // 初始化
  12155. } else {
  12156. $('#RunTestButton').prop("disabled", true);
  12157. }
  12158. }
  12159.  
  12160. // wandbox编译器通信
  12161. async function wandboxCompiler(code, input) {
  12162. try {
  12163. const result = await OJB_promiseRetryWrapper(wandboxCompilerRequest, {
  12164. maxRetries: 5,
  12165. retryInterval: 500,
  12166. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  12167. }, code, input);
  12168. return result;
  12169. } catch (error) {
  12170. return { Errors: error.message, Result: '', Stats: '' };
  12171. }
  12172. }
  12173.  
  12174. // wandbox编译器请求方法
  12175. async function wandboxCompilerRequest(code, input) {
  12176. const data = {
  12177. code: code,
  12178. codes: [],
  12179. compiler: $('#CompilerChange').val().replace($('#compiler_option_raw').val(), '').replace($('#runtime_option_raw').val(), ''),
  12180. 'compiler-option-raw': $('#compiler_option_raw').val(),
  12181. 'runtime-option-raw': $('#runtime_option_raw').val(),
  12182. options: $("#CompilerArgsInput").val(),
  12183. description: '',
  12184. stdin: input,
  12185. title: ''
  12186. };
  12187.  
  12188. const responseDetails = await OJB_GMRequest({
  12189. method: 'POST',
  12190. url: 'https://wandbox.org/api/compile.json',
  12191. data: JSON.stringify(data),
  12192. headers: {
  12193. 'Content-Type': 'application/json'
  12194. }
  12195. });
  12196.  
  12197. if (responseDetails.status !== 200 || !responseDetails.response) {
  12198. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12199. }
  12200.  
  12201. const response = JSON.parse(responseDetails.response);
  12202. return {
  12203. Errors: response.compiler_error === "" ? response.signal : response.compiler_error,
  12204. Result: response.program_output || '',
  12205. Stats: response.status === "0" ? "Finish" : "Error"
  12206. };
  12207. }
  12208.  
  12209. // 更改编译器参数
  12210. function changeCompilerArgs(nowSelect) {
  12211. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12212. officialCompilerArgsChange(nowSelect);
  12213. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12214. rextesterCompilerArgsChange(nowSelect);
  12215. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12216. wandboxCompilerArgsChange(nowSelect);
  12217. }
  12218. }
  12219.  
  12220. // 在线编译器通信
  12221. async function onlineCompilerConnect(code, input) {
  12222. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12223. return await officialCompiler(code, input);
  12224. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12225. return await rextesterCompiler(code, input);
  12226. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12227. return await wandboxCompiler(code, input);
  12228. }
  12229. }
  12230.  
  12231. // 差异对比
  12232. function codeDiff(expectedText, actualText) {
  12233. // 将文本按行拆分
  12234. const expectedLines = expectedText ? expectedText.split('\n') : [];
  12235. const actualLines = actualText ? actualText.split('\n') : [];
  12236.  
  12237. const output = document.createElement('div');
  12238.  
  12239. const createLineElement = (lineNo, contentElement) => {
  12240. const lineDiv = document.createElement('div');
  12241. lineDiv.className = 'diffLine';
  12242.  
  12243. const lineNoDiv = document.createElement('div');
  12244. lineNoDiv.className = 'lineNo';
  12245. lineNoDiv.textContent = lineNo;
  12246.  
  12247. lineDiv.appendChild(lineNoDiv);
  12248. lineDiv.appendChild(contentElement);
  12249.  
  12250. return lineDiv;
  12251. };
  12252.  
  12253. const createContentElement = (isEquals = true, expected = null, removed = null) => {
  12254. const contentDiv = document.createElement('div');
  12255. contentDiv.className = 'lineContent';
  12256.  
  12257. if (isEquals) {
  12258. const span = document.createElement('span');
  12259. span.textContent = expected;
  12260. contentDiv.appendChild(span);
  12261. } else {
  12262. if (removed != null) {
  12263. const removedSpan = document.createElement('span');
  12264. removedSpan.className = 'removed';
  12265. removedSpan.textContent = removed;
  12266. contentDiv.appendChild(removedSpan);
  12267. }
  12268. if (expected != null) {
  12269. const addedSpan = document.createElement('span');
  12270. addedSpan.className = 'added';
  12271. addedSpan.textContent = expected;
  12272. contentDiv.appendChild(addedSpan);
  12273. }
  12274. }
  12275.  
  12276. return contentDiv;
  12277. };
  12278.  
  12279. let index = 1;
  12280.  
  12281. expectedLines.forEach((expectedLine, i) => {
  12282. const actualLine = actualLines[i];
  12283. if (actualLine === undefined) {
  12284. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine)));
  12285. } else if (expectedLine === actualLine) {
  12286. output.appendChild(createLineElement(index++, createContentElement(true, expectedLine)));
  12287. } else {
  12288. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine, actualLine)));
  12289. }
  12290. });
  12291.  
  12292. // 处理多余的 actualLines
  12293. for (let i = expectedLines.length; i < actualLines.length; i++) {
  12294. output.appendChild(createLineElement(index++, createContentElement(false, null, actualLines[i])));
  12295. }
  12296.  
  12297. return output.innerHTML;
  12298. }
  12299.  
  12300. // 内容类型常量
  12301. const TestCaseContentType = {
  12302. TERMINAL: 'terminal',
  12303. DIFF: 'diff',
  12304. NO_DIFF: 'no_diff',
  12305. SUCCESS: 'success'
  12306. };
  12307.  
  12308. // 样例测试状态类
  12309. class TestCaseStatus {
  12310. constructor(item, prefix) {
  12311. this.testCase = OJB_safeCreateJQElement(`<div class="test-case"></div>`);
  12312. this.item = item;
  12313. this.prefix = prefix;
  12314. this.titleElement = OJB_safeCreateJQElement(`<div class="test-case-title">${this.prefix} ${this.item}</div>`);
  12315. this.statusElement = OJB_safeCreateJQElement(`<div class="test-case-status"></div>`);
  12316. this.contentElement = OJB_safeCreateJQElement(`<div class="test-case-content"></div>`);
  12317. this.judgeElement = OJB_safeCreateJQElement(`<div class="test-case-judge"></div>`);
  12318. this.testCase.append(this.titleElement, this.statusElement, this.contentElement, this.judgeElement);
  12319. $('#statePanel').append(this.testCase);
  12320. this.setStatus('Queued', 'queued');
  12321. }
  12322.  
  12323. setTitle(title) {
  12324. this.titleElement.text(title);
  12325. }
  12326.  
  12327. setStatus(text, status) {
  12328. this.statusElement.text(text).removeClass('queued running success error').addClass(status);
  12329. }
  12330.  
  12331. setContent(content, type) {
  12332. // 如果内容类型为SUCCESS,隐藏内容元素并提前返回
  12333. if (type === TestCaseContentType.SUCCESS) {
  12334. this.contentElement.hide();
  12335. return;
  12336. }
  12337.  
  12338. // 根据内容类型创建内容元素
  12339. const createContentElementByType = (content, type) => {
  12340. let contentElement;
  12341. switch (type) {
  12342. case TestCaseContentType.TERMINAL:
  12343. // 为TERMINAL类型创建一个新的终端容器
  12344. contentElement = OJB_safeCreateJQElement(`<div class="terminal-container" style="overflow: auto;"></div>`);
  12345. break;
  12346. case TestCaseContentType.DIFF:
  12347. case TestCaseContentType.NO_DIFF:
  12348. // 为DIFF和NO_DIFF类型创建相应的内容元素,并添加差异说明
  12349. const className = type === TestCaseContentType.DIFF ? "output_diff" : "output_no_diff";
  12350. contentElement = OJB_safeCreateJQElement(`<pre class="${className}">${content}</pre>`);
  12351. appendDiffNote();
  12352. break;
  12353. default:
  12354. throw new Error("Unsupported content type.");
  12355. }
  12356. return contentElement;
  12357. };
  12358.  
  12359. // 初始化终端
  12360. const initializeTerminal = (content, contentElement) => {
  12361. const term = new Terminal({ rows: 10, cols: 150 });
  12362. term.setOption('theme', { background: '#2d2e2c' });
  12363. term.setOption('convertEol', true); // 将换行符\n转换为\r\n
  12364. term.write(content);
  12365. term.open(contentElement.get(0));
  12366. };
  12367.  
  12368. // 添加差异说明
  12369. const appendDiffNote = () => {
  12370. const diffNote = OJB_safeCreateJQElement(`<div class="diff_note">${i18next.t('resultBlock.diffNote', { ns: 'codeEditor' })}</div>`);
  12371. this.testCase.append(diffNote);
  12372. };
  12373.  
  12374. // 创建并追加内容元素
  12375. const contentElement = createContentElementByType(content, type);
  12376. this.contentElement.append(contentElement);
  12377.  
  12378. // 如果内容类型为TERMINAL,初始化并打开终端
  12379. if (type === TestCaseContentType.TERMINAL) {
  12380. initializeTerminal(content, contentElement);
  12381. }
  12382. }
  12383.  
  12384. setJudge(judge) {
  12385. this.judgeElement.text(judge);
  12386. }
  12387. }
  12388.  
  12389. // 样例测试函数
  12390. async function runCode(event, runButton, sourceDiv) {
  12391. event.preventDefault();
  12392. const statePanel = $('#statePanel').show().empty();
  12393. const testData = getTestData();
  12394. const customTestData = await getCustomTestData();
  12395. const totalTests = Object.keys(customTestData).length + Object.keys(testData).length;
  12396.  
  12397. let passedTests = 0;
  12398. let failedTests = 0;
  12399. let hasError = false;
  12400.  
  12401. // 定义一个对象队列,包括创建的样例块实例和对应的样例数据
  12402. const queue = [];
  12403.  
  12404. // 先生成各个样例的块,并显示排队中,将创建的各个对象存到队列中,以便后面更新
  12405. for (const [item, data] of Object.entries(customTestData)) {
  12406. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.custom', { ns: 'codeEditor' }));
  12407. queue.push({ testCase, data });
  12408. }
  12409.  
  12410. if (!$('#onlyCustomTest').prop('checked')) {
  12411. for (const [item, data] of Object.entries(testData)) {
  12412. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.sample', { ns: 'codeEditor' }));
  12413. queue.push({ testCase, data });
  12414. }
  12415. }
  12416.  
  12417. // 测试函数
  12418. const runTest = async (testCase, data, index) => {
  12419. runButton.setButtonState('running', `${index}/${totalTests}`);
  12420.  
  12421. testCase.setStatus('Running', 'running');
  12422. const result = await onlineCompilerConnect(sourceDiv.val(), data.input);
  12423.  
  12424. if (result.Errors) {
  12425. testCase.setStatus('Compilation error or Time limit', 'error');
  12426. testCase.setContent(result.Errors, TestCaseContentType.TERMINAL);
  12427. hasError = true;
  12428. } else if (result.Result.trim() === data.output.trim()) {
  12429. testCase.setStatus('Accepted', 'success');
  12430. testCase.setContent('The output is correct.', TestCaseContentType.SUCCESS);
  12431. passedTests++;
  12432. } else {
  12433. testCase.setStatus('Wrong Answer', 'error');
  12434. const diffContent = $('#DontShowDiff').prop('checked') ? result.Result.trim() : codeDiff(data.output.trim(), result.Result.trim());
  12435. const contentType = $('#DontShowDiff').prop('checked') ? TestCaseContentType.NO_DIFF : TestCaseContentType.DIFF;
  12436. testCase.setContent(diffContent, contentType);
  12437. failedTests++;
  12438. }
  12439.  
  12440. const judgeStats = `${i18next.t('resultBlock.state', { ns: 'codeEditor' })}${result.Stats}`;
  12441. testCase.setJudge(judgeStats);
  12442.  
  12443. await OJB_delay(500); // 等待500毫秒
  12444. };
  12445.  
  12446. // 对队列中的对象进行测试
  12447. for (let i = 0; i < queue.length; i++) {
  12448. const { testCase, data } = queue[i];
  12449. await runTest(testCase, data, i + 1);
  12450. }
  12451.  
  12452. // 测试完成后更新按钮状态
  12453. if (hasError) {
  12454. runButton.setButtonState('error', i18next.t('runTestButton.error', { ns: 'codeEditor' }));
  12455. } else if (failedTests > 0) {
  12456. runButton.setButtonState('error', `${passedTests}/${totalTests} ` + i18next.t('runTestButton.partial', { ns: 'codeEditor' }));
  12457. } else {
  12458. runButton.setButtonState('success', i18next.t('runTestButton.success', { ns: 'codeEditor' }));
  12459. if (OJBetter.monaco.setting.autoSubmitAfterPass) {
  12460. $('#OJBetter_SubmitForm').submit(); // 自动提交
  12461. }
  12462. }
  12463. }
  12464.  
  12465. /**
  12466. * 添加题目页代码编辑器
  12467. * @returns
  12468. */
  12469. async function addProblemPageCodeEditor() {
  12470. // if (typeof ace === 'undefined') {
  12471. // const loadingMessage = new LoadingMessage();
  12472. // loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('error.codeEditor.load', { ns: 'alert' })}`, 'error');
  12473. // return; // 因为Codeforces设定的是未登录时不能访问提交页,也不会加载ace库
  12474. // }
  12475.  
  12476. // 获取提交页链接
  12477. const href = window.location.href;
  12478. let submitUrl = OJBetter.common.hostAddress + $('.form-code-submit').attr('action');
  12479. // if (/\/problemset\//.test(href)) {
  12480. // // problemset
  12481. // submitUrl = OJBetter.common.hostAddress + '/problemset/submit';
  12482. // } else if (/\/gym\//.test(href)) {
  12483. // // gym 题目
  12484. // submitUrl = OJBetter.common.hostAddress + '/gym/' + ((href) => {
  12485. // const regex = /\/gym\/(?<num>[0-9a-zA-Z]*?)\/problem\//;
  12486. // const match = href.match(regex);
  12487. // return match && match.groups.num;
  12488. // })(href) + '/submit';
  12489. // } else if (OJBetter.typeOfPage.is_acmsguru) {
  12490. // // acmsguru 题目
  12491. // submitUrl = href.replace(/\/problemsets[A-Za-z0-9\/#]*/, "/problemsets/acmsguru/submit");
  12492. // } else {
  12493. // submitUrl = href.replace(/\/problem[A-Za-z0-9\/#]*/, "/submit");
  12494. // }
  12495.  
  12496. // // 获取提交页HTML
  12497. // let cloneHTML = await getSubmitHTML(submitUrl);
  12498.  
  12499. // 创建
  12500. // let form = await createCodeEditorForm(submitUrl, cloneHTML);
  12501. let form = await createCodeEditorForm(submitUrl);
  12502. let selectLang = form.selectLang;
  12503. let submitButton = form.submitButton;
  12504. let runButton = form.runButton;
  12505.  
  12506. // 初始化
  12507. CustomTestInit(); // 自定义测试数据面板
  12508. selectLang.val(OJBetter.monaco.compilerSelection); // 恢复上一次的语言选择
  12509.  
  12510. // 设置语言选择change事件监听器
  12511. selectLang.on('change', () => {
  12512. changeMonacoLanguage(form); // 编辑器语言切换监听
  12513. });
  12514. changeMonacoLanguage(form);
  12515.  
  12516. // 样例测试
  12517. runButton.on('click', (event) => runCode(event, runButton, form.sourceDiv, form.submitDiv))
  12518. .setHoverRedo();
  12519.  
  12520. // 提交
  12521. submitButton.on('click', async function (event) {
  12522. event.preventDefault();
  12523. if (OJBetter.monaco.setting.isCodeSubmitDoubleConfirm) {
  12524. // 获取题目名
  12525. const questionTitle = (() => {
  12526. const element = document.querySelector('.h2');
  12527. return Array.from(element.childNodes)
  12528. .filter(node => node.nodeType === Node.TEXT_NODE)
  12529. .map(textNode => textNode.textContent.trim())
  12530. .join(' ');
  12531. })();
  12532. const submit = await OJB_createDialog(
  12533. i18next.t('submitCode.title', { ns: 'dialog' }),
  12534. i18next.t('submitCode.content', { ns: 'dialog', questionTitle: questionTitle }),
  12535. [
  12536. i18next.t('submitCode.buttons.0', { ns: 'dialog' }),
  12537. i18next.t('submitCode.buttons.1', { ns: 'dialog' })
  12538. ],
  12539. true
  12540. ); //提交确认
  12541. if (submit) {
  12542. submitButton.after(`<img class="OJBetter_loding" src="//codeforces.org/s/84141/images/ajax-loading-24x24.gif">`);
  12543. $('#OJBetter_SubmitForm').submit();
  12544. } else {
  12545. submitButton.addClass('disabled');
  12546. setTimeout(function () {
  12547. submitButton.removeClass('disabled');
  12548. }, 300);
  12549. }
  12550. } else {
  12551. $('#OJBetter_SubmitForm').submit();
  12552. }
  12553. });
  12554. }
  12555.  
  12556. /**
  12557. * 获取翻译服务目标语言的对应代码
  12558. * @param {string} serverName 服务名称
  12559. * @returns {string} 目标语言,如果没有对应代码则返回中文
  12560. */
  12561. function getTargetLanguage(serverName) {
  12562. let targetLanguage = OJBetter.supportList.translationSupport[serverName][OJBetter.translation.targetLang];
  12563. if (targetLanguage) return targetLanguage;
  12564. else return OJBetter.supportList.translationSupport[serverName]['zh'];
  12565. }
  12566.  
  12567. /**
  12568. * 将文本中Markdown格式的加粗**转换成HTML格式。
  12569. * @param {string} text 文本
  12570. * @returns {string} 替换后的字符串
  12571. */
  12572. function convertBoldMarkdownToHTML(text) {
  12573. return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
  12574. }
  12575.  
  12576. /**
  12577. * 将文本中Markdown格式的链接文本转换成HTML格式。
  12578. * @param {string} text 文本
  12579. * @returns {string} 替换后的字符串
  12580. */
  12581. function convertLinksMarkdownToHTML(text) {
  12582. return text.replace(/(?<!!)\[(.*?)\]\(([^"]*?)("(.*?)")*\)/g, '<a href="$2" title="$4">$1</a>');
  12583. }
  12584.  
  12585. /**
  12586. * 将HTML格式的加粗文本转换回Markdown格式。
  12587. * @param {string} text 文本
  12588. * @returns {string} 替换后的字符串
  12589. */
  12590. function convertBoldHTMLToMarkdown(text) {
  12591. return text.replace(/<strong>(.*?)<\/strong>/g, '**$1**');
  12592. }
  12593.  
  12594. /**
  12595. * 将HTML格式的链接文本转换回Markdown格式。
  12596. * @param {string} html - 包含HTML链接标签<a>的字符串。
  12597. * @returns {string} 转换后的字符串,其中HTML链接标签被替换为Markdown的链接语法。
  12598. */
  12599. function convertLinksHTMLToMarkdown(html) {
  12600. return html.replace(/<a href="([^"]*)"( title="([^"]*)")*>([^<]+)<\/a>/g, '[$4]($1 "$3")');
  12601. }
  12602.  
  12603. /**
  12604. * DeepL翻译
  12605. * @param {string} raw 原文
  12606. * @returns {Promise<TransRawData>} 翻译结果对象
  12607. */
  12608. async function translate_deepl(raw) {
  12609. const id = (Math.floor(Math.random() * 99999) + 100000) * 1000;
  12610. const data = {
  12611. jsonrpc: '2.0',
  12612. method: 'LMT_handle_texts',
  12613. id,
  12614. params: {
  12615. splitting: 'newlines',
  12616. lang: {
  12617. source_lang_user_selected: 'auto',
  12618. target_lang: getTargetLanguage('deepl'),
  12619. },
  12620. texts: [{
  12621. text: raw,
  12622. requestAlternatives: 3
  12623. }],
  12624. timestamp: getTimeStamp(raw.split('i').length - 1)
  12625. }
  12626. }
  12627. let postData = JSON.stringify(data);
  12628. if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
  12629. postData = postData.replace('"method":"', '"method" : "');
  12630. } else {
  12631. postData = postData.replace('"method":"', '"method": "');
  12632. }
  12633. const options = {
  12634. method: 'POST',
  12635. url: 'https://www2.deepl.com/jsonrpc',
  12636. data: postData,
  12637. headers: {
  12638. 'Content-Type': 'application/json',
  12639. 'Host': 'www2.deepl.com',
  12640. 'Origin': 'https://www.deepl.com',
  12641. 'Referer': 'https://www.deepl.com/',
  12642. },
  12643. anonymous: true,
  12644. nocache: true,
  12645. }
  12646.  
  12647. return await BaseTranslate(options, res => JSON.parse(res)?.result?.texts?.[0]?.text || res, res => {
  12648. const resObj = {
  12649. status: true,
  12650. message: 'ok'
  12651. };
  12652. if (res.includes('"message":"Too many requests"')) {
  12653. resObj.status = false;
  12654. resObj.message = i18next.t('error.deepl429', { ns: 'translator' }); // Too many requests 提示
  12655. return resObj;
  12656. };
  12657. return resObj;
  12658. });
  12659. }
  12660.  
  12661. /**
  12662. * 使用 DeepL Free API 进行翻译
  12663. * @param {string} raw 原文
  12664. * @returns {Promise<TransRawData>} 翻译结果对象
  12665. */
  12666. async function translate_deepl_api_free(raw) {
  12667. const data = JSON.stringify({
  12668. text: [raw],
  12669. target_lang: getTargetLanguage('deepl'),
  12670. split_sentences: '1',
  12671. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12672. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12673. });
  12674.  
  12675. const options = {
  12676. method: "POST",
  12677. url: OJBetter.deepl.config.proxy || "https://api-free.deepl.com/v2/translate",
  12678. headers: {
  12679. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12680. "Content-Type": "application/json",
  12681. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12682. },
  12683. data: data,
  12684. onload: response => response.responseText,
  12685. onerror: error => console.error(error)
  12686. };
  12687.  
  12688. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  12689. }
  12690.  
  12691. /**
  12692. * 使用 DeepL Pro API 进行翻译
  12693. * @param {string} raw 原文
  12694. * @returns {Promise<TransRawData>} 翻译结果对象
  12695. */
  12696. async function translate_deepl_api_pro(raw) {
  12697. const data = JSON.stringify({
  12698. text: [raw],
  12699. target_lang: getTargetLanguage('deepl'),
  12700. split_sentences: '1',
  12701. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12702. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12703. });
  12704.  
  12705. const options = {
  12706. method: "POST",
  12707. url: OJBetter.deepl.config.proxy || "https://api.deepl.com/v2/translate",
  12708. headers: {
  12709. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12710. "Content-Type": "application/json",
  12711. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12712. },
  12713. data: data,
  12714. onload: response => response.responseText,
  12715. onerror: error => console.error(error)
  12716. };
  12717.  
  12718. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  12719. }
  12720.  
  12721. /**
  12722. * 使用 DeepLX 进行翻译
  12723. * @param {String} text 原文
  12724. * @returns {Promise<TransRawData>} 翻译结果对象
  12725. */
  12726. async function translate_deeplx(text) {
  12727. const options = {
  12728. method: "POST",
  12729. url: OJBetter.deepl.config.proxy || 'https://api.deeplx.org/translate',
  12730. data: JSON.stringify({
  12731. "text": text,
  12732. "source_lang": "EN",
  12733. "target_lang": getTargetLanguage('deepl'),
  12734. }),
  12735. headers: {
  12736. 'Content-Type': 'application/json',
  12737. ...(OJBetter.deepl.config.key ? { Authorization: `Bearer ${OJBetter.deepl.config.key}` } : {})
  12738. },
  12739. responseType: "json",
  12740. };
  12741.  
  12742. return await BaseTranslate(options, res => {
  12743. const parsedResponse = JSON.parse(res);
  12744. if (parsedResponse.code === 200 && parsedResponse.data) {
  12745. return parsedResponse.data;
  12746. } else {
  12747. throw new Error('Translation failed or invalid response format.');
  12748. }
  12749. });
  12750. }
  12751.  
  12752. function getTimeStamp(iCount) {
  12753. const ts = Date.now();
  12754. if (iCount !== 0) {
  12755. iCount = iCount + 1;
  12756. return ts - (ts % iCount) + iCount;
  12757. } else {
  12758. return ts;
  12759. }
  12760. }
  12761.  
  12762. /**
  12763. * 讯飞听见翻译
  12764. * @param {String} text 要翻译的文本
  12765. * @returns {Promise<TransRawData>} 翻译结果对象
  12766. */
  12767. async function translate_iflyrec(text) {
  12768. const options = {
  12769. method: "POST",
  12770. url: 'https://www.iflyrec.com/TranslationService/v1/textTranslation',
  12771. data: JSON.stringify({
  12772. "from": "2",
  12773. "to": getTargetLanguage('iflyrec'),
  12774. "contents": [{
  12775. "text": text,
  12776. "frontBlankLine": 0
  12777. }]
  12778. }),
  12779. anonymous: true,
  12780. headers: {
  12781. 'Content-Type': 'application/json',
  12782. 'Origin': 'https://www.iflyrec.com',
  12783. },
  12784. responseType: "json",
  12785. };
  12786. return await BaseTranslate(options, res => JSON.parse(res).biz[0].translateResult.replace(/\\n/g, "\n\n"));
  12787. }
  12788.  
  12789. /**
  12790. * 有道翻译
  12791. * @param {string} raw 原文
  12792. * @returns {Promise<TransRawData>} 翻译结果对象
  12793. */
  12794. async function translate_youdao_mobile(raw) {
  12795. /**
  12796. * 生成cookie
  12797. */
  12798. const getcookie = (() => {
  12799. // 生成IP地址
  12800. const generateIP = () => {
  12801. return `${OJB_getRandomNumberInRange(1, 255)}.${OJB_getRandomNumberInRange(1, 255)}.${OJB_getRandomNumberInRange(1, 255)}.${OJB_getRandomNumberInRange(1, 255)}`;
  12802. }
  12803. // 生成OUTFOX_SEARCH_USER_ID_NCOO的值
  12804. const OUTFOX_SEARCH_USER_ID_NCOO = `${OJB_getRandomNumberInRange(100000000, 999999999)}.${OJB_getRandomNumberInRange(100000000, 999999999)}`;
  12805. // 生成OUTFOX_SEARCH_USER_ID的值
  12806. const OUTFOX_SEARCH_USER_ID = `${OJB_getRandomNumberInRange(100000000, 999999999)}@${generateIP()}`;
  12807. return `OUTFOX_SEARCH_USER_ID_NCOO=${OUTFOX_SEARCH_USER_ID_NCOO}; OUTFOX_SEARCH_USER_ID=${OUTFOX_SEARCH_USER_ID}`;
  12808. })();
  12809.  
  12810. /**
  12811. * 生成随机时间戳
  12812. */
  12813. const gettime = (new Date()).getTime();
  12814.  
  12815. /**
  12816. * 生成sign
  12817. */
  12818. const getsign = (() => {
  12819. const d = "fanyideskweb";
  12820. const u = "webfanyi";
  12821. const t = "fsdsogkndfokasodnaso";
  12822. function A(e) {
  12823. return CryptoJS.MD5(e.toString()).toString(CryptoJS.enc.Hex);
  12824. }
  12825. function w(e) {
  12826. return A(`client=${d}&mysticTime=${e}&product=${u}&key=${t}`);
  12827. }
  12828. return w(gettime);
  12829. })();
  12830.  
  12831. /**
  12832. * 解码方法
  12833. * @param {string} src 待解码的字符串
  12834. * @returns {Object} 解码后的数据
  12835. */
  12836. const decode = function (src) {
  12837. // 解码URL安全的Base64
  12838. const decodeUrlSafeBase64 = (str) => {
  12839. let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
  12840. return base64;
  12841. }
  12842.  
  12843. // 使用MD5生成key和iv,取前16字节
  12844. const key = "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl";
  12845. const iv = "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4";
  12846. const keyHash = CryptoJS.MD5(key).toString();
  12847. const ivHash = CryptoJS.MD5(iv).toString();
  12848.  
  12849. // 使用AES-128-CBC模式进行解密
  12850. const keyForAES = CryptoJS.enc.Hex.parse(keyHash).toString().substring(0, 32);
  12851. const ivForAES = CryptoJS.enc.Hex.parse(ivHash).toString().substring(0, 32);
  12852.  
  12853. // 解码URL安全的Base64
  12854. const decodedBase64 = decodeUrlSafeBase64(src);
  12855.  
  12856. // 解密
  12857. const decrypted = CryptoJS.AES.decrypt({
  12858. ciphertext: CryptoJS.enc.Base64.parse(decodedBase64)
  12859. }, CryptoJS.enc.Hex.parse(keyForAES), {
  12860. iv: CryptoJS.enc.Hex.parse(ivForAES),
  12861. mode: CryptoJS.mode.CBC,
  12862. padding: CryptoJS.pad.Pkcs7
  12863. });
  12864.  
  12865. // 将解密结果转换为Utf8字符串
  12866. const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
  12867.  
  12868. // 处理JSON字符串并解析
  12869. const jsonStr = decryptedStr.substring(0, decryptedStr.lastIndexOf("}") + 1);
  12870. return JSON.parse(jsonStr);
  12871. }
  12872. // 整理数据
  12873. const organizeTranslation = (data) => {
  12874. // 提取translateResult数组
  12875. const { translateResult } = data;
  12876.  
  12877. // 整理tgt字段
  12878. return translateResult
  12879. .flat()
  12880. .map(item => item.tgt)
  12881. .join('');
  12882. };
  12883. // 表单数据
  12884. const data = {
  12885. "i": raw,
  12886. "from": "auto",
  12887. "to": getTargetLanguage('youdao'),
  12888. "dictResult": "true",
  12889. "keyid": "webfanyi",
  12890. "sign": getsign,
  12891. "client": "fanyideskweb",
  12892. "product": "webfanyi",
  12893. "appVersion": "1.0.0",
  12894. "vendor": "web",
  12895. "pointParam": "client,mysticTime,product",
  12896. "mysticTime": gettime,
  12897. "keyfrom": "fanyi.web",
  12898. "mid": "1",
  12899. "screen": "1",
  12900. "model": "1",
  12901. "network": "wifi",
  12902. "abtest": "0",
  12903. "yduuid": "abcdefg"
  12904. };
  12905. const options = {
  12906. method: "POST",
  12907. url: 'https://dict.youdao.com/webtranslate',
  12908. data: new URLSearchParams(data),
  12909. anonymous: true,
  12910. cookie: getcookie,
  12911. headers: {
  12912. "Content-Type": "application/x-www-form-urlencoded",
  12913. 'Referer': 'https://fanyi.youdao.com/',
  12914. }
  12915. }
  12916. return await BaseTranslate(options,
  12917. res => {
  12918. const decodeData = decode(res)
  12919. const result = organizeTranslation(decodeData);
  12920. return result;
  12921. }
  12922. );
  12923. }
  12924.  
  12925. /**
  12926. * google翻译
  12927. * @param {string} raw 原文
  12928. * @returns {Promise<TransRawData>} 翻译结果对象
  12929. */
  12930. async function translate_gg(raw) {
  12931. const params = `tl=${getTargetLanguage('google')}&q=${encodeURIComponent(raw)}`;
  12932. const options = {
  12933. method: "GET",
  12934. url: `https://translate.google.com/m?${params}`,
  12935. }
  12936. return await BaseTranslate(options,
  12937. res => $(res).filter('.result-container').text() || $(res).find('.result-container').text());
  12938. }
  12939.  
  12940. /**
  12941. * 彩云翻译
  12942. * @param {string} raw 原文
  12943. * @returns {Promise<TransRawData>} 翻译结果对象
  12944. */
  12945. async function translate_caiyun(raw) {
  12946. const source = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
  12947. const dic = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"].reduce((dic, current, index) => { dic[current] = source[index]; return dic }, {});
  12948. const browser_id = CryptoJS.MD5(Math.random().toString()).toString();
  12949. const caiyun_jwt = await (async () => {
  12950. const options = {
  12951. method: "POST",
  12952. url: 'https://api.interpreter.caiyunai.com/v1/user/jwt/generate',
  12953. headers: {
  12954. "content-type": "application/json",
  12955. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  12956. "origin": "https://fanyi.caiyunapp.com",
  12957. },
  12958. data: JSON.stringify({ browser_id }),
  12959. }
  12960. const res = await OJB_GMRequest(options);
  12961. return JSON.parse(res.responseText).jwt;
  12962. })();
  12963.  
  12964. // 解码
  12965. const decodeUnicode = str => {
  12966. const decoder = new TextDecoder();
  12967. const data = Uint8Array.from(atob(str), c => c.charCodeAt(0));
  12968. return decoder.decode(data);
  12969. };
  12970. const decoder = line => decodeUnicode([...line].map(i => dic[i] || i).join(""));
  12971.  
  12972. const options = {
  12973. method: "POST",
  12974. url: 'https://api.interpreter.caiyunai.com/v1/translator',
  12975. data: JSON.stringify({
  12976. "source": raw.split('\n'),
  12977. "browser_id": browser_id,
  12978. "trans_type": getTargetLanguage('caiyun'),
  12979. "request_id": "web_fanyi",
  12980. "media": "text",
  12981. "os_type": "web",
  12982. "dict": true,
  12983. "cached": true,
  12984. "replaced": true,
  12985. "style": "formal",
  12986. "model": "",
  12987. "detect": true,
  12988. }),
  12989. headers: {
  12990. "content-type": "application/json;charset=UTF-8",
  12991. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  12992. "t-authorization": caiyun_jwt
  12993. }
  12994. }
  12995. return await BaseTranslate(options, res => JSON.parse(res).target.map(decoder).join('\n'))
  12996. }
  12997.  
  12998. /**
  12999. * ChatGPT
  13000. * @param {string} raw 原文
  13001. * @returns {Promise<TransRawData>} 翻译结果对象
  13002. */
  13003. async function translate_openai(raw) {
  13004. const modelDefault = 'gpt-3.5-turbo';
  13005. const lang = getTargetLanguage('openai');
  13006. let prompt = "";
  13007. if (OJBetter.chatgpt.customPrompt) {
  13008. prompt = `\n${OJBetter.chatgpt.customPrompt}`;
  13009. if (!OJBetter.chatgpt.asSystemPrompt) {
  13010. prompt += `\n${raw}`;
  13011. };
  13012. } else {
  13013. prompt = `
  13014. As a professional English translator, your task is to accurately translate a segment of an algorithm programming competition question into ${lang}.
  13015. The translation should use professional terms and maintain the text format, including ${OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru
  13016. ? "keeping the LaTeX equations unchanged."
  13017. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13018. }
  13019. After translation, please ensure that the ${lang} version conforms to normal expression habits.
  13020. What I need is a carefully polished ${lang} translation of my question segment. ${OJBetter.chatgpt.asSystemPrompt ? '' :
  13021. `The segment to be translated is as follows: "
  13022. ${raw}
  13023. "`}`;
  13024. };
  13025. const data = {
  13026. model: OJBetter.chatgpt.config.model || modelDefault,
  13027. messages: OJBetter.chatgpt.asSystemPrompt ?
  13028. [
  13029. {
  13030. role: "system",
  13031. content: prompt
  13032. },
  13033. {
  13034. role: "user",
  13035. content: raw
  13036. }
  13037. ] :
  13038. [
  13039. {
  13040. role: "user",
  13041. content: prompt
  13042. }
  13043. ],
  13044. temperature: 0.7,
  13045. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13046. };
  13047. const options = {
  13048. method: "POST",
  13049. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13050. data: JSON.stringify(data),
  13051. responseType: 'json',
  13052. headers: {
  13053. 'Content-Type': 'application/json',
  13054. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13055. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13056. }
  13057. }
  13058. return await BaseTranslate(options,
  13059. res => res,
  13060. undefined,
  13061. response => response.response.choices[0].message.content);
  13062. }
  13063.  
  13064. /**
  13065. * ChatGPT 流式传输
  13066. * @param {string} raw 原文
  13067. * @param {TranslateDiv} translateDiv 翻译结果面板
  13068. * @returns {Promise<TransRawData>} 翻译结果对象
  13069. */
  13070. async function translate_openai_stream(raw, translateDiv) {
  13071. const result = {
  13072. done: true,
  13073. checkPassed: null,
  13074. response: null,
  13075. responseText: null,
  13076. text: "",
  13077. error: null,
  13078. message: null
  13079. };
  13080. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13081. try {
  13082. for await (const delta of openai_stream(raw)) {
  13083. result.text += delta;
  13084. // 翻译结果面板更新
  13085. translateDiv.updateTranslateDiv(result.text, !(OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru), false);
  13086. }
  13087. return result;
  13088. } catch (err) {
  13089. console.warn(err);
  13090. result.error = {
  13091. message: err.message || null,
  13092. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13093. enumerable: err,
  13094. source: 'openai_stream'
  13095. };
  13096. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13097. }
  13098.  
  13099. return result;
  13100. }
  13101.  
  13102. /**
  13103. * 流式传输
  13104. * @param {string} raw 原文
  13105. * @returns {AsyncGenerator<string>} 返回 AsyncGenerator
  13106. */
  13107. async function* openai_stream(raw) {
  13108. const modelDefault = 'gpt-3.5-turbo';
  13109. const lang = getTargetLanguage('openai');
  13110. let prompt = "";
  13111. if (OJBetter.chatgpt.customPrompt) {
  13112. prompt = `\n${OJBetter.chatgpt.customPrompt}`;
  13113. if (!OJBetter.chatgpt.asSystemPrompt) {
  13114. prompt += `\n${raw}`;
  13115. };
  13116. } else {
  13117. prompt = `
  13118. As a professional English translator, your task is to accurately translate a segment of an algorithm programming competition question into ${lang}.
  13119. The translation should use professional terms and maintain the text format, including ${OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru
  13120. ? "keeping the LaTeX equations unchanged."
  13121. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13122. }
  13123. After translation, please ensure that the ${lang} version conforms to normal expression habits.
  13124. What I need is a carefully polished ${lang} translation of my question segment. ${OJBetter.chatgpt.asSystemPrompt ? '' :
  13125. `The segment to be translated is as follows: "
  13126. ${raw}
  13127. "`}`;
  13128. };
  13129. const data = {
  13130. model: OJBetter.chatgpt.config.model || modelDefault,
  13131. messages: OJBetter.chatgpt.asSystemPrompt ?
  13132. [
  13133. {
  13134. role: "system",
  13135. content: prompt
  13136. },
  13137. {
  13138. role: "user",
  13139. content: raw
  13140. }
  13141. ] :
  13142. [
  13143. {
  13144. role: "user",
  13145. content: prompt
  13146. }
  13147. ],
  13148. temperature: 0.7,
  13149. stream: true,
  13150. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13151. };
  13152. const options = {
  13153. method: "POST",
  13154. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13155. data: JSON.stringify(data),
  13156. responseType: 'stream',
  13157. headers: {
  13158. 'Content-Type': 'application/json',
  13159. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13160. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13161. }
  13162. }
  13163. const response = await OJB_GMRequest(options, true);
  13164. const reader = response.response.getReader();
  13165. const decoder = new TextDecoder();
  13166. let buffer = ''; // 用于累积数据片段的缓冲区
  13167.  
  13168. while (true) {
  13169. const { done, value } = await reader.read();
  13170. if (done) break;
  13171. buffer += decoder.decode(value, { stream: true }); // 将新的数据片段追加到缓冲区
  13172. let lines = buffer.split("\n\n"); // 处理累积的数据
  13173.  
  13174. // 缓冲区的最后一行可能还未完整接收,保留在缓冲区中,-1
  13175. for (let i = 0; i < lines.length - 1; i++) {
  13176. let line = lines[i];
  13177. line = line.substring(5); // 移除 'data:' 前缀
  13178. if (line.includes('[DONE]')) {
  13179. return; // End
  13180. }
  13181. try {
  13182. let data = JSON.parse(line);
  13183. let delta = data['choices'][0]['delta'];
  13184. let content = delta['content'] ? delta['content'] : "";
  13185. yield content; // 传递数据给调用者
  13186. } catch (error) {
  13187. console.warn(`Error parsing JSON: ${error}\n\nError data: ${line}`);
  13188. }
  13189. }
  13190.  
  13191. // 保留最后一行在缓冲区中
  13192. buffer = lines.slice(-1);
  13193. }
  13194.  
  13195. return buffer;
  13196. }
  13197.  
  13198. /**
  13199. * @typedef {Object} CheckResponseResult
  13200. * @property {boolean} status 检查是否通过
  13201. * @property {string} message 检查失败时的消息
  13202. */
  13203.  
  13204. /**
  13205. * @typedef {Object} ErrorResponse
  13206. * @property {Object} message 错误消息
  13207. * @property {Object} stack 错误堆栈
  13208. * @property {Object} enumerable 可枚举的错误属性
  13209. * @property {string} source 错误来源
  13210. */
  13211.  
  13212. /**
  13213. * @typedef {Object} TransRawData
  13214. * @property {boolean} done 操作是否完成
  13215. * @property {CheckResponseResult|null} checkPassed 检查是否通过的结果
  13216. * @property {Object|null} response 响应对象
  13217. * @property {string|null} text 处理后的文本
  13218. * @property {ErrorResponse} error 错误列表
  13219. * @property {string|null} message 可能的消息
  13220. */
  13221.  
  13222. /**
  13223. * 通用翻译函数
  13224. * @param {Object} options GM_xmlhttpRequest 的参数
  13225. * @param {Function} processer 响应再处理函数,它接收响应文本,并应返回处理后的文本。
  13226. * @param {Function} checkResponse 检查文本是否符合预期的函数,它接收文本,并返回一个Object,包含状态和信息。默认为返回 { status: true, message: 'ok' }
  13227. * @param {Function} getResponseText 重写响应文本获取函数,它接收response,并返回响应文本。 默认为 response.responseText
  13228. * @returns {Promise<TransRawData>} 返回 Promise,其解析值为翻译结果对象
  13229. */
  13230. async function BaseTranslate(options, processer, checkResponse = () => { return { status: true, message: 'ok' } }, getResponseText = (response) => response.responseText) {
  13231. const result = {
  13232. done: false,
  13233. checkPassed: null,
  13234. response: null,
  13235. responseText: null,
  13236. text: "",
  13237. error: null,
  13238. message: null
  13239. };
  13240. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13241. const toDo = async () => {
  13242. try {
  13243. result.response = await OJB_GMRequest(options);
  13244. result.responseText = result.response.responseText;
  13245. result.text = getResponseText(result.response);
  13246. } catch (err) {
  13247. console.warn(err);
  13248. result.error = {
  13249. message: err.message || null,
  13250. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13251. enumerable: err,
  13252. source: 'GMRequest'
  13253. };
  13254. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13255. throw result;
  13256. }
  13257. try {
  13258. result.text = processer(result.text);
  13259. } catch (err) {
  13260. console.warn(err);
  13261. result.error = {
  13262. message: err.message || null,
  13263. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13264. enumerable: err,
  13265. source: 'processer'
  13266. };
  13267. result.message = `${i18next.t('error.processer', { ns: 'translator' })}${helpText}`;
  13268. throw result;
  13269. }
  13270. try {
  13271. result.checkPassed = checkResponse(result.text);
  13272. if (result.checkPassed.status) result.done = true;
  13273. else result.message = result.checkPassed.message;
  13274. return result;
  13275. } catch (err) {
  13276. console.warn(err);
  13277. result.error = {
  13278. message: err.message || null,
  13279. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13280. enumerable: err,
  13281. source: 'checkResponse'
  13282. };
  13283. result.message = `${i18next.t('error.checkResponse', { ns: 'translator' })}${helpText}`;
  13284. throw result;
  13285. }
  13286. };
  13287.  
  13288. return await OJB_promiseRetryWrapper(toDo, {
  13289. maxRetries: 3,
  13290. errorHandler: (err, maxRetries, attemptsLeft) => {
  13291. const detailedError = {
  13292. maxRetries: maxRetries,
  13293. attemptsLeft: attemptsLeft,
  13294. ...err
  13295. };
  13296. return detailedError;
  13297. }
  13298. });
  13299. }
  13300.  
  13301. /**
  13302. * 查询服务余额
  13303. * @param {Object} quotaConfig - 配额配置对象
  13304. * @returns {Promise} 返回包含余额信息的 Promise
  13305. */
  13306. async function queryServerBalance(quotaConfig) {
  13307. // 确保传入了有效的配置对象
  13308. if (!quotaConfig || !quotaConfig.url) {
  13309. return Promise.reject(new Error('Quota configuration is missing.'));
  13310. }
  13311.  
  13312. // 准备请求选项
  13313. const requestOptions = {
  13314. method: quotaConfig.method || 'GET',
  13315. url: quotaConfig.url,
  13316. headers: {
  13317. ...Object.assign({}, ...quotaConfig.header)
  13318. },
  13319. data: JSON.stringify({ ...Object.assign({}, ...quotaConfig.data) })
  13320. };
  13321.  
  13322. // 发送请求并返回 Promise
  13323. return OJB_GMRequest(requestOptions).then(response => {
  13324. try {
  13325. const responseData = JSON.parse(response.responseText);
  13326. // 从响应数据中提取余额
  13327. const surplusPath = quotaConfig.surplus;
  13328. const surplusValue = OJB_evaluatePathOrExpression(responseData, surplusPath);
  13329. return surplusValue;
  13330. } catch (error) {
  13331. return Promise.reject(new Error('Failed to parse balance response.'));
  13332. }
  13333. }).catch(error => {
  13334. console.warn('Error querying balance:', error);
  13335. return Promise.reject(error);
  13336. });
  13337. }
  13338.  
  13339. /**
  13340. * 确认 jQuery 已加载
  13341. * @param {number} retryDelay 重试延迟(毫秒)
  13342. * @returns {Promise<void>}
  13343. */
  13344. async function ensureJQueryIsLoaded(retryDelay = 50) {
  13345. while (typeof jQuery === 'undefined') {
  13346. console.warn(`JQuery is not loaded. Retry after ${retryDelay} ms.`);
  13347. await OJB_delay(retryDelay);
  13348. retryDelay = Math.min(retryDelay * 2, 2000);
  13349. }
  13350. }
  13351.  
  13352. /**
  13353. * 加载必须的函数
  13354. * @returns {Promise} 加载提示信息
  13355. */
  13356. async function loadRequiredFunctions() {
  13357. await initVar();// 初始化全局变量
  13358. return Promise.allSettled([
  13359. initDB(), // 连接数据库
  13360. initI18next(), // i18next初始化
  13361. initButtonFunc(), // 加载按钮相关函数
  13362. initHTML2MarkDown(), // 初始化html2markdown转换器
  13363. checkScriptVersion(), // 更新检查
  13364. // ...(OJBetter.typeOfPage.is_acmsguru ? [acmsguruReblock()] : []) // 为acmsguru题面重新划分div
  13365. ]);
  13366. }
  13367.  
  13368. /**
  13369. * DOM加载后即可执行
  13370. */
  13371. function initOnDOMReady() {
  13372. showAnnounce(); // 显示公告
  13373. showWarnMessage(); // 显示警告消息
  13374. initSettingsPanel(); // 加载设置按钮面板
  13375. initMonacoEditor(); // 初始化monaco编辑器资源
  13376. localizeWebsite(); // 网站本地化替换
  13377. addDependencyStyles(); // 添加一些依赖库的样式
  13378. addI18nStyles(); // 添加包含i18n内容的样式
  13379. // if (OJBetter.basic.expandFoldingblocks) ExpandFoldingblocks(); // 折叠块展开
  13380. // if (OJBetter.basic.renderPerfOpt) RenderPerfOpt(); // 折叠块渲染优化
  13381. // if (OJBetter.basic.selectElementPerfOpt) SelectElementPerfOpt(); // 下拉选择框性能优化
  13382. if (OJBetter.typeOfPage.is_problem) {
  13383. const problemPageLinkbar = new ProblemPageLinkbar(); // 创建题目页相关链接栏
  13384. if (OJBetter.basic.showCF2vjudge) CF2vjudge(problemPageLinkbar); // 跳转到Vjudge按钮
  13385. if (OJBetter.basic.showJumpToLuogu) CF2luogu(problemPageLinkbar); // 跳转到洛谷按钮
  13386. if (OJBetter.clist.enabled.problem) showRatingByClist_problem(problemPageLinkbar); // problem页显示Rating
  13387. }
  13388. if (OJBetter.typeOfPage.is_contest) {
  13389. if (OJBetter.clist.enabled.contest) showRatingByClist_contest(); // contest页显示Rating
  13390. }
  13391. // if (OJBetter.typeOfPage.is_problemset) {
  13392. // if (OJBetter.clist.enabled.problemset) showRatingByClist_problemset(); // problemset页显示Rating
  13393. // }
  13394. if (OJBetter.typeOfPage.is_problem && OJBetter.monaco.enableOnProblemPage) {
  13395. addProblemPageCodeEditor(); // 添加题目页代码编辑器
  13396. }
  13397. }
  13398.  
  13399. /**
  13400. * 需要在页面资源完全加载后执行的函数
  13401. */
  13402. function onResourcesReady(loadingMessage) {
  13403. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadFunc', { ns: 'alert' })}`);
  13404. initializeInParallel(loadingMessage);
  13405. initializeSequentially(loadingMessage);
  13406. }
  13407.  
  13408. /**
  13409. * 可以异步并行的函数
  13410. */
  13411. function initializeInParallel(loadingMessage) {
  13412. if (OJBetter.basic.darkMode == "dark") darkModeStyleAdjustment(); // 黑暗模式额外的处理事件
  13413. // if (OJBetter.basic.commentPaging) CommentPagination(); // 评论区分页
  13414. if (OJBetter.translation.comment.transMode == "2") multiChoiceTranslation(); // 选段翻译支持
  13415. if (OJBetter.monaco.beautifyPreBlocks) beautifyPreBlocksWithMonaco(); // 美化Pre代码块
  13416. }
  13417.  
  13418. /**
  13419. * 必须按序执行的函数
  13420. */
  13421. async function initializeSequentially(loadingMessage) {
  13422. await addConversionButton(); // 添加MD/复制/翻译按钮
  13423. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  13424. await initTransResultsRecover(); // 翻译结果恢复功能初始化
  13425. }
  13426. if (OJBetter.translation.auto.enabled) {
  13427. await initTransWhenViewable(); // 自动翻译
  13428. }
  13429. // if (OJBetter.basic.standingsRecolor && OJBetter.typeOfPage.is_cfStandings) {
  13430. // await recolorStandings(); // cf赛制榜单重新着色
  13431. // }
  13432. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadSuccess', { ns: 'alert' })}`, 'success', 3000);
  13433. }
  13434.  
  13435. /**
  13436. * 主方法
  13437. */
  13438. async function main() {
  13439. await ensureJQueryIsLoaded(); // 等待jQuery加载
  13440. const loadingMessage = new LoadingMessage();
  13441. await loadRequiredFunctions(); // 加载必须的函数
  13442. initOnDOMReady(); // DOM加载后即可执行的函数
  13443. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('onload', { ns: 'alert' })}`);
  13444.  
  13445. // 检查页面资源是否已经完全加载
  13446. if (OJBetter.state.notWaiteLoaded) {
  13447. onResourcesReady(loadingMessage);
  13448. } else {
  13449. if (document.readyState === 'complete') {
  13450. onResourcesReady(loadingMessage);
  13451. } else {
  13452. window.addEventListener('load', () => onResourcesReady(loadingMessage));
  13453. }
  13454. }
  13455. };
  13456.  
  13457. // ------------------------------
  13458. // 脚本加载入口
  13459. if (document.readyState === 'loading') {
  13460. document.addEventListener("DOMContentLoaded", main);
  13461. } else {
  13462. main(); // 如果DOMContentLoaded已经触发,立即执行
  13463. }
  13464. // ------------------------------
  13465.  
  13466. // ------------------------------
  13467. // 配置自动迁移代码(将在10个小版本后移除-1.19)
  13468. // ------------------------------
  13469.  
  13470. if (GM_getValue("openai_key") || GM_getValue("api2d_key")) {
  13471. const newConfig = { "choice": -1, "configurations": [] };
  13472. if (GM_getValue("openai_key")) {
  13473. let config1 = {
  13474. "note": "我的配置1",
  13475. "model": GM_getValue("openai_model"),
  13476. "key": GM_getValue("openai_key"),
  13477. "proxy": GM_getValue("openai_proxy"),
  13478. "_header": "",
  13479. "_data": ""
  13480. }
  13481. if (GM_getValue("translation") === "openai") newConfig.choice = 0;
  13482. newConfig.configurations.push(config1);
  13483. }
  13484. if (GM_getValue("api2d_key")) {
  13485. let config2 = {
  13486. "note": "api2d",
  13487. "model": GM_getValue("api2d_model"),
  13488. "key": GM_getValue("api2d_key"),
  13489. "proxy": GM_getValue("api2d_request_entry") + '/v1/chat/completions',
  13490. "_header": GM_getValue("x_api2d_no_cache") ? "" : " x-api2d-no-cache : 1",
  13491. "_data": ""
  13492. }
  13493. if (GM_getValue("translation") === "api2d") {
  13494. if (GM_getValue("openai_key")) newConfig.choice = 1;
  13495. else newConfig.choice = 0;
  13496. }
  13497. newConfig.configurations.push(config2);
  13498. }
  13499. GM_setValue("chatgpt-config", newConfig);
  13500. const keysToDelete = ["openai_key", "openai_model", "openai_proxy", "api2d_key", "api2d_model", "api2d_request_entry", "x_api2d_no_cache", "showOpneAiAdvanced"];
  13501. keysToDelete.forEach(key => {
  13502. if (GM_getValue(key) != undefined) GM_deleteValue(key);
  13503. });
  13504. if (GM_getValue("translation") === "api2d") GM_setValue("translation", "openai");
  13505. location.reload();
  13506. }
  13507.  
  13508.  
  13509. // ------------------------------
  13510. // 配置自动迁移代码(将在10个小版本后移除-1.23)
  13511. // ------------------------------
  13512.  
  13513. {
  13514. let bottomZh_CN = GM_getValue("bottomZh_CN");
  13515. if (bottomZh_CN !== undefined) {
  13516. if (bottomZh_CN == true) {
  13517. GM_setValue("localizationLanguage", "zh");
  13518. } else {
  13519. GM_setValue("localizationLanguage", "initial");
  13520. }
  13521. GM_deleteValue("bottomZh_CN");
  13522. location.reload();
  13523. }
  13524. }
  13525. {
  13526. let config = GM_getValue("chatgpt-config");
  13527. if (config && config !== undefined) {
  13528. let index = parseInt(config.choice, 10);
  13529. if (index == -1) config.choice = "";
  13530. else config.choice = config.configurations[index].note;
  13531. config.configurations.forEach(function (item) {
  13532. item.name = item.note;
  13533. delete item.note;
  13534. });
  13535. GM_deleteValue("chatgpt-config");
  13536. GM_setValue("chatgpt_config", config);
  13537. location.reload();
  13538. }
  13539. }
  13540.  
  13541. // ------------------------------
  13542. // 配置自动迁移代码(将在10个小版本后移除-1.24)
  13543. // ------------------------------
  13544.  
  13545. {
  13546. let config = GM_getValue("compilerSelection");
  13547. if (config !== undefined) {
  13548. if (config === "61") {
  13549. GM_setValue("compilerSelection", "5001");
  13550. location.reload();
  13551. }
  13552. }
  13553. }