// ==UserScript== // @name Atcoder Better! // @namespace https://greasyfork.org/users/747162 // @version 0.12 // @description Atcoder界面汉化、题目翻译,markdown视图,一键复制题目,跳转到洛谷 // @author 北极小狐 // @match *://atcoder.jp/* // @connect www2.deepl.com // @connect m.youdao.com // @connect translate.google.com // @connect openai.api2d.net // @connect api.openai.com // @connect www.luogu.com.cn // @connect greasyfork.org // @connect * // @grant GM_xmlhttpRequest // @grant GM_info // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_setClipboard // @icon https://atcoder-cdn.oss-cn-beijing.aliyuncs.com/atcoder.png // @require https://cdn.staticfile.org/turndown/7.1.2/turndown.min.js // @require https://cdn.staticfile.org/markdown-it/13.0.1/markdown-it.min.js // @license MIT // @compatible Chrome // @compatible Firefox // @compatible Edge // @downloadURL none // ==/UserScript== // 状态与初始化 const getGMValue = (key, defaultValue) => { const value = GM_getValue(key); if (value === undefined) { GM_setValue(key, defaultValue); return defaultValue; } return value; }; const bottomZh_CN = getGMValue("bottomZh_CN", true); const translation = getGMValue("translation", "deepl"); const enableSegmentedTranslation = getGMValue("enableSegmentedTranslation", false); const showJumpToLuogu = getGMValue("showJumpToLuogu", true); const showLoading = getGMValue("showLoading", true); var x_api2d_no_cache = getGMValue("x_api2d_no_cache", true); var showOpneAiAdvanced = getGMValue("showOpneAiAdvanced", false); // 常量 const helpCircleHTML = '
'; const darkenPageStyle = `body::before { content: ""; display: block; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); z-index: 9999; }`; // 样式 GM_addStyle(` :root { --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"; } span.mdViewContent { white-space: pre-wrap; } /*翻译div*/ .translate-problem-statement { display: grid; justify-items: start; white-space: pre-wrap; letter-spacing: 1.8px; color: #059669; background-color: #f9f9fa; border: 1px solid #10b981; border-radius: 0.3rem; padding: 5px; margin: 10px 0px; width: 100%; box-sizing: border-box; font-size: 13px; } .translate-problem-statement.error_translate { color: red; border-color: red; } .translate-problem-statement h2, .translate-problem-statement h3 { font-size: 16px; } .translate-problem-statement ul { line-height: 100%; } .translate-problem-statement a { color: #10b981; font-weight: 600; background: 0 0; text-decoration: none; } .translate-problem-statement p { margin: 8px 0 !important; font-size: 14px !important; } .translate-problem-statement img { max-width: 100.0%; max-height: 100.0%; } .translate-problem-statement .katex { font-size: 14px; } .translate-problem-statement a:hover { text-decoration: revert; } .html2md-panel { display: flex; justify-content: flex-end; } .html2md-panel a { text-decoration: none; } button.html2mdButton { display: flex; align-items: center; cursor: pointer; background-color: #ffffff; color: #606266; height: 22px; width: auto; font-size: 13px; border-radius: 0.3rem; padding: 1px 5px; margin: 5px; border: 1px solid #dcdfe6; } button.html2mdButton:hover { color: #409eff; border-color: #409eff; } button.html2mdButton.copied { background-color: #f0f9eb; color: #67c23e; border: 1px solid #b3e19d; } button.html2mdButton.mdViewed { background-color: #fdf6ec; color: #e6a23c; border: 1px solid #f3d19e; } button.html2mdButton.error { background-color: #fef0f0; color: #f56c6c; border: 1px solid #fab6b6; } button.translated { cursor: not-allowed; background-color: #f0f9eb; color: #67c23e; border: 1px solid #b3e19d; } button.html2mdButton.reTranslation { background-color: #f4f4f5; color: #909399; border: 1px solid #c8c9cc; } .translate-problem-statement table { border: 1px #ccc solid; border-collapse: collapse; margin: 1.3571em 0 0; color: #222; } .translate-problem-statement table td { border-right: 1px solid #ccc; border-top: 1px solid #ccc; padding: 0.7143em 0.5em; } .translate-problem-statement table th { padding: 0.7143em 0.5em; } .translate-problem-statement p:not(:first-child) { margin: 1.5em 0 0; } /*设置面板*/ header .enter-or-register-box, header .languages { position: absolute; right: 170px; } button.html2mdButton.AtBetter_setting { float: right; height: 30px; background: #3c5a7f; color: white; margin: 10px; border: 0px; } button.html2mdButton.AtBetter_setting.open { background-color: #e6e6e61f; color: #727378; cursor: not-allowed; } #AtBetter_setting_menu { z-index: 9999; box-shadow: 0px 0px 0px 4px #ffffff; display: grid; position: fixed; top: 50%; left: 50%; width: 320px; transform: translate(-50%, -50%); border-radius: 6px; background-color: #edf1ff; border-collapse: collapse; border: 1px solid #ffffff; color: #697e91; font-family: var(--vp-font-family-base); padding: 10px 20px 20px 20px; } #AtBetter_setting_menu h3 { margin-top: 10px; } #AtBetter_setting_menu hr { border: none; height: 1px; background-color: #ccc; margin: 10px 0; } /*设置面板-关闭按钮*/ #AtBetter_setting_menu .tool-box { position: absolute; display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; top: 3px; right: 3px; } #AtBetter_setting_menu .btn-close { display: flex; align-items: center; justify-content: center; text-align: center; padding: 10px !important; width: 1px; height: 1px !important; color: transparent; font-size: 0; cursor: pointer; background-color: #ff000080; border: none; border-radius: 10px; transition: .15s ease all; } #AtBetter_setting_menu .btn-close:hover { width: 20px; height: 20px !important; font-size: 17px; color: #ffffff; background-color: #ff0000cc; box-shadow: 0 5px 5px 0 #00000026; } #AtBetter_setting_menu .btn-close:active { width: .9rem; height: .9rem; font-size: 1px; color: #ffffffde; --shadow-btn-close: 0 3px 3px 0 #00000026; box-shadow: var(--shadow-btn-close); } /*设置面板-checkbox*/ #AtBetter_setting_menu input[type=checkbox]:focus { outline: 0px; } #AtBetter_setting_menu input[type="checkbox"] { margin: 0px; appearance: none; -webkit-appearance: none; width: 40px; height: 20px !important; border: 1.5px solid #D7CCC8; padding: 0px !important; border-radius: 20px; background: #efebe978; position: relative; box-sizing: border-box; } #AtBetter_setting_menu input[type="checkbox"]::before { content: ""; width: 14px; height: 14px; background: #D7CCC8; border: 1.5px solid #BCAAA4; border-radius: 50%; position: absolute; top: 0; left: 0; transform: translate(2%, 2%); transition: all 0.3s ease-in-out; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } #AtBetter_setting_menu input[type="checkbox"]::after { 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"); position: absolute; top: 0; left: 24px; } #AtBetter_setting_menu input[type="checkbox"]:checked { border: 1.5px solid #C5CAE9; background: #E8EAF6; } #AtBetter_setting_menu input[type="checkbox"]:checked::before { background: #C5CAE9; border: 1.5px solid #7986CB; transform: translate(122%, 2%); transition: all 0.3s ease-in-out; } #AtBetter_setting_menu input[type="checkbox"]:checked::after { 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"); position: absolute; top: 1.5px; left: 4.5px; } #AtBetter_setting_menu label { font-size: 16px; font-weight: initial; margin-bottom: 0px; } .AtBetter_setting_list { display: flex; align-items: center; padding: 10px; margin: 5px 0px; background-color: #ffffff; border-bottom: 1px solid #c9c6c696; border-radius: 8px; justify-content: space-between; } /*设置面板-radio*/ #AtBetter_setting_menu>label { display: flex; list-style-type: none; padding-inline-start: 0px; overflow-x: auto; max-width: 100%; margin: 0px; align-items: center; margin: 3px 0px; } .AtBetter_setting_menu_label_text { display: flex; border: 1px dashed #00aeeccc; height: 20px; width: 100%; color: gray; font-weight: 300; font-size: 14px; letter-spacing: 2px; padding: 7px; align-items: center; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } input[type="radio"]:checked+.AtBetter_setting_menu_label_text { background: #41e49930; border: 1px solid green; color: green; font-weight: 500; } #AtBetter_setting_menu>label input[type="radio"] { -webkit-appearance: none; appearance: none; list-style: none; padding: 0px !important; margin: 0px; } #AtBetter_setting_menu input[type="text"] { display: block; height: 25px !important; width: 100%; background-color: #ffffff; color: #727378; font-size: 12px; border-radius: 0.3rem; padding: 1px 5px !important; box-sizing: border-box; margin: 5px 0px 5px 0px; border: 1px solid #00aeeccc; box-shadow: 0 0 1px #0000004d; } .AtBetter_setting_menu_input { width: 100%; display: grid; margin-top: 5px; } #AtBetter_setting_menu #save { cursor: pointer; display: inline-flex; padding: 0.5rem 1rem; background-color: #1aa06d; color: #ffffff; font-size: 1rem; line-height: 1.5rem; font-weight: 500; justify-content: center; width: 100%; border-radius: 0.375rem; border: none; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } #AtBetter_setting_menu button#debug_button.debug_button { width: 18%; } #AtBetter_setting_menu span.tip { color: #999; font-size: 12px; font-weight: 500; padding: 5px 0px; } /*设置面板-tip*/ #AtBetter_setting_menu .help_tip { margin-right: auto; } #AtBetter_setting_menu .help_tip .tip_text { display: none; position: absolute; color: #697e91; font-weight: 400; letter-spacing: 0px; background-color: #ffffff; padding: 10px; margin: 5px 0px; border-radius: 4px; border: 1px solid #e4e7ed; box-shadow: 0px 0px 12px rgba(0, 0, 0, .12); z-index: 999; } #AtBetter_setting_menu .help_tip .tip_text p { margin-bottom: 5px; } #AtBetter_setting_menu .help_tip .tip_text:before { content: ""; position: absolute; top: -20px; right: -10px; bottom: -10px; left: -10px; z-index: -1; } #AtBetter_setting_menu .help-icon { display: flex; cursor: help; width: 15px; color: rgb(255, 153, 0); margin-left: 5px; } #AtBetter_setting_menu .AtBetter_setting_menu_label_text .help_tip .help-icon { color: #7fbeb2; } #AtBetter_setting_menu .help_tip .help-icon:hover + .tip_text, #AtBetter_setting_menu .help_tip .tip_text:hover { display: block; cursor: help; width: 250px; } /*设置面板-展开*/ #is_showOpneAiAdvanced{ width: 100%; background-color: aliceblue; padding: 8px; box-sizing: border-box; border-radius: 10px; } /*确认弹窗*/ .wordsExceeded { z-index: 99999; box-shadow: 0px 0px 5px 1px rgb(0 0 0 / 10%), 0 10px 10px -5px rgba(0, 0, 0, 0.04); display: grid; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); border-radius: 4px; background-color: #ffffff; border: 1px solid #e4e7ed; color: #697e91; font-family: var(--vp-font-family-base); padding: 10px 20px 20px 20px; } .wordsExceeded button { display: inline-flex; justify-content: center; align-items: center; line-height: 1; white-space: nowrap; cursor: pointer; text-align: center; box-sizing: border-box; outline: none; transition: .1s; user-select: none; vertical-align: middle; -webkit-appearance: none; height: 24px; padding: 5px 11px; font-size: 12px; border-radius: 4px; color: #ffffff; background: #409eff; border-color: #409eff; border: none; margin-right: 12px; } .wordsExceeded button:hover{ background-color:#79bbff; } .wordsExceeded .help-icon { margin: 0px 8px 0px 0px; height: 1em; width: 1em; line-height: 1em; display: inline-flex; justify-content: center; align-items: center; position: relative; fill: currentColor; font-size: inherit; } .wordsExceeded p { margin: 5px 0px; } /*更新检查*/ div#update_panel { z-index: 9999; position: fixed; top: 50%; left: 50%; width: 240px; transform: translate(-50%, -50%); box-shadow: 0px 0px 4px 0px #0000004d; padding: 10px 20px 20px 20px; color: #444242; background-color: #f5f5f5; border: 1px solid #848484; border-radius: 8px; } div#update_panel #updating { cursor: pointer; display: inline-flex; padding: 0px; background-color: #1aa06d; color: #ffffff; font-size: 1rem; line-height: 1.5rem; font-weight: 500; justify-content: center; width: 100%; border-radius: 0.375rem; border: none; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } div#update_panel #updating a { text-decoration: none; color: white; display: flex; position: inherit; top: 0; left: 0; width: 100%; height: 22px; font-size: 14px; justify-content: center; align-items: center; } `); // 更新检查 (function checkScriptVersion() { function compareVersions(version1 = "0", version2 = "0") { const v1Array = String(version1).split("."); const v2Array = String(version2).split("."); const minLength = Math.min(v1Array.length, v2Array.length); let result = 0; for (let i = 0; i < minLength; i++) { const curV1 = Number(v1Array[i]); const curV2 = Number(v2Array[i]); if (curV1 > curV2) { result = 1; break; } else if (curV1 < curV2) { result = -1; break; } } if (result === 0 && v1Array.length !== v2Array.length) { const v1IsBigger = v1Array.length > v2Array.length; const maxLenArray = v1IsBigger ? v1Array : v2Array; for (let i = minLength; i < maxLenArray.length; i++) { const curVersion = Number(maxLenArray[i]); if (curVersion > 0) { v1IsBigger ? result = 1 : result = -1; break; } } } return result; } GM_xmlhttpRequest({ method: "GET", url: "https://greasyfork.org/zh-CN/scripts/471106.json", timeout: 10 * 1e3, onload: function (response) { const scriptData = JSON.parse(response.responseText); if (scriptData.name === GM_info.script.name && compareVersions(scriptData.version, GM_info.script.version) === 1) { const styleElement = GM_addStyle(darkenPageStyle); $("body").append(`

${GM_info.script.name}有新版本!


版本信息:${GM_info.script.version} → ${scriptData.version}

`); } } }); })(); // 汉化替换 (function () { if (!bottomZh_CN) return; // 文本节点遍历替换 $(document).ready(function () { function traverseTextNodes(node, rules) { if (!node) return; if (node.nodeType === Node.TEXT_NODE) { rules.forEach(rule => { const regex = new RegExp(rule.match, 'g'); node.textContent = node.textContent.replace(regex, rule.replace); }); } else { $(node).contents().each((_, child) => traverseTextNodes(child, rules)); } } const rules1 = [ { match: 'Present Contests', replace: '目前的比赛' }, { match: 'Past Contests', replace: '过去的比赛' }, { match: 'Home', replace: '主页' }, { match: 'Contest', replace: '比赛' }, { match: 'Ranking', replace: '排名' }, { match: 'Sign Up', replace: '注册' }, { match: 'Sign In', replace: '登录' }, { match: 'Top', replace: '首页' }, { match: 'Tasks', replace: '问题集' }, { match: 'Clarifications', replace: '问题答疑' }, { match: 'Submit', replace: '提交' }, { match: 'Results', replace: '结果' }, { match: 'All Submissions', replace: '所有提交' }, { match: 'My Submissions', replace: '我的提交' }, { match: 'My Score', replace: '我的得分' }, { match: 'Virtual Standings', replace: '虚拟排名' }, { match: 'Standings', replace: '排名' }, { match: 'Custom Test', replace: '自定义测试' }, { match: 'Editorial', replace: '题解' }, { match: 'Discuss', replace: '讨论' }, { match: 'Algorithm', replace: '算法' }, { match: 'Heuristic', replace: '启发式' }, { match: 'Active Users', replace: '活跃用户' }, { match: 'All Users', replace: '所有用户' }, { match: 'Profile', replace: '个人资料' }, { match: 'Competition History', replace: '比赛记录' }, { match: 'General Settings', replace: '常规设置' }, { match: 'Settings', replace: '设置' }, { match: 'Change/Verify Email address', replace: '更改/验证电子邮件地址' }, { match: 'Remind Username', replace: '提醒用户名' }, { match: 'Change Username', replace: '更改用户名' }, { match: 'Delete Account', replace: '删除账户' }, { match: 'Change Photo', replace: '更改照片' }, { match: 'Change Password', replace: '更改密码' }, { match: 'Manage Fav', replace: '管理收藏' }, { match: 'Other', replace: '其他' }, { match: 'Remind Username', replace: '提醒用户名' }, { match: 'Change Username', replace: '更改用户名' }, { match: 'Delete Account', replace: '删除账户' } ]; traverseTextNodes($('.nav'), rules1); const rules2 = [ { match: 'My Profile', replace: '个人资料' }, { match: 'General Settings', replace: '常规设置' }, { match: 'Change Photo', replace: '更改照片' }, { match: 'Change Password', replace: '更改密码' }, { match: 'Manage Fav', replace: '管理收藏' }, { match: 'Sign Out', replace: '退出登录' } ]; traverseTextNodes($('.dropdown-menu'), rules2); const rules3 = [ { match: 'Search in Archive', replace: '搜索存档' }, { match: 'Permanent Contests', replace: '永久比赛' }, { match: 'Upcoming Contests', replace: '即将举行的比赛' }, { match: 'Recent Contests', replace: '最近的比赛' }, { match: 'Ranking', replace: '排行' }, { match: 'Contest Archive', replace: '比赛档案' }, { match: 'Information', replace: '信息' }, { match: 'About the situation where it is difficult to access the contest site', replace: '关于难以访问比赛网站的情况' }, ]; traverseTextNodes($('.panel-title'), rules3); traverseTextNodes($('.h3'), rules3); const rules4 = [ { match: 'Rated Range', replace: '限定范围' }, { match: 'Category', replace: '类别' }, { match: 'Search', replace: '搜索' } ]; traverseTextNodes($('.filter-body-heading'), rules4); const rules5 = [ { match: 'Current Password', replace: '当前密码' }, { match: 'New Password', replace: '新密码' }, { match: 'Confirm Password', replace: '确认密码' }, { match: 'Update', replace: '更新' }, { match: 'Contest Name', replace: '比赛名称' }, { match: 'Username', replace: '用户名' }, { match: 'Password', replace: '密码' }, { match: 'Sign In', replace: '登录' }, { match: 'Sign Up', replace: '注册' }, { match: 'Nickname', replace: '昵称' }, { match: 'Country/Region', replace: '国家/地区' }, { match: 'Birth Year', replace: '出生年份' }, { match: 'Affiliation', replace: '机构' }, { match: 'Email Notifications', replace: '邮件通知' }, { match: 'New Email address', replace: '新电子邮件地址' }, { match: 'Request Email address verify', replace: '请求电子邮件地址验证' }, { match: 'I agree.', replace: '我同意。' }, { match: 'Do you live in Japan?', replace: '您是否居住在日本?' }, { match: 'Family Name', replace: '姓氏' }, { match: 'First Name', replace: '名字' }, { match: 'Category', replace: '分类' }, { match: 'College Students (Master or Doctor cource)', replace: '大学生(硕士或博士课程)' }, { match: 'College Students', replace: '大学生' }, { match: 'Technical college/Vocational school/Short-term university', replace: '技术学院/职业学校/短期大学' }, { match: 'High school', replace: '高中' }, { match: 'Junior high school', replace: '初中' }, { match: 'Office worker', replace: '上班族' }, { match: 'Other', replace: '其他' }, { match: 'Organization Name \\(Company Name or School Name\\)', replace: '组织名称(公司名称或学校名称)' }, { match: 'Depertment \\(For Students\\)', replace: '部门(适用于学生)' }, { match: 'Do you have any intention or plan to find a job or change jobs in 2023 or 2024?', replace: '您是否有意向或计划在2023年或2024年找工作或换工作?' }, { match: 'Graduation Schedule', replace: '毕业时间表' }, { match: "I'm already employed.", replace: '我已经就业了。' }, { match: 'Later years', replace: '以后的几年' }, { match: 'I am interested in going into the digital area of Toyota Motor Corporation\'s operations.', replace: '我对加入丰田汽车公司的数字领域感兴趣。' }, { match: 'Toyota is currently actively recruiting engineers. Would you like to be considered?', replace: '丰田目前正在积极招聘工程师。您有兴趣被考虑吗?' }, { match: 'I\'d like to talk to you first.', replace: '我想先和您交谈。' }, { match: 'Department name', replace: '部门名称' }, { match: 'What kind of work do you currently do?', replace: '您目前从事什么样的工作?' }, { match: 'How can the Algorithms Group of the Digital Transformation Office help\\?', replace: '数字转型办公室的算法组可以如何帮助您?' } ]; traverseTextNodes($('.form-group'), rules5); const rules6 = [ { match: 'Unofficial(unrated)', replace: '非官方(无评级)' }, { match: 'Sponsored Parallel(rated)', replace: '赞助平行(有评级)' }, { match: 'Sponsored Parallel(unrated)', replace: '赞助平行(无评级)' }, { match: 'Sponsored Heuristic Contest', replace: '赞助启发式比赛' }, { match: 'All', replace: '全部' }, { match: 'AtCoder Typical Contest', replace: 'AtCoder 典型比赛' }, { match: 'PAST Archive', replace: 'PAST 比赛归档' }, { match: 'JOI Archive', replace: 'JOI 比赛归档' }, { match: 'Sponsored Tournament', replace: '赞助比赛' }, { match: 'Sponsored ABC', replace: '赞助 ABC' }, { match: 'Sponsored ARC', replace: '赞助 ARC' }, { match: 'Heuristic Contest', replace: '启发式比赛' } ]; traverseTextNodes($('#category-btn-group'), rules6); const rules7 = [ { match: 'Task', replace: '任务' }, { match: 'Language', replace: '语言' }, { match: 'Source Code', replace: '源代码' }, { match: 'Standard Input', replace: '标准输入' }, { match: 'Standard Output', replace: '标准输出' }, { match: 'Standard Error', replace: '标准错误' }, ]; traverseTextNodes($('.control-label'), rules7); const rules8 = [ { match: 'Permanent Contests', replace: '永久比赛' }, { match: 'Upcoming Contests', replace: '即将举行的比赛' }, { match: 'Recent Contests', replace: '最近的比赛' } ]; traverseTextNodes($('h4'), rules8); const rules9 = [ { match: 'Open File', replace: '打开文件' }, { match: 'Toggle Editor', replace: '切换编辑器' }, { match: 'Auto Height', replace: '自动调整高度' } ]; traverseTextNodes($('.editor-buttons'), rules9); const rules10 = [ { match: 'Register', replace: '报名' }, { match: 'Virtual Participation', replace: '虚拟参加' } ]; traverseTextNodes($('.btn'), rules10); }); })(); // 设置面板 $(document).ready(function () { var htmlContent = ""; $('#navbar-collapse > ul:nth-child(2) > li:last-child').after(""); }); $(document).ready(function () { const $settingBtns = $(".AtBetter_setting"); $settingBtns.click(() => { const styleElement = GM_addStyle(darkenPageStyle); $settingBtns.prop("disabled", true).addClass("open"); $("body").append(`

基本设置


`+ helpCircleHTML + `

当你开启 显示加载信息 时,每次加载页面时会在上方显示加载信息提示:“Atcoder Better! —— xxx”

这用于观察脚本当前的工作情况,如果你不想看到,可以选择关闭

需要说明的是,如果你需要反馈脚本的任何加载问题,请开启该选项后再截图,以便于分析问题

`+ helpCircleHTML + `

分段翻译会对区域内的每一个<p/>和<i/>标签依次进行翻译,

这通常在翻译长篇博客或者超长的题目时很有用。

注意:开启分段翻译会产生如下问题:

- 使得翻译接口无法知晓整个文本的上下文信息,会降低翻译质量。

- 会有部分内容不会被翻译,因为它们不是<p/>或<i/>标签

`+ helpCircleHTML + `

洛谷OJ上收录了Atcoder的部分题目,一些题目有翻译和题解

开启显示后,如果当前题目被收录,则会在题目的右上角显示洛谷标志,

点击即可一键跳转到该题洛谷的对应页面。

翻译设置



`); $("#bottomZh_CN").prop("checked", GM_getValue("bottomZh_CN") === true); $("#showLoading").prop("checked", GM_getValue("showLoading") === true); $("#enableSegmentedTranslation").prop("checked", GM_getValue("enableSegmentedTranslation") === true); $("#showJumpToLuogu").prop("checked", GM_getValue("showJumpToLuogu") === true); $("#x_api2d_no_cache").prop("checked", GM_getValue("x_api2d_no_cache") === true); $("#showOpneAiAdvanced").prop("checked", GM_getValue("showOpneAiAdvanced") === true); $("input[name='translation'][value='" + translation + "']").prop("checked", true); $("input[name='translation']").css("color", "gray"); if (translation == "openai") { $("#openai").show(); $("#openai_key").val(GM_getValue("openai_key")); $("#openai_proxy").val(GM_getValue("openai_proxy")); $("#openai_key").css("color", "gray"); } else if (translation == "api2d") { $("#api2d").show(); $("#api2d_key").val(GM_getValue("api2d_key")); $("#api2d_key").css("color", "gray"); } // 当单选框被选中时,显示对应的输入框,同时隐藏其他输入框 $("input[name='translation']").change(function () { var selected = $(this).val(); // 获取当前选中的值 if (selected === "openai") { $("#openai").show(); $("#openai_key").val(GM_getValue("openai_key")); $("#showOpneAiAdvanced").prop("checked", showOpneAiAdvanced); if (showOpneAiAdvanced) { $("#is_showOpneAiAdvanced").show(); $("#openai_proxy").val(GM_getValue("openai_proxy")); } else $("#is_showOpneAiAdvanced").hide(); $("#api2d").hide(); } else if (selected === "api2d") { $("#api2d").show(); $("#api2d_key").val(GM_getValue("api2d_key")); $("#x_api2d_no_cache").prop("checked", GM_getValue("x_api2d_no_cache")); $("#openai").hide(); } else { $("#openai, #api2d").hide(); } }); // ChatGPT高级选项 $("input[name='showOpneAiAdvanced']").change(function () { var isChecked = $(this).is(":checked"); if (isChecked) { $("#is_showOpneAiAdvanced").show(); } else { $("#is_showOpneAiAdvanced").hide(); } }); const $settingMenu = $("#AtBetter_setting_menu"); $("#save").click(function () { GM_setValue("bottomZh_CN", $("#bottomZh_CN").prop("checked")); GM_setValue("showLoading", $("#showLoading").prop("checked")); GM_setValue("enableSegmentedTranslation", $("#enableSegmentedTranslation").prop("checked")); GM_setValue("showJumpToLuogu", $("#showJumpToLuogu").prop("checked")); var translation = $("input[name='translation']:checked").val(); var openai_key = $("#openai_key").val(); var openai_proxy = $("#openai_proxy").val(); var api2d_key = $("#api2d_key").val(); GM_setValue("translation", translation); if (translation == "openai") { GM_setValue("openai_key", openai_key); GM_setValue("openai_proxy", openai_proxy); } else if (translation == "api2d") { GM_setValue("api2d_key", api2d_key); GM_setValue("x_api2d_no_cache", $("#x_api2d_no_cache").prop("checked")); } $settingMenu.remove(); $(styleElement).remove(); location.reload(); }); // 关闭 $settingMenu.on("click", ".btn-close", () => { $settingMenu.remove(); $settingBtns.prop("disabled", false).removeClass("open"); $(styleElement).remove(); }); }); }); // html2md转换/处理规则 var turndownService = new TurndownService({ bulletListMarker: '-', escape: (text) => text }); var turndown = turndownService.turndown; // 保留原始 turndownService.keep(['del']); turndownService.addRule('removeByClass', { filter: function (node) { return node.classList.contains('html2md-panel') || node.classList.contains('div-btn-copy') || node.classList.contains('btn-copy') }, replacement: function () { return ''; } }); // inline math turndownService.addRule('inline-math', { filter: function (node, options) { return node.tagName.toLowerCase() == "span" && node.className == "katex"; }, replacement: function (content, node) { return "$" + $(node).find('annotation').text() + "$"; } }); // block math turndownService.addRule('block-math', { filter: function (node, options) { return node.tagName.toLowerCase() == "span" && node.className == "katex-display"; }, replacement: function (content, node) { return "\n$$\n" + $(node).find('annotation').text() + "\n$$\n"; } }); // pre turndownService.addRule('pre', { filter: function (node, options) { return node.tagName.toLowerCase() == "pre"; }, replacement: function (content, node) { return "```\n" + content + "```\n"; } }); // bordertable turndownService.addRule('bordertable', { filter: 'table', replacement: function (content, node) { if (node.classList.contains('table')) { var output = [], thead = '', trs = node.querySelectorAll('tr'); if (trs.length > 0) { var ths = trs[0].querySelectorAll('th'); if (ths.length > 0) { thead = '| ' + Array.from(ths).map(th => turndownService.turndown(th.innerHTML.trim())).join(' | ') + ' |\n' + '| ' + Array.from(ths).map(() => ' --- ').join('|') + ' |\n'; } } var rows = node.querySelectorAll('tr'); Array.from(rows).forEach(function (row, i) { if (i > 0) { var cells = row.querySelectorAll('td,th'); var trow = '| ' + Array.from(cells).map(cell => turndownService.turndown(cell.innerHTML.trim())).join(' | ') + ' |'; output.push(trow); } }); return thead + output.join('\n'); } else { return content; } } }); // 随机数生成 function getRandomNumber(numDigits) { let min = Math.pow(10, numDigits - 1); let max = Math.pow(10, numDigits) - 1; return Math.floor(Math.random() * (max - min + 1)) + min; } // 题目markdown转换/翻译面板 function addButtonPanel(parent, suffix, type, is_simple = false) { let htmlString = `
`; if (type === "this_level") { $(parent).before(htmlString); } else if (type === "child_level") { $(parent).prepend(htmlString); } if (is_simple) { $('.html2md-panel').find('.html2mdButton.html2md-view' + suffix + ', .html2mdButton.html2md-cb' + suffix).remove(); } } function addButtonWithHTML2MD(parent, suffix, type) { $(document).on("click", ".html2md-view" + suffix, function () { var target, removedChildren = $(); if (type === "this_level") { target = $(".html2md-view" + suffix).parent().next().get(0); } else if (type === "child_level") { target = $(".html2md-view" + suffix).parent().parent().get(0); removedChildren = $(".html2md-view" + suffix).parent().parent().children(':first').detach(); } if (target.viewmd) { target.viewmd = false; $(this).text("MarkDown视图"); $(this).removeClass("mdViewed"); $(target).html(target.original_html); } else { target.viewmd = true; if (!target.original_html) { target.original_html = $(target).html(); } if (!target.markdown) { target.markdown = turndownService.turndown($(target).html()); } $(this).text("原始内容"); $(this).addClass("mdViewed"); $(target).html(`${target.markdown}`); } // 恢复删除的元素 if (removedChildren) $(target).prepend(removedChildren); }); } function addButtonWithCopy(parent, suffix, type) { $(document).on("click", ".html2md-cb" + suffix, function () { let target, removedChildren, text; if (type === "this_level") { target = $(".translateButton" + suffix).parent().next().eq(0).clone(); } else if (type === "child_level") { target = $(".translateButton" + suffix).parent().parent().eq(0).clone(); $(target).children(':first').remove(); } if ($(target).find('.mdViewContent').length <= 0) { text = turndownService.turndown($(target).html()); } else { text = $(target).find('.mdViewContent').text(); } GM_setClipboard(text); $(this).addClass("copied"); $(this).text("Copied"); // 更新复制按钮文本 setTimeout(() => { $(this).removeClass("copied"); $(this).text("Copy"); }, 2000); $(target).remove(); }); } async function addButtonWithTranslation(parent, suffix, type) { $(document).on('click', '.translateButton' + suffix, async function () { $(this).removeClass("translated"); $(this).text("翻译中"); $(this).css("cursor", "not-allowed"); var target, element_node, block, result, errerNum = 0; if (type === "this_level") block = $(".translateButton" + suffix).parent().next(); else if (type === "child_level") block = $(".translateButton" + suffix).parent().parent(); // 分段翻译 if (enableSegmentedTranslation) { var pElements = block.find("p, li"); for (let i = 0; i < pElements.length; i++) { target = $(pElements[i]).eq(0).clone(); if (type === "child_level") $(target).children(':first').remove(); element_node = pElements[i]; if (type === "child_level") { $(pElements[i]).append("
"); element_node = $(pElements[i]).find("div:last-child").get(0); } result = await blockProcessing(target, element_node, $(".translateButton" + suffix)); if (result.status) errerNum += 1; $(target).remove(); if (translation == "deepl") await new Promise(resolve => setTimeout(resolve, 2000)); } } else { target = block.eq(0).clone(); if (type === "child_level") $(target).children(':first').remove(); element_node = $(block).get(0); if (type === "child_level") { $(parent).append("
"); element_node = $(parent).find("div:last-child").get(0); } //是否跳过折叠块 if ($(target).find('details').length > 0) { const shouldSkip = await skiFoldingBlocks(); if (shouldSkip) { $(target).find('details').remove(); } else { $(target).find('.html2md-panel').remove(); } } //跳过代码块 $(target).find('pre').remove(); result = await blockProcessing(target, element_node, $(".translateButton" + suffix)); if (result.status) errerNum += 1; $(target).remove(); } if (!errerNum) { $(this).addClass("translated") .text("已翻译") .prop("disabled", true); } // 重新翻译按钮 if ($(this).next('.reTranslation').length === 0) { const reTranslateBtn = $('`); problemLink.appendTo('.h2'); } } $(document).ready(function () { var newElement = $("
").addClass("alert alert-info").html(` Atcoder Better! —— 正在等待页面资源加载…… `).css({ "margin": "1em", "text-align": "center", "font-weight": "600", "position": "relative" }); var tip_SegmentedTranslation = $("
").addClass("alert alert-danger").html(` Atcoder Better! —— 注意!分段翻译已开启,这会造成负面效果,

除非你现在需要翻译超长篇的博客或者题目,否则请前往设置关闭分段翻译

`).css({ "margin": "1em", "text-align": "center", "font-weight": "600", "position": "relative" }); if (showLoading) $('#main-container').prepend(newElement); // 页面完全加载完成后执行 window.onload = function () { if (enableSegmentedTranslation) $('#main-container').prepend(tip_SegmentedTranslation); //显示分段翻译警告 if (showLoading) { newElement.html('Atcoder Better! —— 正在处理中……'); newElement.removeClass('alert-info').addClass('alert-success'); } if (showJumpToLuogu) At2luogu(); addConversionButton(); if (showLoading){ newElement.html('Atcoder Better! —— 加载已完成'); setTimeout(function () { newElement.remove(); }, 3000); } } }) // 字数超限确认 function showWordsExceededDialog(button) { return new Promise(resolve => { const styleElement = GM_addStyle(darkenPageStyle); $(button).removeClass("translated"); $(button).text("字数超限"); $(button).css("cursor", "not-allowed"); $(button).prop("disabled", true); let htmlString = `

字数超限!

注意,即将翻译的内容字数超过了4950个字符,您可能选择了错误的翻译按钮

`+ helpCircleHTML + `

由于实现方式,区域中会出现多个翻译按钮,请点击更小的子区域中的翻译按钮,
或者在设置面板中开启 分段翻译 后重试。

对于免费的接口,大量请求可能导致你的IP被暂时禁止访问,对于GPT,会消耗大量的token

您确定要继续翻译吗?

`; $('body').before(htmlString); $("#continueButton").click(function () { $(styleElement).remove(); $('.wordsExceeded').remove(); resolve(true); }); $("#cancelButton").click(function () { $(styleElement).remove(); $('.wordsExceeded').remove(); resolve(false); }); }); } // 跳过折叠块确认 function skiFoldingBlocks() { return new Promise(resolve => { const styleElement = GM_addStyle(darkenPageStyle); let htmlString = `

是否跳过折叠块?

即将翻译的区域中包含折叠块,可能不需要翻译,现在您需要选择是否跳过这些折叠块,

如果其中有您需要翻译的折叠块,可以稍后再单独点击这些折叠块内的翻译按钮进行翻译

要跳过折叠块吗?(建议选择跳过)

`; $('body').before(htmlString); $("#skipButton").click(function () { $(styleElement).remove(); $('.wordsExceeded').remove(); resolve(true); }); $("#cancelButton").click(function () { $(styleElement).remove(); $('.wordsExceeded').remove(); resolve(false); }); }); } // 翻译框/翻译处理器 var translatedText = ""; async function translateProblemStatement(text, element_node, button) { let status = 0; let id = getRandomNumber(8); let matches = []; let replacements = {}; // 创建元素并放在element_node的后面 const translateDiv = document.createElement('div'); translateDiv.setAttribute('id', id); translateDiv.classList.add('translate-problem-statement'); const spanElement = document.createElement('span'); translateDiv.appendChild(spanElement); element_node.insertAdjacentElement('afterend', translateDiv); // 替换latex公式 if (translation != "api2d" && translation != "openai") { // 使用GPT翻译时不必替换latex公式 let i = 0; // 块公式 matches = matches.concat(text.match(/\$\$([\s\S]*?)\$\$/g)); try { for (i; i < matches.length; i++) { let match = matches[i]; text = text.replace(match, `【${i + 1}】`); replacements[`【${i + 1}】`] = match; } } catch (e) { } // 行内公式 matches = matches.concat(text.match(/\$(.*?)\$/g)); try { for (i; i < matches.length; i++) { let match = matches[i]; text = text.replace(match, `【${i + 1}】`); replacements[`【${i + 1}】`] = match; } } catch (e) { } } if (text.length > 4950) { const shouldContinue = await showWordsExceededDialog(button); if (!shouldContinue) { status = 1; return { translateDiv: translateDiv, status: status }; } } // 翻译 if (translation == "deepl") { translateDiv.textContent = "正在翻译中……请稍等"; translatedText = await translate_deepl(text); } else if (translation == "youdao") { translateDiv.textContent = "正在翻译中……请稍等"; translatedText = await translate_youdao_mobile(text); } else if (translation == "google") { translateDiv.textContent = "正在翻译中……请稍等"; translatedText = await translate_gg(text); } else if (translation == "openai") { translateDiv.textContent = "正在翻译中……\n\n使用GPT(ChatGPT/api2d)进行翻译通常需要很长的时间,请耐心等待"; translatedText = await translate_openai(text); } else if (translation == "api2d") { translateDiv.textContent = "正在翻译中……\n\n使用GPT(ChatGPT/api2d)进行翻译通常需要很长的时间,请耐心等待"; translatedText = await translate_api2d(text); } if (/^翻译出错/.test(translatedText)) status = 2; // 还原latex公式 if (translation != "api2d" && translation != "openai") { try { for (let i = 0; i < matches.length; i++) { let match = matches[i]; let replacement = replacements[`【${i + 1}】`]; let regex; regex = new RegExp(`【${i + 1}】`, 'g'); translatedText = translatedText.replace(regex, replacement); regex = new RegExp(`\\[${i + 1}\\]`, 'g'); translatedText = translatedText.replace(regex, replacement); regex = new RegExp(`【${i + 1}[^】\\d]`, 'g'); translatedText = translatedText.replace(regex, replacement); regex = new RegExp(`[^【\\d]${i + 1}】`, 'g'); translatedText = translatedText.replace(regex, " " + replacement); } } catch (e) { } } // 创建一个隐藏的元素来保存 translatedText 的值 var textElement = document.createElement("div"); textElement.style.display = "none"; textElement.textContent = translatedText; translateDiv.parentNode.insertBefore(textElement, translateDiv); // 翻译复制按钮 var copyButton = document.createElement("button"); copyButton.textContent = "Copy"; $(copyButton).addClass("html2mdButton html2md-cb"); $(copyButton).css({ "float": "right", }); copyButton.addEventListener("click", function () { var translatedText = textElement.textContent; GM_setClipboard(translatedText); $(this).addClass("copied"); $(this).text("Copied"); // 更新复制按钮文本 setTimeout(() => { $(this).removeClass("copied"); $(this).text("Copy"); }, 2000); }); translateDiv.parentNode.insertBefore(copyButton, translateDiv); // 更新 translateDiv.innerHTML = translatedText; // 渲染MarkDown var md = window.markdownit(); var html = md.render(translateDiv.innerText); translateDiv.innerHTML = html; // 渲染Latex if (typeof renderMathInElement === 'function') { renderMathInElement(document.getElementById(id), { delimiters: [{ left: "$$", right: "$$", display: true }, { left: "$", right: "$", display: false }] }); } return { translateDiv: translateDiv, status: status, copyDiv: textElement, copyButton: copyButton }; } // ChatGPT async function translate_openai(raw) { var openai_key = GM_getValue("openai_key"); var openai_retext = ""; var data = { prompt: "(You:请帮我将下面的文本翻译为中文,这是一个编程竞赛题描述的一部分,注意术语的翻译,注意保持其中的latex公式不翻译,你只需要回复翻译后的内容即可,不要回复任何其他内容:\n\n" + raw + ")", model: "gpt-3.5-turbo", temperature: 0.7 }; return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: 'POST', url: (showOpneAiAdvanced && GM_getValue("openai_proxy") !== null && GM_getValue("openai_proxy") !== "") ? GM_getValue("openai_proxy") : 'https://api.openai.com/v1/completions', data: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + GM_getValue("openai_key") + '' }, responseType: 'json', onload: function (res) { if (res.status === 200) { openai_retext = res.response.choices[0].text; openai_retext = openai_retext.replace(/^\s+/, ''); resolve(openai_retext); } else { resolve("翻译出错,请重试\n如果无法解决,请前往 https://greasyfork.org/zh-CN/scripts/465777/feedback 反馈\n\n报错信息:" + JSON.stringify(res.response, null, '\n')); } } }); }); } // api2d async function translate_api2d(raw) { var api2d_key = GM_getValue("api2d_key"); var api2d_retext = ""; var postData = JSON.stringify({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: '请帮我将下面的文本翻译为中文,这是一个编程竞赛题描述的一部分,注意术语的翻译,注意保持其中的latex公式不翻译,你只需要回复翻译后的内容即可,不要回复任何其他内容:\n\n' + raw }], temperature: 0.7 }); const options = { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + api2d_key, ...(x_api2d_no_cache ? {} : { 'x-api2d-no-cache': 1 }) }, data: postData, }; return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: options.method, url: `https://openai.api2d.net/v1/chat/completions`, headers: options.headers, data: options.data, responseType: 'json', onload: function (response) { if (!response.response.choices || response.response.choices.length < 1 || !response.response.choices[0].message) { resolve("翻译出错,请重试\n如果无法解决,请前往 https://greasyfork.org/zh-CN/scripts/465777/feedback 反馈\n\n报错信息:" + JSON.stringify(response.response, null, '\n')); } else { api2d_retext = response.response.choices[0].message.content; resolve(api2d_retext); } }, onerror: function (response) { console.error(response.statusText); reject(response.statusText); }, }); }); } // //--谷歌翻译--start async function translate_gg(raw) { return new Promise((resolve, reject) => { const url = 'https://translate.google.com/m'; const params = `tl=zh-CN&q=${encodeURIComponent(raw)}`; GM_xmlhttpRequest({ method: 'GET', url: `${url}?${params}`, onload: function (response) { const html = response.responseText; const translatedText = $(html).find('.result-container').text(); resolve(translatedText); }, onerror: function (error) { console.error('Error:', error); reject(error); } }); }); } //--谷歌翻译--end //--有道翻译m--start async function translate_youdao_mobile(raw) { const options = { method: "POST", url: 'http://m.youdao.com/translate', data: "inputtext=" + encodeURIComponent(raw) + "&type=AUTO", anonymous: true, headers: { "Content-Type": "application/x-www-form-urlencoded", 'Host': 'm.youdao.com', 'Origin': 'http://m.youdao.com', 'Referer': 'http://m.youdao.com/translate', } } return await BaseTranslate('有道翻译mobile', raw, options, res => /id="translateResult">\s*?
  • ([\s\S]*?)<\/li>\s*?<\/ul/.exec(res)[1]) } //--有道翻译m--end //--Deepl翻译--start function getTimeStamp(iCount) { const ts = Date.now(); if (iCount !== 0) { iCount = iCount + 1; return ts - (ts % iCount) + iCount; } else { return ts; } } async function translate_deepl(raw) { const id = (Math.floor(Math.random() * 99999) + 100000) * 1000; const data = { jsonrpc: '2.0', method: 'LMT_handle_texts', id, params: { splitting: 'newlines', lang: { source_lang_user_selected: 'auto', target_lang: 'ZH', }, texts: [{ text: raw, requestAlternatives: 3 }], timestamp: getTimeStamp(raw.split('i').length - 1) } } let postData = JSON.stringify(data); if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) { postData = postData.replace('"method":"', '"method" : "'); } else { postData = postData.replace('"method":"', '"method": "'); } const options = { method: 'POST', url: 'https://www2.deepl.com/jsonrpc', data: postData, headers: { 'Content-Type': 'application/json', 'Host': 'www2.deepl.com', 'Origin': 'https://www.deepl.com', 'Referer': 'https://www.deepl.com/', }, anonymous: true, nocache: true, } return await BaseTranslate('Deepl翻译', raw, options, res => JSON.parse(res).result.texts[0].text) } //--Deepl翻译--end //--异步请求包装工具--start async function PromiseRetryWrap(task, options, ...values) { const { RetryTimes, ErrProcesser } = options || {}; let retryTimes = RetryTimes || 5; const usedErrProcesser = ErrProcesser || (err => { throw err }); if (!task) return; while (true) { try { return await task(...values); } catch (err) { if (!--retryTimes) { console.log(err); return usedErrProcesser(err); } } } } async function BaseTranslate(name, raw, options, processer) { let errtext; const toDo = async () => { var tmp; try { const data = await Request(options); tmp = data.responseText; const result = await processer(tmp); if (result) sessionStorage.setItem(name + '-' + raw, result); return result } catch (err) { errtext = tmp; throw { responseText: tmp, err: err } } } return await PromiseRetryWrap(toDo, { RetryTimes: 3, ErrProcesser: () => "翻译出错,请重试或更换翻译接口\n如果无法解决,请前往 https://greasyfork.org/zh-CN/scripts/465777/feedback 反馈\n\n报错信息:" + errtext }) } function Request(options) { return new Promise((reslove, reject) => GM_xmlhttpRequest({ ...options, onload: reslove, onerror: reject })) } //--异步请求包装工具--end