Codeforces Better!

Codeforces界面汉化、题目翻译,markdown视图,一键复制题目

当前为 2023-05-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Codeforces Better!
  3. // @namespace https://greasyfork.org/users/747162
  4. // @version 1.12
  5. // @description Codeforces界面汉化、题目翻译,markdown视图,一键复制题目
  6. // @author 北极小狐
  7. // @match https://codeforces.com/*
  8. // @connect www2.deepl.com
  9. // @connect m.youdao.com
  10. // @connect openai.api2d.net
  11. // @connect api.openai.com
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_info
  14. // @connect greasyfork.org
  15. // @icon https://aowuucdn.oss-cn-beijing.aliyuncs.com/codeforces.png
  16. // @require https://cdn.bootcdn.net/ajax/libs/turndown/7.1.1/turndown.min.js
  17. // @license MIT
  18. // ==/UserScript==
  19. // 样式
  20. function loadCssCode(code){
  21. var style = document.createElement('style');
  22. style.type = 'text/css';
  23. style.rel = 'stylesheet';
  24. style.appendChild(document.createTextNode(code));
  25. var head = document.getElementsByTagName('head')[0];
  26. head.appendChild(style);
  27. }
  28. loadCssCode(`
  29. span.mdViewContent {
  30. white-space: pre-wrap;
  31. }
  32. .translate-problem-statement {
  33. white-space: pre-wrap;
  34. border: 1px dashed #00aeeccc;
  35. border-radius: 0.3rem;
  36. padding: 5px;
  37. margin: 5px 0px;
  38. }
  39. .html2md-panel {
  40. display: flex;
  41. justify-content: flex-end;
  42. }
  43. button.html2mdButton {
  44. height: 3vh;
  45. width: 3vh;
  46. }
  47. button.html2mdButton {
  48. cursor: pointer;
  49. background-color: #e6e6e6;
  50. color: #727378;
  51. height: 3vh;
  52. width: auto;
  53. font-size: 1.3vh;
  54. border-radius: 0.3rem;
  55. border: none;
  56. padding: 1px 5px;
  57. margin: 5px;
  58. box-shadow: 0 0 1px #0000004d;
  59. }
  60. button.html2mdButton.copied {
  61. background-color: #07e65196;
  62. color: #104f2b;
  63. }
  64. button.html2mdButton.mdViewed {
  65. background-color: #ff980057;
  66. color: #5a3a0c;
  67. }
  68. button.translated {
  69. cursor: not-allowed;
  70. background-color: #2a6dc296;
  71. color: #fffdfd;
  72. }
  73. /*设置面板*/
  74. #CFBetter_setting_menu {
  75. z-index: 9999;
  76. border-radius: 0.5rem;
  77. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  78. display: grid;
  79. position: fixed;
  80. top: 50%;
  81. left: 50%;
  82. width: 270px;
  83. transform: translate(-50%, -50%);
  84. border-radius: 16px;
  85. background-color: #ecf0ff;
  86. border: 6px solid #ffffff;
  87. color: #697e91;
  88. padding: 10px 20px 20px 20px;
  89. }
  90.  
  91. /*设置面板-关闭按钮*/
  92. #CFBetter_setting_menu .tool-box {
  93. position: absolute;
  94. display: flex;
  95. align-items: center;
  96. justify-content: center;
  97. width: 2.5rem;
  98. height: 2.5rem;
  99. top: 3px;
  100. right: 3px;
  101. }
  102.  
  103. #CFBetter_setting_menu .btn-close {
  104. display: flex;
  105. align-items: center;
  106. justify-content: center;
  107. text-align: center;
  108. width: 2rem;
  109. height: 2rem;
  110. color: transparent;
  111. font-size: 0;
  112. cursor: pointer;
  113. background-color: #ff000080;
  114. border: none;
  115. border-radius: 10px;
  116. transition: .2s ease all;
  117. }
  118.  
  119. #CFBetter_setting_menu .btn-close:hover {
  120. width: 2rem;
  121. height: 2rem;
  122. font-size: 1rem;
  123. color: #ffffff;
  124. background-color: #ff0000cc;
  125. box-shadow: 0 5px 5px 0 #00000026;
  126. }
  127.  
  128. #CFBetter_setting_menu .btn-close:active {
  129. width: .9rem;
  130. height: .9rem;
  131. font-size: .9rem;
  132. color: #ffffffde;
  133. --shadow-btn-close: 0 3px 3px 0 #00000026;
  134. box-shadow: var(--shadow-btn-close);
  135. }
  136.  
  137. /*设置面板-checkbox*/
  138. #CFBetter_setting_menu input[type=checkbox]:focus {
  139. outline: 0px;
  140. }
  141.  
  142. #CFBetter_setting_menu input[type="checkbox"] {
  143. margin: 0px;
  144. appearance: none;
  145. width: 48px;
  146. height: 24px;
  147. border: 2px solid #6b8092;
  148. border-radius: 20px;
  149. background: #f1e1e1;
  150. position: relative;
  151. box-sizing: border-box;
  152. }
  153.  
  154. #CFBetter_setting_menu input[type="checkbox"]::before {
  155. content: "";
  156. width: 16px;
  157. height: 16px;
  158. background: #6b80927a;
  159. border: 2px solid #6b8092;
  160. border-radius: 50%;
  161. position: absolute;
  162. top: 0;
  163. left: 0;
  164. transform: translate(2%, 0%);
  165. transition: all 0.3s ease-in-out;
  166. }
  167.  
  168. #CFBetter_setting_menu input[type="checkbox"]::after {
  169. 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");
  170. position: absolute;
  171. top: 0;
  172. left: 24px;
  173. }
  174.  
  175. #CFBetter_setting_menu input[type="checkbox"]:checked {
  176. border: 2px solid #02c202;
  177. background: #e2f1e1;
  178. }
  179.  
  180. #CFBetter_setting_menu input[type="checkbox"]:checked::before {
  181. background: rgba(2, 194, 2, 0.5);
  182. border: 2px solid #02c202;
  183. transform: translate(118%, 0%);
  184. transition: all 0.3s ease-in-out;
  185. }
  186.  
  187. #CFBetter_setting_menu input[type="checkbox"]:checked::after {
  188. content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='13' 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='%2302C202' fill-opacity='0.9'/%3E%3C/svg%3E");
  189. position: absolute;
  190. top: 3px;
  191. left: 4px;
  192. }
  193.  
  194. #CFBetter_setting_menu input[type="checkbox"] + label {
  195. margin: 0px 0px 0px 10px !important;
  196. font-size: 16px;
  197. }
  198.  
  199. .CFBetter_setting_list {
  200. display: flex;
  201. align-items: center;
  202. margin-bottom: 18px;
  203. }
  204.  
  205. /*设置面板-radio*/
  206. #CFBetter_setting_menu>label {
  207. display: flex;
  208. list-style-type: none;
  209. padding-inline-start: 0px;
  210. overflow-x: auto;
  211. max-width: 100%;
  212. margin: 0px;
  213. align-items: center;
  214. margin: 3px 0px;
  215. }
  216.  
  217. .CFBetter_setting_menu_label_text {
  218. border: 1px dashed #00aeeccc;
  219. display: block;
  220. height: 20px;
  221. width: 100%;
  222. color: gray;
  223. font-weight: 300;
  224. font-size: 14px;
  225. letter-spacing: 2px;
  226. padding: 7px;
  227. }
  228.  
  229. input[type="radio"]:checked+.CFBetter_setting_menu_label_text {
  230. background: #41e49930;
  231. border: 1px solid green;
  232. color: green;
  233. font-weight: 500;
  234. }
  235.  
  236. #CFBetter_setting_menu>label input[type="radio"] {
  237. -webkit-appearance: none;
  238. appearance: none;
  239. list-style: none;
  240. margin: 0px;
  241. }
  242.  
  243. #CFBetter_setting_menu input[type="text"] {
  244. display: block;
  245. height: 25px;
  246. background-color: #ffffff;
  247. color: #727378;
  248. font-size: 1.3vh;
  249. border-radius: 0.3rem;
  250. padding: 1px 5px;
  251. margin: 5px 0px 5px 0px;
  252. border: 1px solid #00aeeccc;
  253. box-shadow: 0 0 1px #0000004d;
  254. }
  255.  
  256. .CFBetter_setting_menu_input {
  257. width: 100%;
  258. display: grid;
  259. justify-content: start;
  260. }
  261.  
  262. #CFBetter_setting_menu #save {
  263. cursor: pointer;
  264. display: inline-flex;
  265. padding: 0.5rem 1rem;
  266. background-color: #1aa06d;
  267. color: #ffffff;
  268. font-size: 1rem;
  269. line-height: 1.5rem;
  270. font-weight: 500;
  271. justify-content: center;
  272. width: 100%;
  273. border-radius: 0.375rem;
  274. border: none;
  275. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  276. }
  277.  
  278. button.html2mdButton.CFBetter_setting {
  279. float: right;
  280. background: #60a5fa;
  281. color: white;
  282. box-shadow: 0px 0px 1px 1px #0000004d;
  283. margin: 10px;
  284. }
  285. #CFBetter_setting_menu span.tip {
  286. color: red;
  287. font-size: 2px;
  288. font-weight: 500;
  289. padding: 0px 0px 10px 0px;
  290. }
  291. /*更新检查*/
  292. div#update_panel {
  293. position: fixed;
  294. top: 50%;
  295. left: 50%;
  296. width: 240px;
  297. transform: translate(-50%, -50%);
  298. background-color: #fdfdfd;
  299. border: 1px solid #00aeeccc;
  300. border-radius: 5px;
  301. box-shadow: 2px 2px 3px 1px #0000004d;
  302. padding: 10px 20px 20px 20px;
  303. color: #444242;
  304. background-color: #ecf0ff;
  305. border: 6px solid #ffffff;
  306. border-radius: 16px;
  307. }
  308. div#update_panel #updating {
  309. cursor: pointer;
  310. display: inline-flex;
  311. padding: 0px;
  312. background-color: #1aa06d;
  313. color: #ffffff;
  314. font-size: 1rem;
  315. line-height: 1.5rem;
  316. font-weight: 500;
  317. justify-content: center;
  318. width: 100%;
  319. border-radius: 0.375rem;
  320. border: none;
  321. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  322. }
  323. div#update_panel #updating a {
  324. text-decoration: none;
  325. color: white;
  326. display: flex;
  327. position: inherit;
  328. top: 0;
  329. left: 0;
  330. width: 100%;
  331. height: 22px;
  332. font-size: 14px;
  333. justify-content: center;
  334. align-items: center;
  335. }
  336. `);
  337.  
  338. // 更新检查
  339. (function checkScriptVersion() {
  340. function compareVersions(version1 = "0", version2 = "0") {
  341. const v1Array = String(version1).split(".");
  342. const v2Array = String(version2).split(".");
  343. const minLength = Math.min(v1Array.length, v2Array.length);
  344. let result = 0;
  345. for (let i = 0; i < minLength; i++) {
  346. const curV1 = Number(v1Array[i]);
  347. const curV2 = Number(v2Array[i]);
  348. if (curV1 > curV2) {
  349. result = 1;
  350. break;
  351. } else if (curV1 < curV2) {
  352. result = -1;
  353. break;
  354. }
  355. }
  356. if (result === 0 && v1Array.length !== v2Array.length) {
  357. const v1IsBigger = v1Array.length > v2Array.length;
  358. const maxLenArray = v1IsBigger ? v1Array : v2Array;
  359. for (let i = minLength; i < maxLenArray.length; i++) {
  360. const curVersion = Number(maxLenArray[i]);
  361. if (curVersion > 0) {
  362. v1IsBigger ? result = 1 : result = -1;
  363. break;
  364. }
  365. }
  366. }
  367. return result;
  368. }
  369.  
  370. GM_xmlhttpRequest({
  371. method: "GET",
  372. url: "https://greasyfork.org/zh-CN/scripts/465777.json",
  373. timeout: 10 * 1e3,
  374. onload: function(response) {
  375. const scriptData = JSON.parse(response.responseText);
  376. if (scriptData.name === GM_info.script.name && compareVersions(scriptData.version, GM_info.script.version) === 1) {
  377. $("body").append(`
  378. <div id='update_panel'>
  379. <h3>${GM_info.script.name}有新版本!</h3>
  380. <hr>
  381. <div class='update_panel_menu'>
  382. <span class ='tip'>版本信息:${GM_info.script.version} ${scriptData.version}</span>
  383. </div>
  384. <br>
  385. <button id='updating'><a target="_blank" href="${scriptData.url}">更新</a></button>
  386. </div>
  387. `);
  388. }
  389. }
  390. });
  391. })();
  392.  
  393. // 汉化替换
  394. (function() {
  395. if(getCookie("bottomZh_CN")=== '') setCookie("bottomZh_CN", "true", 3650);
  396. if(getCookie("bottomZh_CN") != "true")return;
  397. // 设置语言为zh
  398. var htmlTag = document.getElementsByTagName("html")[0];
  399. htmlTag.setAttribute("lang", "zh-CN");
  400.  
  401. // 定义classData,存放元素的class名和对应的替换规则
  402. const classData = {
  403. '.menu-list.main-menu-list': [
  404. { match: 'Help', replace: '帮助' },
  405. { match: 'Calendar', replace: '日历' },
  406. { match: 'Edu', replace: '培训' },
  407. { match: 'Rating', replace: '积分榜' },
  408. { match: 'Groups', replace: '团体' },
  409. { match: 'Problemset', replace: '题单' },
  410. { match: 'Gym', replace: '训练营(过去的大型比赛)' },
  411. { match: 'Contests', replace: '比赛' },
  412. { match: 'Catalog', replace: '指南目录' },
  413. { match: 'Top', replace: '热门' },
  414. { match: 'Home', replace: '主页' },
  415. ],
  416. '.caption.titled': [
  417. { match: 'Pay attention', replace: '注意' },
  418. { match: 'Top rated', replace: '积分排行' },
  419. { match: 'Top contributors', replace: '贡献者排行' },
  420. { match: 'Find user', replace: '查找用户' },
  421. { match: 'Recent actions', replace: '最近热帖' },
  422. { match: 'Training filter', replace: '过滤筛选' },
  423. { match: 'Find training', replace: '搜索比赛/问题' },
  424. { match: 'Virtual participation', replace: '什么是虚拟参赛' },
  425. { match: 'Contest materials', replace: '比赛相关资料' },
  426. { match: 'Settings', replace: '设置' },
  427. { match: 'Clone Contest to Mashup', replace: 'Clone比赛到组合混搭' },
  428. { match: 'Submit', replace: '提交' },
  429. ],
  430. '.personal-sidebar ': [
  431. { match: 'Contribution', replace: '贡献' },
  432. { match: 'Settings', replace: '设置' },
  433. { match: 'Blog', replace: '博客' },
  434. { match: 'Teams', replace: '队伍' },
  435. { match: 'Submissions', replace: '提交' },
  436. { match: 'Talks', replace: '私信' },
  437. { match: 'Contests', replace: '比赛' },
  438. ],
  439. '.contest-state-phase': [
  440. { match: 'Before contest', replace: '即将进行的比赛' },
  441. ],
  442. '.notice': [
  443. { match: 'has extra registration', replace: '有额外的报名时期' },
  444. { match: 'If you are late to register in 5 minutes before the start, you can register later during the extra registration. Extra registration opens 10 minutes after the contest starts and lasts 25 minutes.', replace: '如果您在比赛开始前5分钟前还未报名,您可以在额外的报名期间稍后报名。额外的报名将在比赛开始后10分钟开放,并持续25分钟。' },
  445. ],
  446. '.act-item': [
  447. { match: 'Add to favourites', replace: '添加到收藏' },
  448. { match: 'Submit', replace: '提交' },
  449. ],
  450. '.datatable': [
  451. { match: 'Virtual participation', replace: '参加虚拟重现赛' },
  452. { match: 'Enter', replace: '进入' },
  453. { match: 'Final standings', replace: '榜单' },
  454. { match: 'School/University/City/Region Championship', replace: '学校/大学/城市/区域比赛' },
  455. { match: 'Official School Contest', replace: '学校官方比赛' },
  456. { match: 'Training Contest', replace: '训练赛' },
  457. { match: 'Training Camp Contest', replace: '训练营比赛' },
  458. { match: 'Official ICPC Contest', replace: 'ICPC官方比赛' },
  459. { match: 'Official International Personal Contest', replace: '官方国际个人赛' },
  460. { match: 'China', replace: '中国' },
  461. { match: 'Statements', replace: '题目描述' },
  462. { match: 'in Chinese', replace: '中文' },
  463. { match: 'Trainings', replace: '训练' },
  464. { match: 'Prepared by', replace: '编写人' },
  465. { match: 'Current or upcoming contests', replace: '当前或即将举行的比赛' },
  466. { match: 'Past contests', replace: '过去的比赛' },
  467. { match: 'Exclusions', replace: '排除' },
  468. { match: 'Before start', replace: '还有' },
  469. { match: 'Before registration', replace: '报名还有' },
  470. { match: 'Until closing ', replace: '结束报名' },
  471. { match: 'Register', replace: '报名' },
  472. { match: 'Registration completed', replace: '已报名' },
  473. { match: 'Questions about problems', replace: '关于题目的提问' },
  474. ],
  475. '.ask-question-link': [
  476. { match: 'Ask a question', replace: '提一个问题' },
  477. ],
  478. '.contests-table': [
  479. { match: 'Contest history', replace: '比赛历史' },
  480. ],
  481. '.roundbox.sidebox.borderTopRound ': [
  482. { match: 'Season:', replace: '时间范围(年度)' },
  483. { match: 'Contest type', replace: '比赛类型' },
  484. { match: 'ICPC region', replace: 'ICPC地区' },
  485. { match: 'Contest format', replace: '比赛形式' },
  486. { match: 'Duration, hours', replace: '持续时间(小时)' },
  487. { match: 'Order by', replace: '排序方式' },
  488. { match: 'Secondary order by', replace: '次要排序方式' },
  489. { match: 'Hide, if participated', replace: '隐藏我参与过的' },
  490. { match: 'Hide excluded gyms', replace: '隐藏排除的比赛' },
  491. { match: 'Register now', replace: '现在报名' },
  492. { match: 'Show tags for unsolved problems', replace: '显示未解决问题的标签' },
  493. { match: 'Hide solved problems', replace: '隐藏已解决的问题' },
  494. ],
  495. '.icon-eye-close.icon-large': [
  496. { match: 'Add to exclusions', replace: '添加到排除列表' },
  497. ],
  498. '._ContestFilterExclusionsManageFrame_addExclusionLink': [
  499. { match: 'Add to exclusions for gym contests filter', replace: '添加训练营过滤器的排除项' },
  500. ],
  501. '.roundbox.sidebox.sidebar-menu.borderTopRound ': [
  502. { match: 'Announcement', replace: '公告' },
  503. { match: 'Statements', replace: '统计报表' },
  504. { match: 'Tutorial', replace: '题解' },
  505. ],
  506. '.second-level-menu ': [
  507. { match: 'Problems', replace: '问题' },
  508. { match: 'Submit Code', replace: '提交代码' },
  509. { match: 'My Submissions', replace: '我的提交' },
  510. { match: 'Status', replace: '状态' },
  511. { match: 'Standings', replace: '榜单' },
  512. { match: 'Custom Invocation', replace: '自定义调试' },
  513. { match: 'Common standings', replace: '全部排行' },
  514. { match: 'Friends standings', replace: '只看朋友' },
  515. { match: 'Submit', replace: '提交' },
  516. { match: 'Custom test', replace: '自定义测试' },
  517. { match: 'Blog', replace: '博客' },
  518. { match: 'Teams', replace: '队伍' },
  519. { match: 'Submissions', replace: '提交' },
  520. { match: 'Groups', replace: '团体' },
  521. { match: 'Contests', replace: '比赛' },
  522. { match: '问题etting', replace: '参与编写的问题' },
  523. { match: 'Gym', replace: '训练营' },
  524. { match: 'Mashups', replace: '组合混搭' },
  525. { match: 'Posts', replace: '帖子' },
  526. { match: 'Comments', replace: '回复' },
  527. { match: 'Main', replace: '主要' },
  528. { match: 'Settings', replace: '设置' },
  529. { match: 'Lists', replace: '列表' },
  530. ],
  531. '.topic-toggle-collapse': [
  532. { match: 'Expand', replace: '展开' },
  533. ],
  534. '.topic-read-more': [
  535. { match: 'Full text and comments', replace: '阅读全文/评论' },
  536. ],
  537. '.toggleEditorCheckboxLabel': [
  538. { match: 'Switch off editor', replace: '关闭编辑器语法高亮' },
  539. ],
  540. '.content-with-sidebar': [
  541. { match: 'Notice', replace: '注意' },
  542. { match: 'virtual participation', replace: '虚拟参与' },
  543. { match: 'Registration for the contest', replace: '比赛报名' },
  544. { match: 'Terms of agreement', replace: '协议条款' },
  545. { match: 'Take part', replace: '参与' },
  546. { match: 'as individual participant', replace: '作为个人参与者' },
  547. { match: 'as a team member', replace: '作为团队成员' },
  548. { match: 'Virtual start time', replace: '虚拟开始时间' },
  549. { match: 'Complete problemset', replace: '完整的问题集' },
  550. ],
  551. '.submit': [
  552. { match: 'Registration for the contest', replace: '比赛报名' },
  553. ],
  554. '.table-form': [
  555. { match: 'Problem', replace: '题目' },
  556. { match: 'Language', replace: '语言' },
  557. { match: 'Source code', replace: '源代码' },
  558. { match: 'Or choose file', replace: '或者选择文件' },
  559. { match: 'Choose file', replace: '选择文件' },
  560. ],
  561. };
  562.  
  563. // 将所有 class 名与之符合的元素的 text 中包含的匹配的文字均替换为对应的文字
  564. for (const className in classData) {
  565. const elements = document.querySelectorAll(className);
  566. elements.forEach((element) => {
  567. let html = element.outerHTML; // 获取元素的 html 代码
  568. const parent = element.parentNode;
  569. const childIndex = Array.from(parent.children).indexOf(element);
  570. let matched = false; // 标记该元素是否匹配了规则
  571. classData[className].forEach((rule) => {
  572. if (html.match(new RegExp(rule.match, 'g'))) {
  573. // 如果匹配成功,则将 matched 设置为 true
  574. matched = true;
  575. html = html.replace(new RegExp(rule.match, 'g'), rule.replace); // 将其中匹配的文字替换为对应的文字
  576. const temp = document.createElement('div'); // 创建临时元素
  577. temp.innerHTML = html.trim();
  578. const newElement = element.cloneNode(true);
  579. newElement.innerHTML = temp.firstChild.innerHTML;
  580. parent.replaceChild(newElement, parent.children[childIndex]); // 替换元素
  581. }
  582. });
  583. });
  584. }
  585. })();
  586.  
  587. // **设置面板**
  588. $("div[class='lang-chooser']").each(function() {
  589. $(this).before(
  590. "<button class='html2mdButton CFBetter_setting'>CodeforcesBetter设置</button>"
  591. );
  592. });
  593. $(document).ready(function () {
  594. $(".CFBetter_setting").click(function () {
  595. $(".CFBetter_setting").attr("disabled", true);
  596. $(".CFBetter_setting").css("background-color", "#e6e6e6");
  597. $(".CFBetter_setting").css("color", "#727378");
  598. $(".CFBetter_setting").css("cursor", "not-allowed");
  599. $("body").append(`
  600. <div id='CFBetter_setting_menu'>
  601. <div class="tool-box">
  602. <button class="btn-close">×</button>
  603. </div>
  604. <h3>基本设置</h3>
  605. <hr>
  606. <div class='CFBetter_setting_list'>
  607. <input type="checkbox" id="bottomZh_CN" name="bottomZh_CN" checked>
  608. <label for="bottomZh_CN">界面汉化</label>
  609. </div>
  610. <h3>翻译设置</h3>
  611. <hr>
  612. <label>
  613. <input type='radio' name='translation' value='deepl'>
  614. <span class='CFBetter_setting_menu_label_text'>deepl翻译</span>
  615. </label>
  616. <label>
  617. <input type='radio' name='translation' value='youdao'>
  618. <span class='CFBetter_setting_menu_label_text'>有道翻译</span>
  619. </label>
  620. <label>
  621. <input type='radio' name='translation' value='openai'>
  622. <span class='CFBetter_setting_menu_label_text'>使用ChatGPT翻译(API)</span>
  623. </label>
  624. <label>
  625. <input type='radio' name='translation' value='api2d'>
  626. <span class='CFBetter_setting_menu_label_text'>使用api2d翻译(API)</span>
  627. </label><br>
  628. <div class='CFBetter_setting_menu_input' id='baidu' style='display: none;'>
  629. <label for='baidu_uid'>APP ID:</label><input type='text' id='baidu_uid'>
  630. <label for='baidu_key'>KEY:</label><input type='text' id='baidu_key'>
  631. </div>
  632. <div class='CFBetter_setting_menu_input' id='openai' style='display: none;'>
  633. <span class ='tip'>提示:<br>请确保你能够正常访问OpenAIapi<br></span>
  634. <span class ='tip'>使用ChatGPT-3.5进行翻译,脚本的所有请求均在本地完成,</span>
  635. <span class ='tip'>你需要输入自己的OpenAI KEY,<a target="_blank" href="https://platform.openai.com/account/usage">官网</a></span>
  636. <label for='openai_key'>KEY:</label><input type='text' id='openai_key'>
  637. </div>
  638. <div class='CFBetter_setting_menu_input' id='api2d' style='display: none;'>
  639. <span class ='tip'>提示:<br>api2d是国内的一家提供代理直连访问OpenAIapi的服务商,相当于OpenAIapi的套壳<br></span>
  640. <span class ='tip'>使用ChatGPT-3.5进行翻译,脚本的所有请求均在本地完成<br></span>
  641. <span class ='tip'>你需要输入自己的api2d KEY,<a target="_blank" href="https://api2d.com/profile">官网</a></span>
  642. <label for='api2d_key'>KEY:</label><input type='text' id='api2d_key'>
  643. </div>
  644. <br>
  645. <button id='save'>保存</button>
  646. </div>
  647. `);
  648. var translation = getCookie("translation");
  649. $("#bottomZh_CN").prop("checked", getCookie("bottomZh_CN") === "true");
  650. if (translation === 'undefined' || translation === '') {
  651. setCookie("translation", "deepl", 3650);
  652. $("input[name='translation'][value='deepl']").prop("checked", true);
  653. } else {
  654. $("input[name='translation'][value='" + translation + "']").prop("checked", true);
  655. $("input[name='translation']").css("color", "gray");
  656. if (translation == "baidu") {
  657. $("#baidu").show();
  658. $("#baidu_uid").val(getCookie("baidu_uid"));
  659. $("#baidu_key").val(getCookie("baidu_key"));
  660. $("#baidu_uid").css("color", "gray");
  661. $("#baidu_key").css("color", "gray");
  662. } else if (translation == "openai") {
  663. $("#openai").show();
  664. $("#openai_key").val(getCookie("openai_key"));
  665. $("#openai_key").css("color", "gray");
  666. } else if (translation == "api2d") {
  667. $("#api2d").show();
  668. $("#api2d_key").val(getCookie("api2d_key"));
  669. $("#api2d_key").css("color", "gray");
  670. }
  671. }
  672.  
  673. // 当单选框被选中时,显示对应的输入框,同时隐藏其他输入框
  674. $("input[name='translation']").change(function () {
  675. var selected = $(this).val(); // 获取当前选中的值
  676. if (selected === 'baidu') {
  677. $("#baidu").show();
  678. $("#baidu_uid").val(getCookie("baidu_uid"));
  679. $("#baidu_key").val(getCookie("baidu_key"));
  680. $("#openai, #api2d").hide();
  681. } else if (selected === 'openai') {
  682. $("#openai").show();
  683. $("#openai_key").val(getCookie("openai_key"));
  684. $("#baidu, #api2d").hide();
  685. } else if (selected === 'api2d') {
  686. $("#api2d").show();
  687. $("#api2d_key").val(getCookie("api2d_key"));
  688. $("#baidu, #openai").hide();
  689. } else {
  690. $("#baidu, #openai, #api2d").hide();
  691. }
  692. });
  693.  
  694. $("#save").click(function () {
  695. setCookie("bottomZh_CN", $("#bottomZh_CN").prop("checked"), 3650);
  696. var translation = $("input[name='translation']:checked").val();
  697. var baidu_uid = $("#baidu_uid").val();
  698. var baidu_key = $("#baidu_key").val();
  699. var openai_key = $("#openai_key").val();
  700. var api2d_key = $("#api2d_key").val();
  701. setCookie("translation", translation, 3650);
  702. if (translation == "baidu") {
  703. setCookie("baidu_uid", baidu_uid, 3650);
  704. setCookie("baidu_key", baidu_key, 3650);
  705. } else if (translation == "openai") {
  706. setCookie("openai_key", openai_key, 3650);
  707. } else if (translation == "api2d") {
  708. setCookie("api2d_key", api2d_key, 3650);
  709. }
  710. location.reload();
  711. });
  712. // 关闭
  713. $("#CFBetter_setting_menu .btn-close").click(function () {
  714. $("#CFBetter_setting_menu").remove();
  715. $(".CFBetter_setting").attr("disabled", false);
  716. $(".CFBetter_setting").css("background-color", "#60a5fa");
  717. $(".CFBetter_setting").css("color", "white");
  718. $(".CFBetter_setting").css("cursor", "pointer");
  719. });
  720. });
  721. });
  722.  
  723. // 题目markdown转换/翻译面板
  724. function addButtonPanel(parent, suffix){
  725. $(parent).children(':eq(1)').before(
  726. "<div class='html2md-panel'> <button class='html2mdButton html2md-view" + suffix + "'>MarkDown视图</button> <button class='html2mdButton html2md-cb" + suffix + "'>Copy</button> <button class='html2mdButton translateButton" + suffix + "'>翻译</button> </div>");
  727. }
  728. function addButtonWithHTML2MD(parent, suffix) {
  729. $(".html2md-view" + suffix).click(function () {
  730. var target, removedChildren;
  731. if(suffix === "1") {
  732. target = $(this).parent().next().get(0);
  733. } else {
  734. target = $(this).parent().parent().get(0);
  735. removedChildren = $(this).parent().parent().children().slice(0, 2).detach();
  736. }
  737. if (target.viewmd) {
  738. target.viewmd = false;
  739. $(this).text("MarkDown视图");
  740. $(this).removeClass("mdViewed");
  741. $(target).html(target.original_html);
  742. } else {
  743. target.viewmd = true;
  744. if (!target.original_html)
  745. target.original_html = $(target).html();
  746. if (!target.markdown)
  747. target.markdown = turndownService.turndown($(target).html());
  748. $(this).text("原始内容");
  749. $(this).addClass("mdViewed");
  750. $(target).html(`<span class="mdViewContent" oninput="$(this).parent().get(0).markdown=this.value;" style="width:auto; height:auto;"> ${target.markdown} </span>`);
  751. }
  752. // 恢复删除的元素
  753. if(suffix != "1") $(target).prepend(removedChildren);
  754. });
  755. }
  756.  
  757. function addButtonWithCopy(parent, suffix) {
  758. $(".html2md-cb" + suffix).click(function () {
  759. var target, removedChildren;
  760. if(suffix === "1") {
  761. target = $($(this).parent().next().get(0)).clone();
  762. } else {
  763. target = $($(this).parent().parent().get(0)).clone();
  764. $(target).children(':first').remove(); // 删除前两个元素(标题和按钮面板)
  765. $(target).children(':first').remove();
  766. }
  767. if (!target.markdown) {
  768. target.markdown = turndownService.turndown($(target).html());
  769. }
  770. const textarea = document.createElement('textarea');
  771. textarea.value = target.markdown;
  772. document.body.appendChild(textarea);
  773. textarea.select();
  774. document.execCommand('copy');
  775. document.body.removeChild(textarea);
  776. $(this).addClass("copied");
  777. $(this).text("Copied");
  778. // 更新复制按钮文本
  779. setTimeout(() => {
  780. $(this).removeClass("copied");
  781. $(this).text("Copy");
  782. }, 2000);
  783. $(target).remove();
  784. });
  785. }
  786.  
  787. function addButtonWithTranslation(parent, suffix) {
  788. $(".translateButton" + suffix).click(async function () {
  789. var target, removedChildren;
  790. if(suffix === "1") {
  791. target = $($(this).parent().next().get(0)).clone();
  792. } else {
  793. target = $($(this).parent().parent().get(0)).clone();
  794. $(target).children(':first').remove(); // 删除前两个元素(标题和按钮面板)
  795. $(target).children(':first').remove();
  796. }
  797. if (!target.markdown) {
  798. target.markdown = turndownService.turndown($(target).html());
  799. }
  800. const textarea = document.createElement('textarea');
  801. textarea.value = target.markdown;
  802. // 翻译处理
  803. $(this).text("翻译中");
  804. $(this).css("cursor", "not-allowed");
  805. var element_node;
  806. if(suffix === "1"){
  807. element_node = $("div." + $(parent).attr("class")).children()[2]; // 题面特殊处理
  808. } else {
  809. element_node = $("div." + $(parent).attr("class"))[0];
  810. }
  811. await translateProblemStatement(textarea.value, element_node);
  812. //
  813. $(this).addClass("translated");
  814. $(this).text("已翻译");
  815. $(this).prop("disabled", true);
  816. $(target).remove();
  817. });
  818. }
  819.  
  820. var turndownService = new TurndownService();
  821. window.onload = function() {
  822. turndownService.keep(['del']);
  823.  
  824. // **处理规则**
  825. // 忽略sample-tests
  826. turndownService.addRule('ignore-sample-tests', {
  827. filter: function(node) {
  828. return node.classList.contains('sample-tests')|| node.classList.contains('header');
  829. },
  830. replacement: function (content, node) {
  831. return "";
  832. }
  833. });
  834.  
  835. // remove <script> math
  836. turndownService.addRule('remove-script', {
  837. filter: function (node, options) {
  838. return node.tagName.toLowerCase() == "script" && node.type.startsWith("math/tex");
  839. },
  840. replacement: function (content, node) {
  841. return "";
  842. }
  843. });
  844.  
  845. // inline math
  846. turndownService.addRule('inline-math', {
  847. filter: function (node, options) {
  848. return node.tagName.toLowerCase() == "span" && node.className == "MathJax";
  849. },
  850. replacement: function (content, node) {
  851. return "$ " + $(node).next().text() + " $";
  852. }
  853. });
  854.  
  855. // block math
  856. turndownService.addRule('block-math', {
  857. filter: function (node, options) {
  858. return node.tagName.toLowerCase() == "div" && node.className == "MathJax_Display";
  859. },
  860. replacement: function (content, node) {
  861. return "\n$$\n" + $(node).next().text() + "\n$$\n";
  862. }
  863. });
  864.  
  865. // 添加按钮到题面部分
  866. $("div[class='problem-statement']").each(function () {
  867. addButtonPanel(this,"1");
  868. addButtonWithHTML2MD(this, "1");
  869. addButtonWithCopy(this, "1");
  870. addButtonWithTranslation(this, "1");
  871. });
  872.  
  873. // 添加按钮到input部分
  874. $("div[class='input-specification']").each(function () {
  875. addButtonPanel(this,"2");
  876. addButtonWithHTML2MD(this, "2");
  877. addButtonWithCopy(this, "2");
  878. addButtonWithTranslation(this, "2");
  879. });
  880.  
  881. // 添加按钮到output部分
  882. $("div[class='output-specification']").each(function () {
  883. addButtonPanel(this, "3");
  884. addButtonWithHTML2MD(this, "3");
  885. addButtonWithCopy(this, "3");
  886. addButtonWithTranslation(this, "3");
  887. });
  888.  
  889. // 添加按钮到note部分
  890. $("div[class='note']").each(function () {
  891. addButtonPanel(this,"4");
  892. addButtonWithHTML2MD(this, "4");
  893. addButtonWithCopy(this, "4");
  894. addButtonWithTranslation(this, "4");
  895. });
  896. };
  897.  
  898. // **翻译框/翻译处理器**
  899. var translatedText = "";
  900. async function translateProblemStatement(text, element_node){
  901. let id = Math.floor(Date.now() / 1000);
  902. let matches;
  903. let replacements;
  904. // 创建元素并放在element_node的后面
  905. const translateDiv = document.createElement('div');
  906. translateDiv.setAttribute('id', id);
  907. translateDiv.classList.add('translate-problem-statement');
  908. const spanElement = document.createElement('span');
  909. translateDiv.appendChild(spanElement);
  910. element_node.insertAdjacentElement('afterend', translateDiv);
  911. // 替换latex公式
  912. if(getCookie("translation")!= "api2d"&&getCookie("translation")!= "openai"){
  913. matches = text.match(/\$(.*?)\$/g);
  914. replacements = {};
  915. try{
  916. for (let i = 0; i < matches.length; i++) {
  917. let match = matches[i];
  918. text = text.replace(match, `【${i + 1}】`);
  919. replacements[`【${i + 1}】`] = match;
  920. }
  921. }catch(e){}
  922. text = text.replace(/\\/g, "");
  923. }
  924. // 翻译
  925. var translation = getCookie("translation");
  926. if (translation === 'undefined' || translation === ''){
  927. setCookie("translation", "deepl", 3650);
  928. translation = "deepl";
  929. }
  930. if(translation == "deepl") {
  931. translateDiv.textContent = "正在翻译中……请耐心等待,\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
  932. translatedText = await translate_deepl(text);
  933. }else if (translation == "youdao" ){
  934. translateDiv.textContent = "正在翻译中……请耐心等待,\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
  935. translatedText = await translate_youdao_mobile(text);
  936. }else if (translation == "baidu") {
  937. var baidu_appid = getCookie("baidu_uid");
  938. var baidu_key = getCookie("baidu_key");
  939. } else if (translation == "openai") {
  940. translateDiv.textContent = "正在翻译中……请稍等\n\n使用GPT(ChatGPT/api2d)进行翻译通常需要很长的时间,请耐心等待\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
  941. translatedText = await translate_openai(text);
  942. } else if (translation == "api2d") {
  943. translateDiv.textContent = "正在翻译中……请稍等\n\n使用GPT(ChatGPT/api2d)进行翻译通常需要很长的时间,请耐心等待\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
  944. translatedText = await translate_api2d(text);
  945. }
  946. // 还原latex公式
  947. if(getCookie("translation")!= "api2d"&&getCookie("translation")!= "openai"){
  948. try{
  949. for (let i = 0; i < matches.length; i++) {
  950. let match = matches[i];
  951. let replacement = replacements[`【${i + 1}】`];
  952. translatedText = translatedText.replace(`【${i + 1}】`, replacement);
  953. }
  954. }catch(e){}
  955. }
  956. // 使符合mathjx的转换语法
  957. translatedText = translatedText.replace(/\$/g, "$$$$$$");
  958. //
  959. // 更新
  960. translateDiv.textContent = translatedText;
  961. // 渲染Latex
  962. MathJax.Hub.Queue(["Typeset", MathJax.Hub, document.getElementById(id)]);
  963. }
  964.  
  965. // ChatGPT
  966. async function translate_openai(raw) {
  967. var openai_key = getCookie("openai_key");
  968. var openai_retext = "";
  969. const data = {
  970. prompt: "(You:请帮我翻译将下面的文本翻译为中文,注意保持其中的latex公式不翻译,你只需要回复翻译后的内容即可,不要回复任何其他内容:\n\n"+ raw + ")",
  971. max_tokens: 2048,
  972. model: "text-davinci-003",
  973. }
  974. return new Promise(function(resolve, reject) {
  975. GM_xmlhttpRequest({
  976. method: 'POST',
  977. url: 'https://api.openai.com/v1/completions',
  978. data: JSON.stringify(data),
  979. headers: {
  980. 'Content-Type': 'application/json',
  981. 'Authorization': 'Bearer ' + getCookie("openai_key") + ''
  982. },
  983. responseType: 'json',
  984. onload: function (res) {
  985. if (res.status === 200) {
  986. openai_retext = res.response.choices[0].text;
  987. openai_retext = openai_retext.replace(/^\s+/, '');
  988. resolve(openai_retext);
  989. }
  990. else {
  991. console.error(res.statusText);
  992. reject(res.statusText);
  993. }
  994. }
  995. });
  996. });
  997. }
  998.  
  999. // api2d
  1000. async function translate_api2d(raw) {
  1001. var api2d_key = getCookie("api2d_key");
  1002. var api2d_retext = "";
  1003. const postData = JSON.stringify({
  1004. model: 'gpt-3.5-turbo',
  1005. messages: [{ role: 'user', content: '请帮我翻译将下面的文本翻译为中文,注意保持其中的latex公式不翻译,你只需要回复翻译后的内容即可,不要回复任何其他内容:\n\n'+ raw }],
  1006. });
  1007.  
  1008. const options = {
  1009. method: 'POST',
  1010. headers: {
  1011. 'Content-Type': 'application/json',
  1012. Authorization: 'Bearer '+api2d_key,
  1013. },
  1014. data: postData,
  1015. };
  1016.  
  1017. return new Promise(function(resolve, reject) {
  1018. GM_xmlhttpRequest({
  1019. method: options.method,
  1020. url: `https://openai.api2d.net/v1/chat/completions`,
  1021. headers: options.headers,
  1022. data: options.data,
  1023. responseType: 'json',
  1024. onload: function (response) {
  1025. api2d_retext = response.response.choices[0].message.content;
  1026. resolve(api2d_retext);
  1027. },
  1028. onerror: function (response) {
  1029. console.error(response.statusText);
  1030. reject(response.statusText);
  1031. },
  1032. });
  1033. });
  1034. }
  1035. //
  1036.  
  1037. //--有道翻译m--start
  1038. async function translate_youdao_mobile(raw){
  1039. const options = {
  1040. method:"POST",
  1041. url:'http://m.youdao.com/translate',
  1042. data:"inputtext="+encodeURIComponent(raw)+"&type=AUTO",
  1043. anonymous:true,
  1044. headers: {
  1045. "Content-Type": "application/x-www-form-urlencoded"
  1046. }
  1047. }
  1048. return await BaseTranslate('有道翻译mobile',raw,options,res=>/id="translateResult">\s*?<li>([\s\S]*?)<\/li>\s*?<\/ul/.exec(res)[1])
  1049. }
  1050. //--有道翻译m--end
  1051.  
  1052. //--Deepl翻译--start
  1053. function getTimeStamp(iCount) {
  1054. const ts = Date.now();
  1055. if (iCount !== 0) {
  1056. iCount = iCount + 1;
  1057. return ts - (ts % iCount) + iCount;
  1058. } else {
  1059. return ts;
  1060. }
  1061. }
  1062.  
  1063. async function translate_deepl(raw) {
  1064. const id = (Math.floor(Math.random() * 99999) + 100000)* 1000;
  1065. const data = {
  1066. jsonrpc: '2.0',
  1067. method: 'LMT_handle_texts',
  1068. id,
  1069. params: {
  1070. splitting: 'newlines',
  1071. lang: {
  1072. source_lang_user_selected: 'auto',
  1073. target_lang: 'ZH',
  1074. },
  1075. texts: [{
  1076. text: raw,
  1077. requestAlternatives:3
  1078. }],
  1079. timestamp: getTimeStamp(raw.split('i').length - 1)
  1080. }
  1081. }
  1082. let postData = JSON.stringify(data);
  1083. if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
  1084. postData = postData.replace('"method":"', '"method" : "');
  1085. } else {
  1086. postData = postData.replace('"method":"', '"method": "');
  1087. }
  1088. const options = {
  1089. method: 'POST',
  1090. url: 'https://www2.deepl.com/jsonrpc',
  1091. data: postData,
  1092. headers: {
  1093. 'Content-Type': 'application/json',
  1094. 'Host': 'www.deepl.com',
  1095. 'Origin': 'https://www.deepl.com',
  1096. 'Referer': 'https://www.deepl.com/'
  1097. },
  1098. anonymous:true,
  1099. nocache:true,
  1100. }
  1101. return await BaseTranslate('Deepl翻译',raw,options,res=>JSON.parse(res).result.texts[0].text)
  1102. }
  1103.  
  1104. //--Deepl翻译--end
  1105.  
  1106. // **Cookie Get/Set**
  1107. function setCookie(cname, cvalue, exdays) {
  1108. var d = new Date();
  1109. d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
  1110. var expires = "expires=" + d.toUTCString();
  1111. document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
  1112. }
  1113.  
  1114. function getCookie(cname) {
  1115. var name = cname + "=";
  1116. var decodedCookie = decodeURIComponent(document.cookie);
  1117. var ca = decodedCookie.split(';');
  1118. for (var i = 0; i < ca.length; i++) {
  1119. var c = ca[i];
  1120. while (c.charAt(0) == ' ') {
  1121. c = c.substring(1);
  1122. }
  1123. if (c.indexOf(name) == 0) {
  1124. return c.substring(name.length, c.length);
  1125. }
  1126. }
  1127. return "";
  1128. }
  1129.  
  1130.  
  1131. //--异步请求包装工具--start
  1132. async function PromiseRetryWrap(task,options,...values){
  1133. const {RetryTimes,ErrProcesser} = options||{};
  1134. let retryTimes = RetryTimes||5;
  1135. const usedErrProcesser = ErrProcesser || (err =>{throw err});
  1136. if(!task)return;
  1137. while(true){
  1138. try{
  1139. return await task(...values);
  1140. }catch(err){
  1141. if(!--retryTimes){
  1142. console.log(err);
  1143. return usedErrProcesser(err);
  1144. }
  1145. }
  1146. }
  1147. }
  1148.  
  1149. async function BaseTranslate(name,raw,options,processer){
  1150. const toDo = async ()=>{
  1151. var tmp;
  1152. try{
  1153. const data = await Request(options);
  1154. tmp = data.responseText;
  1155. const result = await processer(tmp);
  1156. if(result)sessionStorage.setItem(name+'-'+raw,result);
  1157. return result
  1158. }catch(err){
  1159. throw {
  1160. responseText: tmp,
  1161. err: err
  1162. }
  1163. }
  1164. }
  1165. return await PromiseRetryWrap(toDo,{RetryTimes:3,ErrProcesser:()=>"翻译出错"})
  1166. }
  1167.  
  1168. function Request(options){
  1169. return new Promise((reslove,reject)=>GM_xmlhttpRequest({...options,onload:reslove,onerror:reject}))
  1170. }
  1171.  
  1172. //--异步请求包装工具--end