atcoder better
// UserScript
// @name Atcoder Better!
// @namespace https://greasyfork.org/users/747162
// @version 1.19.0
// @description 一个适用于 AtCoder 的 Tampermonkey 脚本,增强功能与界面。
// @author 北极小狐
// @match 😕/atcoder.jp/
// @run-at document-start
// @connect www2.deepl.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect api.deeplx.org
// @connect www.iflyrec.com
// @connect dict.youdao.com
// @connect api.interpreter.caiyunai.com
// @connect translate.google.com
// @connect openai.api2d.net
// @connect api.openai.com
// @connect www.luogu.com.cn
// @connect vjudge.net
// @connect clist.by
// @connect greasyfork.org
// @connect sustech.edu.cn
// @connect aowuucdn.oss-cn-beijing.aliyuncs.com
// @connect aowuucdn.oss-accelerate.aliyuncs.com
// @connect 127.0.0.1
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_getResourceText
// @icon https://aowuucdn.oss-accelerate.aliyuncs.com/atcoder.png
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/turndown/7.2.0/turndown.min.js#sha512-sJzEecN5Nk8cq81zKtGq6/z9Z/r3q38zV9enY75IVxiG7ybtlNUt864sL4L1Kf36bYIwxTMVKQOtU4VhD7hGrw==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/markdown-it/13.0.2/markdown-it.js#sha512-2LtYcLGnCbAWz9nDIrfG2pHFiFu9n+3oGecQlzLuYsLgen/oxiYscGWnDST9J9EZanlsQkDD0ZP2n/6peDuALQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha512-a+SUDuwNzXDvz4XrIcXHuCf089/iJAoN4lmrXJg18XnduKK6YlDHNRalv4yd1N40OKI80tFidF+rqTFKGPoWFQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/chroma-js/2.4.2/chroma.min.js#sha512-zInFF17qBFVvvvFpIfeBzo7Tj7+rQxLeTJDmbxjBz5/zIr89YVbTNelNhdTT+/DCrxoVzBeUPVFJsczKbB7sew==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/xterm/5.5.0/xterm.js#sha512-Gujw5GajF5is3nMoGv9X+tCMqePLL/60qvAv1LofUZTV9jK8ENbM9L+maGmOsNzuZaiuyc/fpph1KT9uR5w3CQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dexie/4.0.7/dexie.min.js#sha512-882VotT07mOQRzqIxsyxHzJX0XUaoeee3qXp4THg1A0KI0XFnWFAaLFQm0x6OW3pHSIipVZW+gzQ1w9b6uvkVw==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/i18next/23.11.5/i18next.min.js#sha512-3RSGkmT48HnO+hlmzGYDx5/w2LIBX0O5hSuYX6KWAxmvVlSjFgoxIaWa2tlMExheGvt3lLyxeTsXfpC47yb8CQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/i18next-http-backend/2.5.2/i18nextHttpBackend.min.js#sha512-bBb+wrGRTx4MvHpksYb1Iv5oJ1o8ineCqpc0cnTgdJQhuAFJJ93SEVXxUOCptvt0vAqYdjzWO5emorYUBt6Ceg==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/jquery-i18next/1.2.1/jquery-i18next.min.js#sha512-79RgNpOyaf8AvNEUdanuk1x6g53UPoB6Fh2uogMkOMGADBG6B0DCzxc+dDktXkVPg2rlxGvPeAFKoZxTycVooQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dialog-polyfill/0.5.6/dialog-polyfill.min.js#sha512-qUIG93zKzcLBVD5RGRbx2PBmbVRu+tJIl+EPLTus0z8I1AMru9sQYdlf6cBacSzYmZVncB9rcc8rYBnazqgrxA==
// @resource acwing_cpp_code_completer https://aowuucdn.oss-accelerate.aliyuncs.com/acwing_cpp_code_completer-0.0.11.json#sha512-DQVpao4qMMExToRdid0g/S0nbO/C9hwCECjI5aW8A0g7nvi8hEcD2Lw3QIqdJBV7haP15oJOocfwuiw7ryTO9w==
// @resource wandboxlist https://wandbox.org/api/list.json
// @resource xtermcss https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/xterm/5.5.0/xterm.min.css#sha512-XpXUuzg5afNt1bsgnrOesXP70TLH8tXYYK5sK+Y0UV+YBvJn9EfRFYWy4HT3TVDfH0nl1CO0lwOxIrt2gk9qjg==
// @resource selectpagecss https://aowuucdn.oss-accelerate.aliyuncs.com/css/selectpage.css#sha512-cRXJfA2tEcAxHEKylJfxteY17N7j9fia3waahHOVnvl63uVZT9OQ7jjjpofZMVZ4JSX3BRET+mI8UvKnsXd3NA==
// @resource dialogpolyfillcss https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dialog-polyfill/0.5.6/dialog-polyfill.min.css#sha512-J2+1q+RsZuJXabBfH1q/fgRr6jMy9By5SwVLk7bScEW7NFJkMUXxfeOyyxtDe6fsaJ4jsciexSlGrPYn9YbBIg==
// @license GPL3
// @compatible Chrome
// @compatible Firefox
// @compatible Edge
// @incompatible safari
// @supportURL https://github.com/beijixiaohu/OJBetter/issues
// @name:zh-TW AtCoder Better!
// @name:en AtCoder Better!
// @name:de AtCoder Better!
// @name:fr AtCoder Better!
// @name:ko AtCoder Better!
// @name:pt AtCoder Better!
// @name:ja AtCoder Better!
// @name:es AtCoder Better!
// @name:it AtCoder Better!
// @name:hi AtCoder Better!
// @description 一个适用于 AtCoder 的 Tampermonkey 脚本,增强功能与界面。
// @description:zh-TW 一個適用於 AtCoder 的 Tampermonkey 腳本,增強功能與界面。
// @description:en A Tampermonkey script for AtCoder that enhances functionality and interface.
// @description:de Ein Tampermonkey-Skript für AtCoder, das Funktionalität und Benutzeroberfläche verbessert.
// @description:fr Un script Tampermonkey pour AtCoder qui améliore les fonctionnalités et l'interface.
// @description:ko AtCoder를 위한 Tampermonkey 스크립트로 기능과 인터페이스를 개선합니다.
// @description:pt Um script Tampermonkey para AtCoder que aprimora a funcionalidade e a interface.
// @description:ja AtCoder用のTampermonkeyスクリプトで機能とインターフェースを強化します。
// @description:es Un script Tampermonkey para AtCoder que mejora la funcionalidad y la interfaz.
// @description:it Uno script Tampermonkey per AtCoder che migliora la funzionalità e l'interfaccia.
// @description:hi AtCoder के लिए एक Tampermonkey स्क्रिप्ट जो कार्यक्षमता और इंटरफ़ेस को बेहतर बनाता है।
// @downloadURL https://update.greasyfork.org/scripts/471106/Atcoder Better!.user.js
// @updateURL https://update.greasyfork.org/scripts/471106/Atcoder Better!.meta.js
// /UserScript
/**
- @namespace OJBetter
- @desc 主命名空间
*/
const OJBetter = {};
/**
- @namespace state
- @desc 描述脚本的当前状态。
- @memberof OJBetter
/
OJBetter.state = {
/* @type {string} 脚本名/
name: GM_info.script.name,
/** @type {string} 格式化后的脚本名/
formatName: undefined,
/** @type {string} 版本号/
version: GM_info.script.version,
/** @type {boolean?} 是否跳过页面加载等待 /
notWaiteLoaded: undefined,
/* @type {string} 最后公告版本,用于标识版本更新完成提示 /
lastAnnounceVer: undefined,
/* @type {string} 最后读取的有效公告版本 /
lastReadAnnounceVer: undefined,
/* @type {number} 当前已打开的模态对话框数量/
openDialogCount: 0
};
/**
- @namespace common
- @desc 通用设置和属性。
- @memberof OJBetter
/
OJBetter.common = {
/* @type {string} 网站的主机地址 /
hostAddress: location.origin,
/* @type {string} 网站当前真实的黑暗模式 /
realDarkMode: undefined,
/* @type {string?} AtCoder的CSRF令牌 /
at_csrf_token: undefined,
/* @type {Array?} 任务队列 /
taskQueue: undefined,
/* @type {object} OJBetter数据库连接实例/
database: undefined,
/** @type {object} turndownService实例/
turndownService: undefined,
};
/**
- @namespace basic
- @desc 基本的用户界面设置。
- @memberof OJBetter
/
OJBetter.basic = {
/* @type {string} 黑暗模式设置 /
darkMode: undefined,
/* @type {boolean?} 是否展开折叠块 /
expandFoldingblocks: undefined,
/* @type {boolean?} 是否开启折叠块渲染性能优化 /
renderPerfOpt: undefined,
/* @type {boolean?} 是否开启下拉选择框性能优化 /
selectElementPerfOpt: undefined,
/* @type {boolean?} 评论区分页 /
commentPaging: undefined,
/* @type {boolean?} 显示跳转到Luogu按钮 /
showJumpToLuogu: undefined,
/* @type {boolean?} 显示跳转到Virtual Judge按钮 /
showCF2vjudge: undefined,
/* @type {boolean?} 比赛排行榜重新着色 */
standingsRecolor: undefined
};
/**
- @namespace typeOfPage
- @desc 页面类型判断。
- @memberof OJBetter
/
OJBetter.typeOfPage = {
/* @type {boolean?} 是否是轻量站 /
is_mSite: false,
/* @type {boolean?} 是否是acmsguru页面 /
is_acmsguru: false,
/* @type {boolean?} 是否是旧版LaTeX页面 /
is_oldLatex: false,
/* @type {boolean?} 是否是题目集页面 /
is_contest: undefined,
/* @type {boolean?} 是否是题目页面 /
is_problem: undefined,
/* @type {boolean?} 是否是完整的问题集页面 /
is_completeProblemset: false,
/* @type {boolean?} 是否是问题集中的问题页面 /
is_problemset_problem: false,
/* @type {boolean?} 是否是问题集页面 /
is_problemset: false,
/* @type {boolean?} 是否是Codeforces排名页面 /
is_cfStandings: false,
/* @type {boolean?} 是否是提交页面 /
is_submitPage: false,
/* @type {boolean?} 是否是代码状态页面 /
is_statePage: false,
/* @type {boolean?} 是否是提交记录页面 /
is_submissions: false,
/* @type {boolean?} 是否是主页 /
is_homepage: undefined,
/* @type {boolean?} 是否选择的是英语页面 /
isEnglishLanguage: undefined,
/* @type {boolean?} 是否是题解页面 */
isEditorial: undefined,
};
/**
- @namespace localization
- @desc 本地化设置。
- @memberof OJBetter
/
OJBetter.localization = {
/* @type {string?} 网站语言 /
websiteLang: undefined,
/* @type {string?} 脚本语言 */
scriptLang: undefined
};
/**
- @namespace translation
- @desc 翻译设置。
- @memberof OJBetter
/
OJBetter.translation = {
/* @type {string?} 翻译服务选择 /
choice: undefined,
/* @type {string?} 目标语言 /
targetLang: undefined,
comment: {
/* @type {string?} 评论翻译服务选择 /
choice: undefined,
/* @type {string?} 评论翻译模式 /
transMode: undefined
},
auto: {
/* @type {boolean?} 自动翻译开关 /
enabled: undefined,
/* @type {number?} 短文本长度限制 /
shortTextLength: undefined,
mixTrans: {
/* @type {boolean?} 混合翻译开关 /
enabled: undefined,
/* @type {Array?} 混合翻译服务列表 /
servers: undefined
}
},
memory: {
/* @type {boolean?} 翻译记忆开关 /
enabled: undefined,
/* @type {Object?} 翻译记忆树 /
ttTree: undefined
},
/* @type {string?} 重翻译时的行为 /
retransAction: undefined,
/* @type {number?} 等待时间 /
waitTime: undefined,
/* @type {boolean?} 替换符 /
replaceSymbol: undefined,
/* @type {boolean?} 过滤文本中的*号 */
filterTextWithoutEmphasis: undefined
};
/**
- @namespace clist
- @desc Clist相关设置。
- @memberof OJBetter
/
OJBetter.clist = {
enabled: {
/* @type {boolean?} 比赛页面开关 /
contest: undefined,
/* @type {boolean?} 问题页面开关 /
problem: undefined,
/* @type {boolean?} 问题集页面开关 /
problemset: undefined
},
/* @type {boolean?} Rating数据防剧透 /
ratingHidden: undefined,
/* @type {string?} Clist key */
authorization: undefined
};
/**
- @namespace monaco
- @desc Monaco编辑器配置。
- @memberof OJBetter
/
OJBetter.monaco = {
/* @type {boolean?} 在问题页面上启用Monaco编辑器 /
enableOnProblemPage: undefined,
/* @type {boolean?} 美化pre代码块 /
beautifyPreBlocks: undefined,
/* @type {boolean} Monaco编辑器加载完成标志 /
loaderOnload: false,
lsp: {
/* @type {Array?} LSP套接字数组 /
socket: [],
/* @type {boolean?} 是否启用LSP /
enabled: undefined,
/* @type {string?} 工作路径 /
workUri: undefined,
/* @type {string?} 套接字URL /
socketUrl: undefined
},
complet: {
/* @type {boolean?} 是否启用C++代码补全模板 /
cppCodeTemplate: undefined,
/* @type {Object?} 自定义配置 /
customConfig: undefined
},
/* @type {Object?} Monaco编辑器实例 /
editor: null,
/* @type {Array?} 代码块美化的Monaco编辑器实例 /
beautifyEditor: [],
/* @type {string?} 在线编译器选择 /
onlineCompilerChoice: undefined,
/* @type {string?} 记忆编译器语言选择 /
compilerSelection: undefined,
/* @type {string?} 当前选择的语言 /
nowLangSelect: undefined,
setting: {
/* @type {Array?} 语言设置数组 /
language: [],
/* @type {string?} 位置 /
position: undefined,
/* @type {boolean} 位置初始化标志 /
position_initialized: false,
/* @type {number?} 字体大小 /
fontsize: undefined,
/* @type {boolean?} 鼠标滚动锁定 /
alwaysConsumeMouseWheel: undefined,
/* @type {boolean?} 提交代码二次确认 /
isCodeSubmitDoubleConfirm: undefined,
/* @type {boolean?} 测试通过后自动提交 /
autoSubmitAfterPass: undefined,
/* @type {string?} 提交按钮位置 /
submitButtonPosition: undefined,
/* @type {boolean?} 自动保存代码 */
autoMemoryCode: undefined
}
};
/**
- @namespace deepl
- @desc DeepL翻译服务配置。
- @memberof OJBetter
/
OJBetter.deepl = {
/* @type {Object?} DeepL配置对象 /
configs: undefined,
config: {
/* @type {string?} 类型 /
type: undefined,
/* @type {string?} 名称 /
name: undefined,
/* @type {string?} API类型 /
apiGenre: undefined,
/* @type {string?} API密钥 /
key: undefined,
/* @type {string?} 代理 /
proxy: undefined,
/* @type {Object?} 额外请求头 /
header: undefined,
/* @type {Object?} 额外请求数据 /
data: undefined,
quota: {
/* @type {string?} 余额URL /
url: undefined,
/* @type {string?} 余额请求方法 /
method: undefined,
/* @type {Object?} 余额请求头 /
header: undefined,
/* @type {Object?} 余额请求数据 /
data: undefined,
/* @type {number?} 剩余配额 /
surplus: undefined
}
},
/* @type {boolean?} 启用重点保护 /
enableEmphasisProtection: undefined,
/* @type {boolean?} 启用链接保护 */
enableLinkProtection: undefined
};
/**
- @namespace chatgpt
- @desc ChatGPT服务配置。
- @memberof OJBetter
/
OJBetter.chatgpt = {
/* @type {Object?} ChatGPT配置对象 /
configs: undefined,
config: {
/* @type {string?} 名称 /
name: undefined,
/* @type {string?} 模型 /
model: undefined,
/* @type {string?} API密钥 /
key: undefined,
/* @type {string?} 代理 /
proxy: undefined,
/* @type {Object?} 额外请求头 /
header: undefined,
/* @type {Object?} 额外请求数据 /
data: undefined,
quota: {
/* @type {string?} 余额URL /
url: undefined,
/* @type {string?} 余额请求方法 /
method: undefined,
/* @type {Object?} 余额请求头 /
header: undefined,
/* @type {Object?} 余额请求数据 /
data: undefined,
/* @type {number?} 剩余配额 /
surplus: undefined
}
},
/* @type {boolean?} 是否为流式传输 /
isStream: undefined,
/* @type {string?} 是否使用自定义Prompt /
customPrompt: undefined,
/* @type {boolean?} 是否作为系统Prompt */
asSystemPrompt: undefined
};
/**
- @namespace preference
- @desc 偏好设置
- @memberof OJBetter
/
OJBetter.preference = {
/* @type {boolean?} 是否显示加载动画 /
showLoading: undefined,
/* @type {boolean?} 是否显示悬停目标区域 /
hoverTargetAreaDisplay: undefined,
/* @type {string?} 按钮图标大小 /
iconButtonSize: undefined,
/* @type {boolean?} 是否显示同比赛题目列表*/
showSameContestProblems: undefined,
};
/**
- @namespace dev
- @desc 维护
- @memberof OJBetter
/
OJBetter.dev = {
/* @type {boolean?} 是否显示规则标记 */
isRuleMarkingEnabled: undefined,
};
/**
- @namespace about
- @desc 关于页信息
- @memberof OJBetter
/
OJBetter.about = {
/* @type {string?} 更新通道 /
updateChannel: undefined,
/* @type {string?} 更新源 */
updateSource: undefined
};
/**
- @namespace supportList
- @desc 支持列表
- @memberof OJBetter
/
OJBetter.supportList = {
/* @type {object} 翻译支持列表和对应语言代码/
translationSupport: {
'deepl': { 'zh': 'ZH', 'de': 'DE', 'fr': 'FR', 'ko': 'KO', 'pt': 'PT', 'ja': 'JA', 'es': 'ES', 'it': 'IT' },
'iflyrec': { 'zh': '1' },
'youdao': { 'zh': 'zh-CHS', 'zh-Hant': 'zh-CHT', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
'google': { 'zh': 'zh-CN', 'zh-Hant': 'zh-TW', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
'caiyun': { 'zh': 'auto2zh', 'ja': 'auto2ja', 'ko': 'auto2ko', 'es': 'auto2es', 'fr': 'auto2fr' },
'openai': { 'zh': 'Chinese', 'zh-Hant': 'Traditional Chinese', 'de': 'German', 'fr': 'French', 'ko': 'Korean', 'pt': 'Portuguese', 'ja': 'Japanese', 'es': 'Spanish', 'it': 'Italian', 'hi': 'Hindi' }
},
/** @type {object} 更新源支持列表/
updateSourceSupportList: {
'greasyfork': {
'release': true,
'dev': false
},
'github': {
'release': true,
'dev': true
},
'aliyunoss': {
'release': true,
'dev': true
}
}
}
// ------------------------------
// 一些工具函数
// ------------------------------
/**
- 延迟函数
- @param {number} ms 延迟时间(毫秒)
- @returns {Promise
}
*/
function OJB_delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
- 等待直到指定的条件函数返回true。
- @param {() => boolean} conditionCheck 一个无参数的函数,用于检查条件是否满足。当函数返回true时,表示条件已满足。
- @param {number} [interval=100] 检查条件的间隔时间,单位为毫秒。默认为100毫秒。
- @returns {Promise
} 返回一个Promise,在条件满足时解决。
*/
async function OJB_waitUntilTrue(conditionCheck, interval = 100) {
return new Promise((resolve) => {
const checkCondition = async () => {
if (conditionCheck()) {
resolve();
} else {
await OJB_delay(interval);
checkCondition();
}
};
checkCondition();
});
}
/**
-
动态加载JavaScript库并返回一个Promise,该Promise在脚本加载完成后解决。
-
@param {string} url - 要加载的JavaScript库的URL地址。
-
@param {string} [expectedHash] - 可选的Base64编码的SHA-512哈希值,用于校验脚本内容。格式为 "sha512-<Base64编码的哈希值>"。
-
@returns {Promise
} 一个Promise,它在脚本加载并执行完成后解决。
/
async function OJB_LoadJS(url, expectedHash) {
/*- 计算给定数据的SHA-512哈希值,并将其转换为十六进制字符串。
- @param {string} data - 要计算哈希值的数据。
- @returns {Promise
} 一个Promise,它解析为数据的SHA-512哈希值的十六进制字符串。
*/
const calculateHash = async (data) => {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-512', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
};
/**
- 将Base64编码的字符串转换为十六进制字符串。
- @param {string} base64 - Base64编码的字符串。
- @returns {string} 转换后的十六进制字符串。
*/
const base64ToHex = (base64) => {
const binaryString = atob(base64);
const byteArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i);
}
return Array.from(byteArray).map(b => b.toString(16).padStart(2, '0')).join('');
};
try {
const response = await fetch(url);
if (!response.ok) throw new Error(Failed to fetch script: ${response.statusText}
);
const scriptContent = await response.text();if (expectedHash) { // 去掉前缀 "sha512-" const base64Hash = expectedHash.replace(/^sha512-/, ''); const actualHash = await calculateHash(scriptContent); const expectedHashHex = base64ToHex(base64Hash); if (actualHash !== expectedHashHex) throw new Error('SHA-512 hash mismatch'); } const scriptElement = document.createElement("script"); scriptElement.textContent = scriptContent; document.head.prepend(scriptElement); return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
}
/**
- 安全地创建JQuery对象
- @description 通过字符串创建JQuery对象时,如果字符串以空格开头,在某些Jquery版本中会发生错误,过滤空格以安全的创建元素。
- @param {string} string - 字符串。
- @returns {JQuery} JQuery对象
*/
const OJB_safeCreateJQElement = function (string) {
return $(string.replace(/^\s+/, ""));
}
/**
- 将数字或者字符串解析为数字。
- @memberof OJBetter.common
- @param {string} val 要解析的字符串
- @param {boolean} [strict=false] 是否进行严格类型检查
- @returns {number} 解析结果
- @throws {Error} 如果解析失败,则抛出错误
*/
const OJB_parseNumber = (val, strict = false) => {
const num = Number(val);
if (isNaN(num) || (strict && val.toString() !== num.toString())) {
throw new Error('Invalid number');
}
return num;
};
/**
- 将字符串解析为布尔值
- @param {string} val - 要解析的字符串
- @param {boolean} strict - 是否进行严格类型检查
- @returns {boolean} - 解析结果
- @throws {Error} - 如果解析失败,则抛出错误
*/
const OJB_parseBoolean = (val, strict) => {
if (strict) {
if (val === true || val === false) return val;
throw new Error('Invalid boolean');
}
return val === 'true' ? true : val === 'false' ? false : val;
};
/**
- 将字符串解析为对象
- @param {string} val - 要解析的字符串
- @returns {Object} - 解析结果
- @throws {Error} - 如果解析失败,则抛出错误
*/
const OJB_parseObject = val => {
try {
return JSON.parse(val);
} catch {
throw new Error('Invalid JSON');
}
};
/**
- 将字符串解析为键值对数组
- @param {string} val - 要解析的字符串
- @returns {Object[]} - 解析结果
- @throws {Error} - 如果解析失败,则抛出错误
*/
const OJB_parseLinePairArray = val => {
if (typeof val !== 'string' || val.trim() === '') return [];
return val.split("\n").filter(line => line.trim() !== '').map(line => {
const indexOfFirstColon = line.indexOf("😊;
if (indexOfFirstColon === -1) throw new Error('Invalid LinePairArray format: ":" is missing');
const key = line.substring(0, indexOfFirstColon).trim();
const value = line.substring(indexOfFirstColon + 1).trim();
return { [key]: value };
});
};
/**
- 移除文本中的HTML标签
- @param {string} text - 包含HTML标签的文本
- @returns {string} - 移除HTML标签后的文本
/
const OJB_removeHTMLTags = function (text) {
return text.replace(/</?[a-zA-Z]+("["]*"|'[']'|[^'">])*>/g, '');
}
/**
- 获取对象中指定路径表达式的值
- @param {Object} obj - 要计算的对象
- @param {string} pathOrExpression - 要计算的路径表达式
- @returns {any} - 计算结果
- @example
- const obj = {
- "a": {
-
"b": 1
- },
- "c": 2
- };
- OJB_evaluatePathOrExpression(obj, "a.b"); // 1
- OJB_evaluatePathOrExpression(obj, "a.b + c"); // 3
- OJB_evaluatePathOrExpression(obj, "a.b + a.c"); // 1
*/
function OJB_evaluatePathOrExpression(obj, pathOrExpression) {
const hasOperator = /[+-*/]/.test(pathOrExpression);
const getPathValue = (obj, path) => {
return path.split('.').reduce((acc, part) => {
return acc !== undefined && acc !== null && acc.hasOwnProperty(part) ? acc[part] : undefined;
}, obj);
};
const evaluateExpression = (obj, expression) => {
const tokens = expression.split(/([+-*/])/).map(token => token.trim());
const values = tokens.map(token => {
if (/[+-*/]/.test(token)) {
return token;
} else {
const value = getPathValue(obj, token);
return value !== undefined ? value : 0;
}
});
const evaluatedExpression = values.join(' ');
try {
return Function('use strict'; return (${evaluatedExpression});
)();
} catch (e) {
console.error('Expression evaluation error:', e);
return undefined;
}
};
return hasOperator ? evaluateExpression(obj, pathOrExpression) : getPathValue(obj, pathOrExpression);
}
/**
-
获取 GM 存储的值并根据类型进行处理
-
@param {string} key - 要检索的值的键。
-
@param {any} defaultValue - 如果值未找到,则返回的默认值。
-
@param {Object} [options={}] - 配置选项对象。
-
@param {string} [options.type='string'] - 期望的值的类型。可选值:'string', 'number', 'boolean', 'object', 'array', 'linePairArray'。
-
@param {boolean} [options.strict=false] - 用于数字和布尔类型,表示是否进行严格类型检查。
-
@param {string} [options.pathOrExpression=''] - 用于对象或数组类型,表示路径表达式或获取元素的索引。
-
@returns {any} - 检索到的值。
*/
const OJB_getGMValue = (key, defaultValue, { type = 'string', strict = false, pathOrExpression = '' } = {}) => {
let value = GM_getValue(key);
if (value === undefined || value === null || value === "") {
GM_setValue?.(key, defaultValue);
return defaultValue;
}const parsers = {
string: val => val,
number: (val) => OJB_parseNumber(val, strict),
boolean: (val) => OJB_parseBoolean(val, strict),
object: OJB_parseObject,
array: OJB_parseObject,
linePairArray: OJB_parseLinePairArray
};if (!(type in parsers)) {
console.error(Unsupported type: ${type}
);
return defaultValue;
}try {
value = parserstype;
} catch (e) {
console.error('Error:', e.message);
return defaultValue;
}// The pathOrExpression processing is not applicable to linePairArray type
if ((type === 'object' || type === 'array') && pathOrExpression) {
const evaluated = OJB_evaluatePathOrExpression(value, pathOrExpression);
if (evaluated === undefined) {
console.error('Path or expression evaluation returned undefined');
return defaultValue;
}
value = evaluated;
}return value;
};
/**
- 版本号比较方法
- @param {string} version1 版本号1
- @param {string} version2 版本号2
- @returns {number} -1: version1 < version2, 0: version1 = version2, 1: version1 > version2
*/
const OJB_compareVersions = function (version1 = "0", version2 = "0") {
const v1Array = version1.split(".").map(Number);
const v2Array = version2.split(".").map(Number);
const length = Math.max(v1Array.length, v2Array.length);
for (let i = 0; i < length; i++) {
const diff = (v1Array[i] || 0) - (v2Array[i] || 0);
if (diff) return Math.sign(diff);
}
return 0;
}
/**
- 获取上一个主版本号
- @param {string} currentVersion 当前版本号
- @returns {string} 上一个主版本号
*/
const OJB_getPreviousVersion = function (currentVersion) {
const versionArray = currentVersion.split(".").map(Number);
let lastNonZeroIndex = versionArray.length - 1;
while (lastNonZeroIndex >= 0 && versionArray[lastNonZeroIndex] === 0) {
lastNonZeroIndex--;
}
if (lastNonZeroIndex >= 0) {
versionArray[lastNonZeroIndex]--;
for (let i = lastNonZeroIndex + 1; i < versionArray.length; i++) {
versionArray[i] = 0;
}
}
return versionArray.join(".");
};
/**
-
在指定根节点下观察指定选择器的元素,当元素存在时,执行回调函数
-
@param {Object} options - 配置对象
-
@param {string} options.selector - CSS选择器文本
-
@param {Function} options.callback - 回调函数,接收变动的节点作为参数
-
@param {Boolean} [options.triggerOnExist=true] - 如果为true,元素已存在时立即触发一次回调
-
@param {Element} [options.root=document.body] - 在哪个根节点下监听变化
-
@param {Boolean} [options.subtree=false] - 是否监听子树变化(即非直接子元素)
*/
function OJB_observeElement({
selector,
callback,
triggerOnExist = true,
root = document.body,
subtree = false
}) {
// 尝试获取选择器指定的元素
const targetNode = root.querySelector(selector);if (targetNode) {
// 如果元素已存在,直接开始观察
observeAndReport(targetNode, callback);
// 如果triggerOnExist为true,则立即触发一次回调
if (triggerOnExist) {
callback(targetNode);
}
} else {
// 如果元素不存在,监听DOM变化直到该元素被添加
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE && node.matches(selector)) {
observeAndReport(node, callback);
if (triggerOnExist) {
callback(node);
}
observer.disconnect(); // 停止监听
}
});
});
});observer.observe(root, { childList: true, subtree, attributes: false });
}
function observeAndReport(node, callback) {
const childObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((addedNode) => {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
callback(addedNode); // 执行回调函数
}
});
});
});childObserver.observe(node, { childList: true, subtree: true, attributes: false });
}
}
/**
- 初始化全局变量
*/
async function initVar() {
const { hostname, href } = window.location;
OJBetter.state.formatName = (() => OJBetter.state.name
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, ''))();
OJBetter.state.lastAnnounceVer = OJB_getGMValue("lastAnnounceVer", "0");
OJBetter.state.lastReadAnnounceVer = OJB_getGMValue("lastReadAnnounceVer", "0");
OJBetter.typeOfPage.is_contest = //contests/[^/]+/tasks/?$/.test(href);
OJBetter.typeOfPage.is_problem = href.includes('/tasks/');
OJBetter.typeOfPage.is_homepage = (href === 'https://atcoder.jp/' || href === 'https://atcoder.jp/?lang=ja');
OJBetter.typeOfPage.isEnglishLanguage = $('meta[http-equiv="Content-Language"]').attr('content') === 'en';
OJBetter.typeOfPage.isEditorial = href.includes("editorial");
OJBetter.localization.websiteLang = OJB_getGMValue("localizationLanguage", "zh");
OJBetter.localization.scriptLang = OJB_getGMValue("scriptL10nLanguage", "zh");
OJBetter.basic.renderPerfOpt = OJB_getGMValue("renderPerfOpt", false);
OJBetter.basic.selectElementPerfOpt = OJB_getGMValue("selectElementPerfOpt", true);
OJBetter.basic.commentPaging = OJB_getGMValue("commentPaging", true);
OJBetter.basic.showJumpToLuogu = OJB_getGMValue("showJumpToLuogu", true);
OJBetter.basic.showCF2vjudge = OJB_getGMValue("showCF2vjudge", true);
OJBetter.basic.standingsRecolor = OJB_getGMValue("standingsRecolor", true);
OJBetter.state.notWaiteLoaded = OJB_getGMValue("notWaiteLoaded", false);
OJBetter.translation.targetLang = OJB_getGMValue("transTargetLang", "zh");
OJBetter.translation.choice = OJB_getGMValue("translation", "deepl");
OJBetter.translation.comment.transMode = OJB_getGMValue("commentTranslationMode", "0");
OJBetter.translation.comment.choice = OJB_getGMValue("commentTranslationChoice", "0");
OJBetter.translation.memory.enabled = OJB_getGMValue("memoryTranslateHistory", true);
OJBetter.translation.auto.enabled = OJB_getGMValue("autoTranslation", false);
OJBetter.translation.auto.shortTextLength = OJB_getGMValue("shortTextLength", "2000");
OJBetter.translation.retransAction = OJB_getGMValue("retransAction", "0");
OJBetter.translation.waitTime = OJB_getGMValue("transWaitTime", "200");
OJBetter.translation.auto.mixTrans.enabled = OJB_getGMValue("allowMixTrans", true);
OJBetter.translation.auto.mixTrans.servers = OJB_getGMValue("mixedTranslation", ['deepl', 'iflyrec', 'youdao', 'caiyun']);
OJBetter.common.taskQueue = new TaskQueue();
OJBetter.translation.replaceSymbol = OJB_getGMValue("replaceSymbol", "2");
OJBetter.translation.filterTextWithoutEmphasis = OJB_getGMValue("filterTextWithoutEmphasis", false);
OJBetter.clist.enabled.contest = OJB_getGMValue("showClistRating_contest", false);
OJBetter.clist.enabled.problem = OJB_getGMValue("showClistRating_problem", false);
OJBetter.clist.enabled.problemset = OJB_getGMValue("showClistRating_problemset", false);
OJBetter.clist.ratingHidden = OJB_getGMValue("RatingHidden", false);
OJBetter.clist.authorization = OJB_getGMValue("clist_Authorization", "");
//deepl
OJBetter.deepl.config.type = OJB_getGMValue("deepl_type", "free");
OJBetter.deepl.configs = OJB_getGMValue("deepl_config", {
"choice": "",
"configurations": []
});
if (OJBetter.deepl.configs.choice !== "" && OJBetter.deepl.configs.configurations.length !== 0) {
const choice = OJBetter.deepl.configs.choice;
const configuration = OJBetter.deepl.configs.configurations.find(obj => obj.name === choice);;
if (configuration == undefined) {
let existingConfig = GM_getValue('deepl_config');
existingConfig.choice = "";
GM_setValue('deepl_config', existingConfig);
location.reload();
}
OJBetter.deepl.config.name = configuration.name;
OJBetter.deepl.config.apiGenre = configuration.apiGenre;
OJBetter.deepl.config.key = configuration.key;
OJBetter.deepl.config.proxy = configuration.proxy;
OJBetter.deepl.config.header = OJB_parseLinePairArray(configuration._header);
OJBetter.deepl.config.data = OJB_parseLinePairArray(configuration._data);
OJBetter.deepl.config.quota.url = configuration.quota_url;
OJBetter.deepl.config.quota.method = configuration.quota_method;
OJBetter.deepl.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
OJBetter.deepl.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
OJBetter.deepl.config.quota.surplus = configuration.quota_surplus;
}
OJBetter.deepl.enableEmphasisProtection = OJB_getGMValue("enableEmphasisProtection", true);
OJBetter.deepl.enableLinkProtection = OJB_getGMValue("enableLinkProtection", true);
//openai
OJBetter.chatgpt.isStream = OJB_getGMValue("openai_isStream", true);
OJBetter.chatgpt.customPrompt = OJB_getGMValue("openai_customPrompt", '');
OJBetter.chatgpt.asSystemPrompt = OJB_getGMValue("openai_asSystemPrompt", false);
OJBetter.chatgpt.configs = OJB_getGMValue("chatgpt_config", {
"choice": "",
"configurations": []
});
if (OJBetter.chatgpt.configs.choice !== "" && OJBetter.chatgpt.configs.configurations.length !== 0) {
const choice = OJBetter.chatgpt.configs.choice;
const configuration = OJBetter.chatgpt.configs.configurations.find(obj => obj.name === choice);;
if (configuration == undefined) {
let existingConfig = GM_getValue('chatgpt_config');
existingConfig.choice = "";
GM_setValue('chatgpt_config', existingConfig);
location.reload();
}
OJBetter.chatgpt.config.name = configuration.name;
OJBetter.chatgpt.config.model = configuration.model;
OJBetter.chatgpt.config.key = configuration.key;
OJBetter.chatgpt.config.proxy = configuration.proxy;
OJBetter.chatgpt.config.header = OJB_parseLinePairArray(configuration._header);
OJBetter.chatgpt.config.data = OJB_parseLinePairArray(configuration._data);
OJBetter.chatgpt.config.quota.url = configuration.quota_url;
OJBetter.chatgpt.config.quota.method = configuration.quota_method;
OJBetter.chatgpt.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
OJBetter.chatgpt.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
OJBetter.chatgpt.config.quota.surplus = configuration.quota_surplus;
}
// 编辑器
// if (!OJBetter.typeOfPage.is_mSite) OJBetter.common.cf_csrf_token = Codeforces.getCsrfToken();
// else OJBetter.common.cf_csrf_token = "";
OJBetter.common.at_csrf_token = csrfToken;
// OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "61");
OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "5001");
OJBetter.monaco.setting.fontsize = OJB_getGMValue("editorFontSize", "15");
OJBetter.monaco.enableOnProblemPage = OJB_getGMValue("problemPageCodeEditor", true);
OJBetter.monaco.beautifyPreBlocks = OJB_getGMValue("beautifyPreBlocks", true);
OJBetter.monaco.complet.cppCodeTemplate = OJB_getGMValue("cppCodeTemplateComplete", true);
OJBetter.monaco.onlineCompilerChoice = OJB_getGMValue("onlineCompilerChoice", "official");
OJBetter.monaco.setting.isCodeSubmitDoubleConfirm = OJB_getGMValue("isCodeSubmitConfirm", true);
OJBetter.monaco.setting.autoSubmitAfterPass = OJB_getGMValue("autoSubmitAfterPass", false);
OJBetter.monaco.setting.alwaysConsumeMouseWheel = OJB_getGMValue("alwaysConsumeMouseWheel", true);
OJBetter.monaco.setting.submitButtonPosition = OJB_getGMValue("submitButtonPosition", "bottom");
OJBetter.monaco.setting.autoMemoryCode = OJB_getGMValue("autoMemoryCode", true);
//自定义补全
OJBetter.monaco.complet.customConfig = OJB_getGMValue("Complet_config", {
"choice": -1,
"configurations": []
});
// monaco
OJBetter.monaco.lsp.enabled = OJB_getGMValue("useLSP", false);
OJBetter.monaco.setting.position = OJB_getGMValue("monacoEditor_position", "initial");
OJBetter.monaco.lsp.workUri = OJB_getGMValue("OJBetter_Bridge_WorkUri", "C:/OJBetter_Bridge");
OJBetter.monaco.lsp.socketUrl = OJB_getGMValue("OJBetter_Bridge_SocketUrl", "ws://127.0.0.1:2323/");
OJBetter.preference.showLoading = OJB_getGMValue("showLoading", true);
OJBetter.preference.hoverTargetAreaDisplay = OJB_getGMValue("hoverTargetAreaDisplay", false);
OJBetter.preference.showSameContestProblems = OJB_getGMValue("showSameContestProblems", false);
OJBetter.basic.expandFoldingblocks = OJB_getGMValue("expandFoldingblocks", true);
OJBetter.preference.iconButtonSize = OJB_getGMValue("iconButtonSize", "16");
OJBetter.dev.isRuleMarkingEnabled = OJB_getGMValue("isRuleMarkingEnabled", false);
OJBetter.about.updateChannel = OJB_getGMValue("updateChannel", "release");
OJBetter.about.updateSource = OJB_getGMValue("updateSource", "aliyunoss");
}
/**
- 显示警告消息
*/
function showWarnMessage() {
if (OJBetter.typeOfPage.is_oldLatex) {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(${OJBetter.state.name} —— ${i18next.t('warning.is_oldLatex', { ns: 'alert' })}
, 'warning');
}
if (OJBetter.typeOfPage.is_acmsguru) {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(${OJBetter.state.name} —— ${i18next.t('warning.is_acmsguru', { ns: 'alert' })}
, 'warning');
}
if (OJBetter.translation.comment.transMode == "1") {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(${OJBetter.state.name} —— ${i18next.t('warning.trans_segment', { ns: 'alert' })}
, 'warning');
}
if (OJBetter.translation.comment.transMode == "2") {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(${OJBetter.state.name} —— ${i18next.t('warning.trans_select', { ns: 'alert' })}
, 'warning');
}
if (OJBetter.typeOfPage.is_submitPage && OJBetter.monaco.enableOnProblemPage) {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(${OJBetter.state.name} —— ${i18next.t('warning.is_submitPage', { ns: 'alert' })}
, 'warning');
}
}
// 常量
const helpCircleHTML = '
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>
;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 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>
;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" /> <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" /> <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>
;
/**
-
连接数据库
*/
async function initDB() {
OJBetter.common.database = new Dexie('OJBetterDB');
OJBetter.common.database.version(3).stores({
samplesData: '&url',
editorCode: '&url',
translateData: '&url',
localizeSubsData: '&lang'
});// 等待数据库打开
await OJBetter.common.database.open();
}
/**
- 清空数据库
*/
async function clearDatabase() {
const isConfirmed = await OJB_createDialog(
i18next.t('isClearDatabase.title', { ns: 'dialog' }),
i18next.t('isClearDatabase.content', { ns: 'dialog' }),
[
i18next.t('isClearDatabase.buttons.0', { ns: 'dialog' }),
i18next.t('isClearDatabase.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
try {
// 开启一个读写事务,包含数据库中的所有表
await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
// 遍历所有表
for (const table of OJBetter.common.database.tables) {
// 清空当前表
await table.clear();
}
});
console.log("All tables in the database have been cleared.");
alert("All tables in the database have been cleared.");
} catch (error) {
console.error("Error clearing the database:", error);
}
}
}
/**
-
导出数据库
-
@returns {Promise
} 数据库的JSON字符串
*/
async function exportDatabase() {
try {
// 创建一个存储数据的对象
const exportData = {};
// 获取数据库中所有表的名称
const tableNames = OJBetter.common.database.tables.map(table => table.name);// 遍历每一个表,获取数据 for (const tableName of tableNames) { const tableData = await OJBetter.common.database.table(tableName).toArray(); exportData[tableName] = tableData; } // 将数据对象转换为JSON字符串 const jsonData = JSON.stringify(exportData, null, 4); return jsonData;
} catch (error) {
console.error("Error exporting database:", error);
}
}
/**
-
导入数据库
-
@param {string} jsonData 数据库的JSON字符串
*/
async function importDatabase(jsonData) {
const isConfirmed = await OJB_createDialog(
i18next.t('isImportDatabase.title', { ns: 'dialog' }),
i18next.t('isImportDatabase.content', { ns: 'dialog' }),
[
i18next.t('isImportDatabase.buttons.0', { ns: 'dialog' }),
i18next.t('isImportDatabase.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
try {
// 将JSON字符串解析为对象
const importData = JSON.parse(jsonData);// 开启一个事务,并清空现有数据 await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => { // 清空所有表的数据 for (const tableName of OJBetter.common.database.tables.map(table => table.name)) { await OJBetter.common.database.table(tableName).clear(); } // 插入新数据 for (const [tableName, rows] of Object.entries(importData)) { await OJBetter.common.database.table(tableName).bulkAdd(rows); } }); alert("Data imported successfully"); } catch (error) { console.error("Error importing database:", error); }
}
}
/**
-
将数据下载为文件
-
@param {string} data 数据
-
@param {string} filename 文件名,默认为'export.json'
-
@param {string} fileType 文件MIME类型,默认为'application/json'
-
@returns {void}
*/
function downloadDataAsFile(data, filename = 'export.json', fileType = 'application/json') {
// 创建一个blob对象,指定文件类型
const blob = new Blob([data], { type: fileType });
const url = URL.createObjectURL(blob);// 创建一个隐藏的a标签,模拟点击进行下载
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();// 清理
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
-
从文件中读取数据
-
@param {Function} callback 回调函数
-
@returns {void}
*/
function readFileInput(callback) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.style.display = 'none'; // 隐藏input元素fileInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const fileContent = e.target.result;
if (callback && typeof callback === 'function') {
callback(fileContent); // 调用回调函数,传入文件内容
}
};
reader.readAsText(file);
}
};document.body.appendChild(fileInput);
fileInput.click();
document.body.removeChild(fileInput);
}
/**
-
清除所有设置
*/
async function deleteAllConfigSettings() {
const isConfirmed = await OJB_createDialog(
i18next.t('isDeleteAllConfigSettings.title', { ns: 'dialog' }),
i18next.t('isDeleteAllConfigSettings.content', { ns: 'dialog' }),
[
i18next.t('isDeleteAllConfigSettings.buttons.0', { ns: 'dialog' }),
i18next.t('isDeleteAllConfigSettings.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
const keys = GM_listValues();keys.forEach(key => { GM_deleteValue(key); }); alert('All settings have been deleted.'); window.location.reload();
}
}
/**
-
导出设置到JSON
-
@returns {string} JSON字符串
*/
function exportSettingsToJSON() {
const keys = GM_listValues();
let settings = {};keys.forEach(key => {
settings[key] = GM_getValue(key);
});return JSON.stringify(settings, null, 4);
}
/**
-
从JSON导入设置
-
@param {string} jsonData JSON字符串
-
@returns {void}
*/
async function importSettingsFromJSON(jsonData) {
const isConfirmed = await OJB_createDialog(
i18next.t('isImportSettings.title', { ns: 'dialog' }),
i18next.t('isImportSettings.content', { ns: 'dialog' }),
[
i18next.t('isImportSettings.buttons.0', { ns: 'dialog' }),
i18next.t('isImportSettings.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
let settings;
try {
settings = JSON.parse(jsonData);
} catch (e) {
console.error('JSON parsing error:', e);
return;
}Object.keys(settings).forEach(key => { GM_setValue(key, settings[key]); }); alert('Settings imported successfully!'); window.location.reload();
}
}
/**
- 加载元素本地化语言数据
- @param {JQuery} element jQuery元素
- @param {number} [retries=10] 重试次数
- @param {number} [interval=50] 重试间隔
*/
function elementLocalize(element, retries = 10, interval = 50) {
if ($.isFunction(element.localize)) {
element.localize();
} else if (retries > 0) {
setTimeout(elementLocalize, interval, element, retries - 1, interval);
} else {
console.error('Unable to localize', element);
}
}
// 切换系统黑暗监听
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const changeEventListeners = [];
/**
-
处理颜色模式变化事件
-
@param {MediaQueryListEvent} event - 媒体查询事件对象
*/
const handleColorSchemeChange = (event) => {
const theme = event.matches ? 'dark' : 'light';// 更新页面主题
$('html').attr('data-theme', theme);
OJBetter.common.realDarkMode = theme;const updateMonacoTheme = (theme) => {
const intervalId = setInterval(() => {
if (OJBetter?.monaco?.editor) {
monaco.editor.setTheme(theme);
clearInterval(intervalId);
}
}, 100);OJBetter.monaco.beautifyEditor.forEach((editor) => { editor.updateOptions({ theme }); });
};
if (event.matches) {
updateMonacoTheme('vs-dark');
} else {
const originalColor = $(this).data("original-color");
$(this).css("background-color", originalColor);
updateMonacoTheme('vs');
}
};
// 黑暗模式
(function setDark() {
// 初始化
function setDarkTheme() {
const htmlElement = document.querySelector('html');
if (htmlElement) {
htmlElement.setAttribute('data-theme', 'dark');
const intervalId = setInterval(() => {
if (OJBetter.monaco && OJBetter.monaco.editor) {
monaco.editor.setTheme('vs-dark');
clearInterval(intervalId);
}
}, 100);
} else {
setTimeout(setDarkTheme, 100);
}
}
// 设置黑暗模式和监听器
OJBetter.basic.darkMode = OJB_getGMValue("darkMode", "follow");
if (OJBetter.basic.darkMode == "dark") {
OJBetter.common.realDarkMode = 'dark';
setDarkTheme();
} else if (OJBetter.basic.darkMode == "light") {
OJBetter.common.realDarkMode = 'light';
} else if (OJBetter.basic.darkMode == "follow") {
OJBetter.common.realDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// 添加事件监听器
changeEventListeners.push(handleColorSchemeChange);
mediaQueryList.addEventListener('change', handleColorSchemeChange);
if (window.matchMedia('(prefers-color-scheme: dark)').matches) setDarkTheme();
}
// 定义全局变量
GM_addStyle(`
/* 黑暗支持 */
html[data-theme=dark]:root {
color-scheme: light dark;
}
/* 颜色 */
:root {
/* 文字颜色 */
--ojb-color-text-primary: #a0adb9; /* 主要文字颜色 */
--ojb-color-text-secondary: #9AA4B1; /* 次要文字颜色 */
--ojb-color-text-tertiary: #9BA5B2; /* 第三级文字颜色 */
--ojb-color-text-success: #43A047; /* 成功状态文字颜色 */
--ojb-color-text-highlight: #cbd6e2; /* 高亮文字颜色 */
--ojb-color-text-disabled: #506778; /* 禁用状态文字颜色 */
--ojb-color-text-icon-success: #2e7d32; /* 成功状态图标颜色 */
--ojb-color-text-link: #4b8eda; /* 链接颜色 */
/* 背景颜色 */
--ojb-color-bg-primary: #22272e; /* 主背景颜色 */
--ojb-color-bg-secondary: #2d333b; /* 次级背景颜色 */
--ojb-color-bg-disabled: #24292e; /* 禁用元素背景颜色 */
/* 边框颜色 */
--ojb-color-border-primary: #48535F; /* 主要边框颜色 */
--ojb-color-border-disabled: #404950; /* 禁用状态边框颜色 */
--ojb-color-border-dashed-hover: #03A9F4; /* 虚线边框悬浮颜色 */
--ojb-color-border-radio-checked: #326154; /* 选中的单选框边框颜色 */
/* 阴影颜色 */
--ojb-shadow-standard: 0px 0px 0.5px 0.5px #3A4048; /* 标准阴影 */
--ojb-shadow-menu-modal: 0px 0px 0px 4px #2d333b; /* 菜单和模态框阴影 */
/* 区域遮罩颜色 */
--ojb-overlay-background: repeating-linear-gradient(135deg, #49525f6e, #49525f6e 30px, #49525f29 0px, #49525f29 55px); /* 区域遮罩背景 */
/* 文字阴影 */
--ojb-text-shadow-icon: 1px 1px 0px #2d333b, 1px -1px 0px #2d333b, -1px -1px 0px #2d333b, -1px 1px 0px #2d333b; /* 图标文字阴影 */
}
/* 边框样式 */
:root {
/* 边框样式 */
--ojb-border-width: 1px; /* 边框宽度 */
--ojb-border-style-solid: solid; /* 实线样式 */
--ojb-border-style-dashed: dashed; /* 虚线样式 */
--ojb-border-radius-small: 4px; /* 小圆角 */
--ojb-border-radius-medium: 8px; /* 中圆角 */
--ojb-border-radius-large: 12px; /* 大圆角 */
/* 组合边框样式 */
--ojb-border-solid-primary: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-primary); /* 主要实线边框 */
--ojb-border-dashed: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-primary); /* 主要虚线边框 */
--ojb-border-dashed-hover: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-dashed-hover); /* 悬浮虚线边框 */
--ojb-border-solid-disabled: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-disabled); /* 禁用状态实线边框 */
}
`);
// OJBetter界面样式
GM_addStyle(`
/* 主要文字颜色 */
html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
html[data-theme=dark] .alert-warning, html[data-theme=dark] .markItUpEditor,
html[data-theme=dark] .translate-problem-statement, html[data-theme=dark] .OJBetter_setting_menu,
html[data-theme=dark] .help_tip .tip_text,
html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
html[data-theme=dark] #items-per-page, html[data-theme=dark] #pagBar,
html[data-theme=dark] .OJBetter_setting_sidebar li a:link,
html[data-theme=dark] .popup .content{
color: var(--ojb-color-text-primary);
}
/* 次要文字颜色 */
html[data-theme=dark] .ojb_btn:hover, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_statusBar,
html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #addCustomTest,
html[data-theme=dark] #customTestBlock, html[data-theme=dark] .OJBetter_setting_list.alert_info{
color: var(--ojb-color-text-secondary);
}
/* 文字颜色3 */
html[data-theme=dark] .ojb_btn{
color: var(--ojb-color-text-tertiary);
}
/* 文字颜色 浅绿 */
html[data-theme=dark] #SubmitButton{
color: var(--ojb-color-text-success);
}
/* 禁止文字颜色 */
html[data-theme=dark] .ojb_btn[disabled]{
color: var(--ojb-color-text-disabled);
}
/* 主要背景层次 */
html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] li#add_button:hover,
html[data-theme=dark] .ojb_btn:hover,
html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
html[data-theme=dark] #OJBetter_SubmitForm input,
html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
html[data-theme=dark] .OJBetter_setting_sidebar li a.active, html[data-theme=dark] .OJBetter_setting_sidebar li,
html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-track, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-track,
html[data-theme=dark] .OJBetter_modal, html[data-theme=dark] .OJBetter_modal button:hover,
html[data-theme=dark] .popup .content,
html[data-theme=dark] .config_bar_list, html[data-theme=dark] #LSPLog,
html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before,
html[data-theme=dark] .OJBetter_setting_menu a, html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button:hover,
html[data-theme=dark] .OJBetter_setting_menu select{
background-color: var(--ojb-color-bg-primary);
点击查看代码
// ==UserScript==
// @name Atcoder Better!
// @namespace https://greasyfork.org/users/747162
// @version 1.19.0
// @description 一个适用于 AtCoder 的 Tampermonkey 脚本,增强功能与界面。
// @author 北极小狐
// @match *://atcoder.jp/*
// @run-at document-start
// @connect www2.deepl.com
// @connect api-free.deepl.com
// @connect api.deepl.com
// @connect api.deeplx.org
// @connect www.iflyrec.com
// @connect dict.youdao.com
// @connect api.interpreter.caiyunai.com
// @connect translate.google.com
// @connect openai.api2d.net
// @connect api.openai.com
// @connect www.luogu.com.cn
// @connect vjudge.net
// @connect clist.by
// @connect greasyfork.org
// @connect sustech.edu.cn
// @connect aowuucdn.oss-cn-beijing.aliyuncs.com
// @connect aowuucdn.oss-accelerate.aliyuncs.com
// @connect 127.0.0.1
// @connect *
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_getResourceText
// @icon https://aowuucdn.oss-accelerate.aliyuncs.com/atcoder.png
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/turndown/7.2.0/turndown.min.js#sha512-sJzEecN5Nk8cq81zKtGq6/z9Z/r3q38zV9enY75IVxiG7ybtlNUt864sL4L1Kf36bYIwxTMVKQOtU4VhD7hGrw==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/markdown-it/13.0.2/markdown-it.js#sha512-2LtYcLGnCbAWz9nDIrfG2pHFiFu9n+3oGecQlzLuYsLgen/oxiYscGWnDST9J9EZanlsQkDD0ZP2n/6peDuALQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha512-a+SUDuwNzXDvz4XrIcXHuCf089/iJAoN4lmrXJg18XnduKK6YlDHNRalv4yd1N40OKI80tFidF+rqTFKGPoWFQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/chroma-js/2.4.2/chroma.min.js#sha512-zInFF17qBFVvvvFpIfeBzo7Tj7+rQxLeTJDmbxjBz5/zIr89YVbTNelNhdTT+/DCrxoVzBeUPVFJsczKbB7sew==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/xterm/5.5.0/xterm.js#sha512-Gujw5GajF5is3nMoGv9X+tCMqePLL/60qvAv1LofUZTV9jK8ENbM9L+maGmOsNzuZaiuyc/fpph1KT9uR5w3CQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dexie/4.0.7/dexie.min.js#sha512-882VotT07mOQRzqIxsyxHzJX0XUaoeee3qXp4THg1A0KI0XFnWFAaLFQm0x6OW3pHSIipVZW+gzQ1w9b6uvkVw==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/i18next/23.11.5/i18next.min.js#sha512-3RSGkmT48HnO+hlmzGYDx5/w2LIBX0O5hSuYX6KWAxmvVlSjFgoxIaWa2tlMExheGvt3lLyxeTsXfpC47yb8CQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/i18next-http-backend/2.5.2/i18nextHttpBackend.min.js#sha512-bBb+wrGRTx4MvHpksYb1Iv5oJ1o8ineCqpc0cnTgdJQhuAFJJ93SEVXxUOCptvt0vAqYdjzWO5emorYUBt6Ceg==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/jquery-i18next/1.2.1/jquery-i18next.min.js#sha512-79RgNpOyaf8AvNEUdanuk1x6g53UPoB6Fh2uogMkOMGADBG6B0DCzxc+dDktXkVPg2rlxGvPeAFKoZxTycVooQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dialog-polyfill/0.5.6/dialog-polyfill.min.js#sha512-qUIG93zKzcLBVD5RGRbx2PBmbVRu+tJIl+EPLTus0z8I1AMru9sQYdlf6cBacSzYmZVncB9rcc8rYBnazqgrxA==
// @resource acwing_cpp_code_completer https://aowuucdn.oss-accelerate.aliyuncs.com/acwing_cpp_code_completer-0.0.11.json#sha512-DQVpao4qMMExToRdid0g/S0nbO/C9hwCECjI5aW8A0g7nvi8hEcD2Lw3QIqdJBV7haP15oJOocfwuiw7ryTO9w==
// @resource wandboxlist https://wandbox.org/api/list.json
// @resource xtermcss https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/xterm/5.5.0/xterm.min.css#sha512-XpXUuzg5afNt1bsgnrOesXP70TLH8tXYYK5sK+Y0UV+YBvJn9EfRFYWy4HT3TVDfH0nl1CO0lwOxIrt2gk9qjg==
// @resource selectpagecss https://aowuucdn.oss-accelerate.aliyuncs.com/css/selectpage.css#sha512-cRXJfA2tEcAxHEKylJfxteY17N7j9fia3waahHOVnvl63uVZT9OQ7jjjpofZMVZ4JSX3BRET+mI8UvKnsXd3NA==
// @resource dialogpolyfillcss https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/dialog-polyfill/0.5.6/dialog-polyfill.min.css#sha512-J2+1q+RsZuJXabBfH1q/fgRr6jMy9By5SwVLk7bScEW7NFJkMUXxfeOyyxtDe6fsaJ4jsciexSlGrPYn9YbBIg==
// @license GPL3
// @compatible Chrome
// @compatible Firefox
// @compatible Edge
// @incompatible safari
// @supportURL https://github.com/beijixiaohu/OJBetter/issues
// @name:zh-TW AtCoder Better!
// @name:en AtCoder Better!
// @name:de AtCoder Better!
// @name:fr AtCoder Better!
// @name:ko AtCoder Better!
// @name:pt AtCoder Better!
// @name:ja AtCoder Better!
// @name:es AtCoder Better!
// @name:it AtCoder Better!
// @name:hi AtCoder Better!
// @description 一个适用于 AtCoder 的 Tampermonkey 脚本,增强功能与界面。
// @description:zh-TW 一個適用於 AtCoder 的 Tampermonkey 腳本,增強功能與界面。
// @description:en A Tampermonkey script for AtCoder that enhances functionality and interface.
// @description:de Ein Tampermonkey-Skript für AtCoder, das Funktionalität und Benutzeroberfläche verbessert.
// @description:fr Un script Tampermonkey pour AtCoder qui améliore les fonctionnalités et l'interface.
// @description:ko AtCoder를 위한 Tampermonkey 스크립트로 기능과 인터페이스를 개선합니다.
// @description:pt Um script Tampermonkey para AtCoder que aprimora a funcionalidade e a interface.
// @description:ja AtCoder用のTampermonkeyスクリプトで機能とインターフェースを強化します。
// @description:es Un script Tampermonkey para AtCoder que mejora la funcionalidad y la interfaz.
// @description:it Uno script Tampermonkey per AtCoder che migliora la funzionalità e l'interfaccia.
// @description:hi AtCoder के लिए एक Tampermonkey स्क्रिप्ट जो कार्यक्षमता और इंटरफ़ेस को बेहतर बनाता है।
// @downloadURL https://update.greasyfork.org/scripts/471106/Atcoder%20Better%21.user.js
// @updateURL https://update.greasyfork.org/scripts/471106/Atcoder%20Better%21.meta.js
// ==/UserScript==
/**
* @namespace OJBetter
* @desc 主命名空间
*/
const OJBetter = {};
/**
* @namespace state
* @desc 描述脚本的当前状态。
* @memberof OJBetter
*/
OJBetter.state = {
/** @type {string} 脚本名*/
name: GM_info.script.name,
/** @type {string} 格式化后的脚本名*/
formatName: undefined,
/** @type {string} 版本号*/
version: GM_info.script.version,
/** @type {boolean?} 是否跳过页面加载等待 */
notWaiteLoaded: undefined,
/** @type {string} 最后公告版本,用于标识版本更新完成提示 */
lastAnnounceVer: undefined,
/** @type {string} 最后读取的有效公告版本 */
lastReadAnnounceVer: undefined,
/** @type {number} 当前已打开的模态对话框数量*/
openDialogCount: 0
};
/**
* @namespace common
* @desc 通用设置和属性。
* @memberof OJBetter
*/
OJBetter.common = {
/** @type {string} 网站的主机地址 */
hostAddress: location.origin,
/** @type {string} 网站当前真实的黑暗模式 */
realDarkMode: undefined,
/** @type {string?} AtCoder的CSRF令牌 */
at_csrf_token: undefined,
/** @type {Array?} 任务队列 */
taskQueue: undefined,
/** @type {object} OJBetter数据库连接实例*/
database: undefined,
/** @type {object} turndownService实例*/
turndownService: undefined,
};
/**
* @namespace basic
* @desc 基本的用户界面设置。
* @memberof OJBetter
*/
OJBetter.basic = {
/** @type {string} 黑暗模式设置 */
darkMode: undefined,
/** @type {boolean?} 是否展开折叠块 */
expandFoldingblocks: undefined,
/** @type {boolean?} 是否开启折叠块渲染性能优化 */
renderPerfOpt: undefined,
/** @type {boolean?} 是否开启下拉选择框性能优化 */
selectElementPerfOpt: undefined,
/** @type {boolean?} 评论区分页 */
commentPaging: undefined,
/** @type {boolean?} 显示跳转到Luogu按钮 */
showJumpToLuogu: undefined,
/** @type {boolean?} 显示跳转到Virtual Judge按钮 */
showCF2vjudge: undefined,
/** @type {boolean?} 比赛排行榜重新着色 */
standingsRecolor: undefined
};
/**
* @namespace typeOfPage
* @desc 页面类型判断。
* @memberof OJBetter
*/
OJBetter.typeOfPage = {
/** @type {boolean?} 是否是轻量站 */
is_mSite: false,
/** @type {boolean?} 是否是acmsguru页面 */
is_acmsguru: false,
/** @type {boolean?} 是否是旧版LaTeX页面 */
is_oldLatex: false,
/** @type {boolean?} 是否是题目集页面 */
is_contest: undefined,
/** @type {boolean?} 是否是题目页面 */
is_problem: undefined,
/** @type {boolean?} 是否是完整的问题集页面 */
is_completeProblemset: false,
/** @type {boolean?} 是否是问题集中的问题页面 */
is_problemset_problem: false,
/** @type {boolean?} 是否是问题集页面 */
is_problemset: false,
/** @type {boolean?} 是否是Codeforces排名页面 */
is_cfStandings: false,
/** @type {boolean?} 是否是提交页面 */
is_submitPage: false,
/** @type {boolean?} 是否是代码状态页面 */
is_statePage: false,
/** @type {boolean?} 是否是提交记录页面 */
is_submissions: false,
/** @type {boolean?} 是否是主页 */
is_homepage: undefined,
/** @type {boolean?} 是否选择的是英语页面 */
isEnglishLanguage: undefined,
/** @type {boolean?} 是否是题解页面 */
isEditorial: undefined,
};
/**
* @namespace localization
* @desc 本地化设置。
* @memberof OJBetter
*/
OJBetter.localization = {
/** @type {string?} 网站语言 */
websiteLang: undefined,
/** @type {string?} 脚本语言 */
scriptLang: undefined
};
/**
* @namespace translation
* @desc 翻译设置。
* @memberof OJBetter
*/
OJBetter.translation = {
/** @type {string?} 翻译服务选择 */
choice: undefined,
/** @type {string?} 目标语言 */
targetLang: undefined,
comment: {
/** @type {string?} 评论翻译服务选择 */
choice: undefined,
/** @type {string?} 评论翻译模式 */
transMode: undefined
},
auto: {
/** @type {boolean?} 自动翻译开关 */
enabled: undefined,
/** @type {number?} 短文本长度限制 */
shortTextLength: undefined,
mixTrans: {
/** @type {boolean?} 混合翻译开关 */
enabled: undefined,
/** @type {Array?} 混合翻译服务列表 */
servers: undefined
}
},
memory: {
/** @type {boolean?} 翻译记忆开关 */
enabled: undefined,
/** @type {Object?} 翻译记忆树 */
ttTree: undefined
},
/** @type {string?} 重翻译时的行为 */
retransAction: undefined,
/** @type {number?} 等待时间 */
waitTime: undefined,
/** @type {boolean?} 替换符 */
replaceSymbol: undefined,
/** @type {boolean?} 过滤文本中的*号 */
filterTextWithoutEmphasis: undefined
};
/**
* @namespace clist
* @desc Clist相关设置。
* @memberof OJBetter
*/
OJBetter.clist = {
enabled: {
/** @type {boolean?} 比赛页面开关 */
contest: undefined,
/** @type {boolean?} 问题页面开关 */
problem: undefined,
/** @type {boolean?} 问题集页面开关 */
problemset: undefined
},
/** @type {boolean?} Rating数据防剧透 */
ratingHidden: undefined,
/** @type {string?} Clist key */
authorization: undefined
};
/**
* @namespace monaco
* @desc Monaco编辑器配置。
* @memberof OJBetter
*/
OJBetter.monaco = {
/** @type {boolean?} 在问题页面上启用Monaco编辑器 */
enableOnProblemPage: undefined,
/** @type {boolean?} 美化pre代码块 */
beautifyPreBlocks: undefined,
/** @type {boolean} Monaco编辑器加载完成标志 */
loaderOnload: false,
lsp: {
/** @type {Array?} LSP套接字数组 */
socket: [],
/** @type {boolean?} 是否启用LSP */
enabled: undefined,
/** @type {string?} 工作路径 */
workUri: undefined,
/** @type {string?} 套接字URL */
socketUrl: undefined
},
complet: {
/** @type {boolean?} 是否启用C++代码补全模板 */
cppCodeTemplate: undefined,
/** @type {Object?} 自定义配置 */
customConfig: undefined
},
/** @type {Object?} Monaco编辑器实例 */
editor: null,
/** @type {Array?} 代码块美化的Monaco编辑器实例 */
beautifyEditor: [],
/** @type {string?} 在线编译器选择 */
onlineCompilerChoice: undefined,
/** @type {string?} 记忆编译器语言选择 */
compilerSelection: undefined,
/** @type {string?} 当前选择的语言 */
nowLangSelect: undefined,
setting: {
/** @type {Array?} 语言设置数组 */
language: [],
/** @type {string?} 位置 */
position: undefined,
/** @type {boolean} 位置初始化标志 */
position_initialized: false,
/** @type {number?} 字体大小 */
fontsize: undefined,
/** @type {boolean?} 鼠标滚动锁定 */
alwaysConsumeMouseWheel: undefined,
/** @type {boolean?} 提交代码二次确认 */
isCodeSubmitDoubleConfirm: undefined,
/** @type {boolean?} 测试通过后自动提交 */
autoSubmitAfterPass: undefined,
/** @type {string?} 提交按钮位置 */
submitButtonPosition: undefined,
/** @type {boolean?} 自动保存代码 */
autoMemoryCode: undefined
}
};
/**
* @namespace deepl
* @desc DeepL翻译服务配置。
* @memberof OJBetter
*/
OJBetter.deepl = {
/** @type {Object?} DeepL配置对象 */
configs: undefined,
config: {
/** @type {string?} 类型 */
type: undefined,
/** @type {string?} 名称 */
name: undefined,
/** @type {string?} API类型 */
apiGenre: undefined,
/** @type {string?} API密钥 */
key: undefined,
/** @type {string?} 代理 */
proxy: undefined,
/** @type {Object?} 额外请求头 */
header: undefined,
/** @type {Object?} 额外请求数据 */
data: undefined,
quota: {
/** @type {string?} 余额URL */
url: undefined,
/** @type {string?} 余额请求方法 */
method: undefined,
/** @type {Object?} 余额请求头 */
header: undefined,
/** @type {Object?} 余额请求数据 */
data: undefined,
/** @type {number?} 剩余配额 */
surplus: undefined
}
},
/** @type {boolean?} 启用重点保护 */
enableEmphasisProtection: undefined,
/** @type {boolean?} 启用链接保护 */
enableLinkProtection: undefined
};
/**
* @namespace chatgpt
* @desc ChatGPT服务配置。
* @memberof OJBetter
*/
OJBetter.chatgpt = {
/** @type {Object?} ChatGPT配置对象 */
configs: undefined,
config: {
/** @type {string?} 名称 */
name: undefined,
/** @type {string?} 模型 */
model: undefined,
/** @type {string?} API密钥 */
key: undefined,
/** @type {string?} 代理 */
proxy: undefined,
/** @type {Object?} 额外请求头 */
header: undefined,
/** @type {Object?} 额外请求数据 */
data: undefined,
quota: {
/** @type {string?} 余额URL */
url: undefined,
/** @type {string?} 余额请求方法 */
method: undefined,
/** @type {Object?} 余额请求头 */
header: undefined,
/** @type {Object?} 余额请求数据 */
data: undefined,
/** @type {number?} 剩余配额 */
surplus: undefined
}
},
/** @type {boolean?} 是否为流式传输 */
isStream: undefined,
/** @type {string?} 是否使用自定义Prompt */
customPrompt: undefined,
/** @type {boolean?} 是否作为系统Prompt */
asSystemPrompt: undefined
};
/**
* @namespace preference
* @desc 偏好设置
* @memberof OJBetter
*/
OJBetter.preference = {
/** @type {boolean?} 是否显示加载动画 */
showLoading: undefined,
/** @type {boolean?} 是否显示悬停目标区域 */
hoverTargetAreaDisplay: undefined,
/** @type {string?} 按钮图标大小 */
iconButtonSize: undefined,
/** @type {boolean?} 是否显示同比赛题目列表*/
showSameContestProblems: undefined,
};
/**
* @namespace dev
* @desc 维护
* @memberof OJBetter
*/
OJBetter.dev = {
/** @type {boolean?} 是否显示规则标记 */
isRuleMarkingEnabled: undefined,
};
/**
* @namespace about
* @desc 关于页信息
* @memberof OJBetter
*/
OJBetter.about = {
/** @type {string?} 更新通道 */
updateChannel: undefined,
/** @type {string?} 更新源 */
updateSource: undefined
};
/**
* @namespace supportList
* @desc 支持列表
* @memberof OJBetter
*/
OJBetter.supportList = {
/** @type {object} 翻译支持列表和对应语言代码*/
translationSupport: {
'deepl': { 'zh': 'ZH', 'de': 'DE', 'fr': 'FR', 'ko': 'KO', 'pt': 'PT', 'ja': 'JA', 'es': 'ES', 'it': 'IT' },
'iflyrec': { 'zh': '1' },
'youdao': { 'zh': 'zh-CHS', 'zh-Hant': 'zh-CHT', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
'google': { 'zh': 'zh-CN', 'zh-Hant': 'zh-TW', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
'caiyun': { 'zh': 'auto2zh', 'ja': 'auto2ja', 'ko': 'auto2ko', 'es': 'auto2es', 'fr': 'auto2fr' },
'openai': { 'zh': 'Chinese', 'zh-Hant': 'Traditional Chinese', 'de': 'German', 'fr': 'French', 'ko': 'Korean', 'pt': 'Portuguese', 'ja': 'Japanese', 'es': 'Spanish', 'it': 'Italian', 'hi': 'Hindi' }
},
/** @type {object} 更新源支持列表*/
updateSourceSupportList: {
'greasyfork': {
'release': true,
'dev': false
},
'github': {
'release': true,
'dev': true
},
'aliyunoss': {
'release': true,
'dev': true
}
}
}
// ------------------------------
// 一些工具函数
// ------------------------------
/**
* 延迟函数
* @param {number} ms 延迟时间(毫秒)
* @returns {Promise<void>}
*/
function OJB_delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 等待直到指定的条件函数返回true。
*
* @param {() => boolean} conditionCheck 一个无参数的函数,用于检查条件是否满足。当函数返回true时,表示条件已满足。
* @param {number} [interval=100] 检查条件的间隔时间,单位为毫秒。默认为100毫秒。
* @returns {Promise<void>} 返回一个Promise,在条件满足时解决。
*/
async function OJB_waitUntilTrue(conditionCheck, interval = 100) {
return new Promise((resolve) => {
const checkCondition = async () => {
if (conditionCheck()) {
resolve();
} else {
await OJB_delay(interval);
checkCondition();
}
};
checkCondition();
});
}
/**
* 动态加载JavaScript库并返回一个Promise,该Promise在脚本加载完成后解决。
*
* @param {string} url - 要加载的JavaScript库的URL地址。
* @param {string} [expectedHash] - 可选的Base64编码的SHA-512哈希值,用于校验脚本内容。格式为 "sha512-<Base64编码的哈希值>"。
* @returns {Promise<void>} 一个Promise,它在脚本加载并执行完成后解决。
*/
async function OJB_LoadJS(url, expectedHash) {
/**
* 计算给定数据的SHA-512哈希值,并将其转换为十六进制字符串。
*
* @param {string} data - 要计算哈希值的数据。
* @returns {Promise<string>} 一个Promise,它解析为数据的SHA-512哈希值的十六进制字符串。
*/
const calculateHash = async (data) => {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-512', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
};
/**
* 将Base64编码的字符串转换为十六进制字符串。
*
* @param {string} base64 - Base64编码的字符串。
* @returns {string} 转换后的十六进制字符串。
*/
const base64ToHex = (base64) => {
const binaryString = atob(base64);
const byteArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i);
}
return Array.from(byteArray).map(b => b.toString(16).padStart(2, '0')).join('');
};
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch script: ${response.statusText}`);
const scriptContent = await response.text();
if (expectedHash) {
// 去掉前缀 "sha512-"
const base64Hash = expectedHash.replace(/^sha512-/, '');
const actualHash = await calculateHash(scriptContent);
const expectedHashHex = base64ToHex(base64Hash);
if (actualHash !== expectedHashHex) throw new Error('SHA-512 hash mismatch');
}
const scriptElement = document.createElement("script");
scriptElement.textContent = scriptContent;
document.head.prepend(scriptElement);
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
}
/**
* 安全地创建JQuery对象
* @description 通过字符串创建JQuery对象时,如果字符串以空格开头,在某些Jquery版本中会发生错误,过滤空格以安全的创建元素。
* @param {string} string - 字符串。
* @returns {JQuery} JQuery对象
*/
const OJB_safeCreateJQElement = function (string) {
return $(string.replace(/^\s+/, ""));
}
/**
* 将数字或者字符串解析为数字。
* @memberof OJBetter.common
* @param {string} val 要解析的字符串
* @param {boolean} [strict=false] 是否进行严格类型检查
* @returns {number} 解析结果
* @throws {Error} 如果解析失败,则抛出错误
*/
const OJB_parseNumber = (val, strict = false) => {
const num = Number(val);
if (isNaN(num) || (strict && val.toString() !== num.toString())) {
throw new Error('Invalid number');
}
return num;
};
/**
* 将字符串解析为布尔值
* @param {string} val - 要解析的字符串
* @param {boolean} strict - 是否进行严格类型检查
* @returns {boolean} - 解析结果
* @throws {Error} - 如果解析失败,则抛出错误
*/
const OJB_parseBoolean = (val, strict) => {
if (strict) {
if (val === true || val === false) return val;
throw new Error('Invalid boolean');
}
return val === 'true' ? true : val === 'false' ? false : val;
};
/**
* 将字符串解析为对象
* @param {string} val - 要解析的字符串
* @returns {Object} - 解析结果
* @throws {Error} - 如果解析失败,则抛出错误
*/
const OJB_parseObject = val => {
try {
return JSON.parse(val);
} catch {
throw new Error('Invalid JSON');
}
};
/**
* 将字符串解析为键值对数组
* @param {string} val - 要解析的字符串
* @returns {Object[]} - 解析结果
* @throws {Error} - 如果解析失败,则抛出错误
*/
const OJB_parseLinePairArray = val => {
if (typeof val !== 'string' || val.trim() === '') return [];
return val.split("\n").filter(line => line.trim() !== '').map(line => {
const indexOfFirstColon = line.indexOf(":");
if (indexOfFirstColon === -1) throw new Error('Invalid LinePairArray format: ":" is missing');
const key = line.substring(0, indexOfFirstColon).trim();
const value = line.substring(indexOfFirstColon + 1).trim();
return { [key]: value };
});
};
/**
* 移除文本中的HTML标签
* @param {string} text - 包含HTML标签的文本
* @returns {string} - 移除HTML标签后的文本
*/
const OJB_removeHTMLTags = function (text) {
return text.replace(/<\/?[a-zA-Z]+("[^"]*"|'[^']*'|[^'">])*>/g, '');
}
/**
* 获取对象中指定路径表达式的值
* @param {Object} obj - 要计算的对象
* @param {string} pathOrExpression - 要计算的路径表达式
* @returns {any} - 计算结果
* @example
* const obj = {
* "a": {
* "b": 1
* },
* "c": 2
* };
* OJB_evaluatePathOrExpression(obj, "a.b"); // 1
* OJB_evaluatePathOrExpression(obj, "a.b + c"); // 3
* OJB_evaluatePathOrExpression(obj, "a.b + a.c"); // 1
*/
function OJB_evaluatePathOrExpression(obj, pathOrExpression) {
const hasOperator = /[\+\-\*\/]/.test(pathOrExpression);
const getPathValue = (obj, path) => {
return path.split('.').reduce((acc, part) => {
return acc !== undefined && acc !== null && acc.hasOwnProperty(part) ? acc[part] : undefined;
}, obj);
};
const evaluateExpression = (obj, expression) => {
const tokens = expression.split(/([\+\-\*\/])/).map(token => token.trim());
const values = tokens.map(token => {
if (/[\+\-\*\/]/.test(token)) {
return token;
} else {
const value = getPathValue(obj, token);
return value !== undefined ? value : 0;
}
});
const evaluatedExpression = values.join(' ');
try {
return Function(`'use strict'; return (${evaluatedExpression});`)();
} catch (e) {
console.error('Expression evaluation error:', e);
return undefined;
}
};
return hasOperator ? evaluateExpression(obj, pathOrExpression) : getPathValue(obj, pathOrExpression);
}
/**
* 获取 GM 存储的值并根据类型进行处理
* @param {string} key - 要检索的值的键。
* @param {any} defaultValue - 如果值未找到,则返回的默认值。
* @param {Object} [options={}] - 配置选项对象。
* @param {string} [options.type='string'] - 期望的值的类型。可选值:'string', 'number', 'boolean', 'object', 'array', 'linePairArray'。
* @param {boolean} [options.strict=false] - 用于数字和布尔类型,表示是否进行严格类型检查。
* @param {string} [options.pathOrExpression=''] - 用于对象或数组类型,表示路径表达式或获取元素的索引。
* @returns {any} - 检索到的值。
*/
const OJB_getGMValue = (key, defaultValue, { type = 'string', strict = false, pathOrExpression = '' } = {}) => {
let value = GM_getValue(key);
if (value === undefined || value === null || value === "") {
GM_setValue?.(key, defaultValue);
return defaultValue;
}
const parsers = {
string: val => val,
number: (val) => OJB_parseNumber(val, strict),
boolean: (val) => OJB_parseBoolean(val, strict),
object: OJB_parseObject,
array: OJB_parseObject,
linePairArray: OJB_parseLinePairArray
};
if (!(type in parsers)) {
console.error(`Unsupported type: ${type}`);
return defaultValue;
}
try {
value = parsers[type](value);
} catch (e) {
console.error('Error:', e.message);
return defaultValue;
}
// The pathOrExpression processing is not applicable to linePairArray type
if ((type === 'object' || type === 'array') && pathOrExpression) {
const evaluated = OJB_evaluatePathOrExpression(value, pathOrExpression);
if (evaluated === undefined) {
console.error('Path or expression evaluation returned undefined');
return defaultValue;
}
value = evaluated;
}
return value;
};
/**
* 版本号比较方法
* @param {string} version1 版本号1
* @param {string} version2 版本号2
* @returns {number} -1: version1 < version2, 0: version1 = version2, 1: version1 > version2
*/
const OJB_compareVersions = function (version1 = "0", version2 = "0") {
const v1Array = version1.split(".").map(Number);
const v2Array = version2.split(".").map(Number);
const length = Math.max(v1Array.length, v2Array.length);
for (let i = 0; i < length; i++) {
const diff = (v1Array[i] || 0) - (v2Array[i] || 0);
if (diff) return Math.sign(diff);
}
return 0;
}
/**
* 获取上一个主版本号
* @param {string} currentVersion 当前版本号
* @returns {string} 上一个主版本号
*/
const OJB_getPreviousVersion = function (currentVersion) {
const versionArray = currentVersion.split(".").map(Number);
let lastNonZeroIndex = versionArray.length - 1;
while (lastNonZeroIndex >= 0 && versionArray[lastNonZeroIndex] === 0) {
lastNonZeroIndex--;
}
if (lastNonZeroIndex >= 0) {
versionArray[lastNonZeroIndex]--;
for (let i = lastNonZeroIndex + 1; i < versionArray.length; i++) {
versionArray[i] = 0;
}
}
return versionArray.join(".");
};
/**
* 在指定根节点下观察指定选择器的元素,当元素存在时,执行回调函数
* @param {Object} options - 配置对象
* @param {string} options.selector - CSS选择器文本
* @param {Function} options.callback - 回调函数,接收变动的节点作为参数
* @param {Boolean} [options.triggerOnExist=true] - 如果为true,元素已存在时立即触发一次回调
* @param {Element} [options.root=document.body] - 在哪个根节点下监听变化
* @param {Boolean} [options.subtree=false] - 是否监听子树变化(即非直接子元素)
*/
function OJB_observeElement({
selector,
callback,
triggerOnExist = true,
root = document.body,
subtree = false
}) {
// 尝试获取选择器指定的元素
const targetNode = root.querySelector(selector);
if (targetNode) {
// 如果元素已存在,直接开始观察
observeAndReport(targetNode, callback);
// 如果triggerOnExist为true,则立即触发一次回调
if (triggerOnExist) {
callback(targetNode);
}
} else {
// 如果元素不存在,监听DOM变化直到该元素被添加
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE && node.matches(selector)) {
observeAndReport(node, callback);
if (triggerOnExist) {
callback(node);
}
observer.disconnect(); // 停止监听
}
});
});
});
observer.observe(root, { childList: true, subtree, attributes: false });
}
function observeAndReport(node, callback) {
const childObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((addedNode) => {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
callback(addedNode); // 执行回调函数
}
});
});
});
childObserver.observe(node, { childList: true, subtree: true, attributes: false });
}
}
/**
* 初始化全局变量
*/
async function initVar() {
const { hostname, href } = window.location;
OJBetter.state.formatName = (() => OJBetter.state.name
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, ''))();
OJBetter.state.lastAnnounceVer = OJB_getGMValue("lastAnnounceVer", "0");
OJBetter.state.lastReadAnnounceVer = OJB_getGMValue("lastReadAnnounceVer", "0");
OJBetter.typeOfPage.is_contest = /\/contests\/[^\/]+\/tasks\/?$/.test(href);
OJBetter.typeOfPage.is_problem = href.includes('/tasks/');
OJBetter.typeOfPage.is_homepage = (href === 'https://atcoder.jp/' || href === 'https://atcoder.jp/?lang=ja');
OJBetter.typeOfPage.isEnglishLanguage = $('meta[http-equiv="Content-Language"]').attr('content') === 'en';
OJBetter.typeOfPage.isEditorial = href.includes("editorial");
OJBetter.localization.websiteLang = OJB_getGMValue("localizationLanguage", "zh");
OJBetter.localization.scriptLang = OJB_getGMValue("scriptL10nLanguage", "zh");
OJBetter.basic.renderPerfOpt = OJB_getGMValue("renderPerfOpt", false);
OJBetter.basic.selectElementPerfOpt = OJB_getGMValue("selectElementPerfOpt", true);
OJBetter.basic.commentPaging = OJB_getGMValue("commentPaging", true);
OJBetter.basic.showJumpToLuogu = OJB_getGMValue("showJumpToLuogu", true);
OJBetter.basic.showCF2vjudge = OJB_getGMValue("showCF2vjudge", true);
OJBetter.basic.standingsRecolor = OJB_getGMValue("standingsRecolor", true);
OJBetter.state.notWaiteLoaded = OJB_getGMValue("notWaiteLoaded", false);
OJBetter.translation.targetLang = OJB_getGMValue("transTargetLang", "zh");
OJBetter.translation.choice = OJB_getGMValue("translation", "deepl");
OJBetter.translation.comment.transMode = OJB_getGMValue("commentTranslationMode", "0");
OJBetter.translation.comment.choice = OJB_getGMValue("commentTranslationChoice", "0");
OJBetter.translation.memory.enabled = OJB_getGMValue("memoryTranslateHistory", true);
OJBetter.translation.auto.enabled = OJB_getGMValue("autoTranslation", false);
OJBetter.translation.auto.shortTextLength = OJB_getGMValue("shortTextLength", "2000");
OJBetter.translation.retransAction = OJB_getGMValue("retransAction", "0");
OJBetter.translation.waitTime = OJB_getGMValue("transWaitTime", "200");
OJBetter.translation.auto.mixTrans.enabled = OJB_getGMValue("allowMixTrans", true);
OJBetter.translation.auto.mixTrans.servers = OJB_getGMValue("mixedTranslation", ['deepl', 'iflyrec', 'youdao', 'caiyun']);
OJBetter.common.taskQueue = new TaskQueue();
OJBetter.translation.replaceSymbol = OJB_getGMValue("replaceSymbol", "2");
OJBetter.translation.filterTextWithoutEmphasis = OJB_getGMValue("filterTextWithoutEmphasis", false);
OJBetter.clist.enabled.contest = OJB_getGMValue("showClistRating_contest", false);
OJBetter.clist.enabled.problem = OJB_getGMValue("showClistRating_problem", false);
OJBetter.clist.enabled.problemset = OJB_getGMValue("showClistRating_problemset", false);
OJBetter.clist.ratingHidden = OJB_getGMValue("RatingHidden", false);
OJBetter.clist.authorization = OJB_getGMValue("clist_Authorization", "");
//deepl
OJBetter.deepl.config.type = OJB_getGMValue("deepl_type", "free");
OJBetter.deepl.configs = OJB_getGMValue("deepl_config", {
"choice": "",
"configurations": []
});
if (OJBetter.deepl.configs.choice !== "" && OJBetter.deepl.configs.configurations.length !== 0) {
const choice = OJBetter.deepl.configs.choice;
const configuration = OJBetter.deepl.configs.configurations.find(obj => obj.name === choice);;
if (configuration == undefined) {
let existingConfig = GM_getValue('deepl_config');
existingConfig.choice = "";
GM_setValue('deepl_config', existingConfig);
location.reload();
}
OJBetter.deepl.config.name = configuration.name;
OJBetter.deepl.config.apiGenre = configuration.apiGenre;
OJBetter.deepl.config.key = configuration.key;
OJBetter.deepl.config.proxy = configuration.proxy;
OJBetter.deepl.config.header = OJB_parseLinePairArray(configuration._header);
OJBetter.deepl.config.data = OJB_parseLinePairArray(configuration._data);
OJBetter.deepl.config.quota.url = configuration.quota_url;
OJBetter.deepl.config.quota.method = configuration.quota_method;
OJBetter.deepl.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
OJBetter.deepl.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
OJBetter.deepl.config.quota.surplus = configuration.quota_surplus;
}
OJBetter.deepl.enableEmphasisProtection = OJB_getGMValue("enableEmphasisProtection", true);
OJBetter.deepl.enableLinkProtection = OJB_getGMValue("enableLinkProtection", true);
//openai
OJBetter.chatgpt.isStream = OJB_getGMValue("openai_isStream", true);
OJBetter.chatgpt.customPrompt = OJB_getGMValue("openai_customPrompt", '');
OJBetter.chatgpt.asSystemPrompt = OJB_getGMValue("openai_asSystemPrompt", false);
OJBetter.chatgpt.configs = OJB_getGMValue("chatgpt_config", {
"choice": "",
"configurations": []
});
if (OJBetter.chatgpt.configs.choice !== "" && OJBetter.chatgpt.configs.configurations.length !== 0) {
const choice = OJBetter.chatgpt.configs.choice;
const configuration = OJBetter.chatgpt.configs.configurations.find(obj => obj.name === choice);;
if (configuration == undefined) {
let existingConfig = GM_getValue('chatgpt_config');
existingConfig.choice = "";
GM_setValue('chatgpt_config', existingConfig);
location.reload();
}
OJBetter.chatgpt.config.name = configuration.name;
OJBetter.chatgpt.config.model = configuration.model;
OJBetter.chatgpt.config.key = configuration.key;
OJBetter.chatgpt.config.proxy = configuration.proxy;
OJBetter.chatgpt.config.header = OJB_parseLinePairArray(configuration._header);
OJBetter.chatgpt.config.data = OJB_parseLinePairArray(configuration._data);
OJBetter.chatgpt.config.quota.url = configuration.quota_url;
OJBetter.chatgpt.config.quota.method = configuration.quota_method;
OJBetter.chatgpt.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
OJBetter.chatgpt.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
OJBetter.chatgpt.config.quota.surplus = configuration.quota_surplus;
}
// 编辑器
// if (!OJBetter.typeOfPage.is_mSite) OJBetter.common.cf_csrf_token = Codeforces.getCsrfToken();
// else OJBetter.common.cf_csrf_token = "";
OJBetter.common.at_csrf_token = csrfToken;
// OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "61");
OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "5001");
OJBetter.monaco.setting.fontsize = OJB_getGMValue("editorFontSize", "15");
OJBetter.monaco.enableOnProblemPage = OJB_getGMValue("problemPageCodeEditor", true);
OJBetter.monaco.beautifyPreBlocks = OJB_getGMValue("beautifyPreBlocks", true);
OJBetter.monaco.complet.cppCodeTemplate = OJB_getGMValue("cppCodeTemplateComplete", true);
OJBetter.monaco.onlineCompilerChoice = OJB_getGMValue("onlineCompilerChoice", "official");
OJBetter.monaco.setting.isCodeSubmitDoubleConfirm = OJB_getGMValue("isCodeSubmitConfirm", true);
OJBetter.monaco.setting.autoSubmitAfterPass = OJB_getGMValue("autoSubmitAfterPass", false);
OJBetter.monaco.setting.alwaysConsumeMouseWheel = OJB_getGMValue("alwaysConsumeMouseWheel", true);
OJBetter.monaco.setting.submitButtonPosition = OJB_getGMValue("submitButtonPosition", "bottom");
OJBetter.monaco.setting.autoMemoryCode = OJB_getGMValue("autoMemoryCode", true);
//自定义补全
OJBetter.monaco.complet.customConfig = OJB_getGMValue("Complet_config", {
"choice": -1,
"configurations": []
});
// monaco
OJBetter.monaco.lsp.enabled = OJB_getGMValue("useLSP", false);
OJBetter.monaco.setting.position = OJB_getGMValue("monacoEditor_position", "initial");
OJBetter.monaco.lsp.workUri = OJB_getGMValue("OJBetter_Bridge_WorkUri", "C:/OJBetter_Bridge");
OJBetter.monaco.lsp.socketUrl = OJB_getGMValue("OJBetter_Bridge_SocketUrl", "ws://127.0.0.1:2323/");
OJBetter.preference.showLoading = OJB_getGMValue("showLoading", true);
OJBetter.preference.hoverTargetAreaDisplay = OJB_getGMValue("hoverTargetAreaDisplay", false);
OJBetter.preference.showSameContestProblems = OJB_getGMValue("showSameContestProblems", false);
OJBetter.basic.expandFoldingblocks = OJB_getGMValue("expandFoldingblocks", true);
OJBetter.preference.iconButtonSize = OJB_getGMValue("iconButtonSize", "16");
OJBetter.dev.isRuleMarkingEnabled = OJB_getGMValue("isRuleMarkingEnabled", false);
OJBetter.about.updateChannel = OJB_getGMValue("updateChannel", "release");
OJBetter.about.updateSource = OJB_getGMValue("updateSource", "aliyunoss");
}
/**
* 显示警告消息
*/
function showWarnMessage() {
if (OJBetter.typeOfPage.is_oldLatex) {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_oldLatex', { ns: 'alert' })}`, 'warning');
}
if (OJBetter.typeOfPage.is_acmsguru) {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_acmsguru', { ns: 'alert' })}`, 'warning');
}
if (OJBetter.translation.comment.transMode == "1") {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_segment', { ns: 'alert' })}`, 'warning');
}
if (OJBetter.translation.comment.transMode == "2") {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_select', { ns: 'alert' })}`, 'warning');
}
if (OJBetter.typeOfPage.is_submitPage && OJBetter.monaco.enableOnProblemPage) {
const loadingMessage = new LoadingMessage();
loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_submitPage', { ns: 'alert' })}`, 'warning');
}
}
// 常量
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>';
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>`;
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
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>`;
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" />
<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" />
<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>`;
/**
* 连接数据库
*/
async function initDB() {
OJBetter.common.database = new Dexie('OJBetterDB');
OJBetter.common.database.version(3).stores({
samplesData: '&url',
editorCode: '&url',
translateData: '&url',
localizeSubsData: '&lang'
});
// 等待数据库打开
await OJBetter.common.database.open();
}
/**
* 清空数据库
*/
async function clearDatabase() {
const isConfirmed = await OJB_createDialog(
i18next.t('isClearDatabase.title', { ns: 'dialog' }),
i18next.t('isClearDatabase.content', { ns: 'dialog' }),
[
i18next.t('isClearDatabase.buttons.0', { ns: 'dialog' }),
i18next.t('isClearDatabase.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
try {
// 开启一个读写事务,包含数据库中的所有表
await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
// 遍历所有表
for (const table of OJBetter.common.database.tables) {
// 清空当前表
await table.clear();
}
});
console.log("All tables in the database have been cleared.");
alert("All tables in the database have been cleared.");
} catch (error) {
console.error("Error clearing the database:", error);
}
}
}
/**
* 导出数据库
* @returns {Promise<string>} 数据库的JSON字符串
*/
async function exportDatabase() {
try {
// 创建一个存储数据的对象
const exportData = {};
// 获取数据库中所有表的名称
const tableNames = OJBetter.common.database.tables.map(table => table.name);
// 遍历每一个表,获取数据
for (const tableName of tableNames) {
const tableData = await OJBetter.common.database.table(tableName).toArray();
exportData[tableName] = tableData;
}
// 将数据对象转换为JSON字符串
const jsonData = JSON.stringify(exportData, null, 4);
return jsonData;
} catch (error) {
console.error("Error exporting database:", error);
}
}
/**
* 导入数据库
* @param {string} jsonData 数据库的JSON字符串
*/
async function importDatabase(jsonData) {
const isConfirmed = await OJB_createDialog(
i18next.t('isImportDatabase.title', { ns: 'dialog' }),
i18next.t('isImportDatabase.content', { ns: 'dialog' }),
[
i18next.t('isImportDatabase.buttons.0', { ns: 'dialog' }),
i18next.t('isImportDatabase.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
try {
// 将JSON字符串解析为对象
const importData = JSON.parse(jsonData);
// 开启一个事务,并清空现有数据
await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
// 清空所有表的数据
for (const tableName of OJBetter.common.database.tables.map(table => table.name)) {
await OJBetter.common.database.table(tableName).clear();
}
// 插入新数据
for (const [tableName, rows] of Object.entries(importData)) {
await OJBetter.common.database.table(tableName).bulkAdd(rows);
}
});
alert("Data imported successfully");
} catch (error) {
console.error("Error importing database:", error);
}
}
}
/**
* 将数据下载为文件
* @param {string} data 数据
* @param {string} filename 文件名,默认为'export.json'
* @param {string} fileType 文件MIME类型,默认为'application/json'
* @returns {void}
*/
function downloadDataAsFile(data, filename = 'export.json', fileType = 'application/json') {
// 创建一个blob对象,指定文件类型
const blob = new Blob([data], { type: fileType });
const url = URL.createObjectURL(blob);
// 创建一个隐藏的a标签,模拟点击进行下载
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
// 清理
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* 从文件中读取数据
* @param {Function} callback 回调函数
* @returns {void}
*/
function readFileInput(callback) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.style.display = 'none'; // 隐藏input元素
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const fileContent = e.target.result;
if (callback && typeof callback === 'function') {
callback(fileContent); // 调用回调函数,传入文件内容
}
};
reader.readAsText(file);
}
};
document.body.appendChild(fileInput);
fileInput.click();
document.body.removeChild(fileInput);
}
/**
* 清除所有设置
*/
async function deleteAllConfigSettings() {
const isConfirmed = await OJB_createDialog(
i18next.t('isDeleteAllConfigSettings.title', { ns: 'dialog' }),
i18next.t('isDeleteAllConfigSettings.content', { ns: 'dialog' }),
[
i18next.t('isDeleteAllConfigSettings.buttons.0', { ns: 'dialog' }),
i18next.t('isDeleteAllConfigSettings.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
const keys = GM_listValues();
keys.forEach(key => {
GM_deleteValue(key);
});
alert('All settings have been deleted.');
window.location.reload();
}
}
/**
* 导出设置到JSON
* @returns {string} JSON字符串
*/
function exportSettingsToJSON() {
const keys = GM_listValues();
let settings = {};
keys.forEach(key => {
settings[key] = GM_getValue(key);
});
return JSON.stringify(settings, null, 4);
}
/**
* 从JSON导入设置
* @param {string} jsonData JSON字符串
* @returns {void}
*/
async function importSettingsFromJSON(jsonData) {
const isConfirmed = await OJB_createDialog(
i18next.t('isImportSettings.title', { ns: 'dialog' }),
i18next.t('isImportSettings.content', { ns: 'dialog' }),
[
i18next.t('isImportSettings.buttons.0', { ns: 'dialog' }),
i18next.t('isImportSettings.buttons.1', { ns: 'dialog' })
]
);
if (!isConfirmed) {
let settings;
try {
settings = JSON.parse(jsonData);
} catch (e) {
console.error('JSON parsing error:', e);
return;
}
Object.keys(settings).forEach(key => {
GM_setValue(key, settings[key]);
});
alert('Settings imported successfully!');
window.location.reload();
}
}
/**
* 加载元素本地化语言数据
* @param {JQuery} element jQuery元素
* @param {number} [retries=10] 重试次数
* @param {number} [interval=50] 重试间隔
*/
function elementLocalize(element, retries = 10, interval = 50) {
if ($.isFunction(element.localize)) {
element.localize();
} else if (retries > 0) {
setTimeout(elementLocalize, interval, element, retries - 1, interval);
} else {
console.error('Unable to localize', element);
}
}
// 切换系统黑暗监听
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const changeEventListeners = [];
/**
* 处理颜色模式变化事件
* @param {MediaQueryListEvent} event - 媒体查询事件对象
*/
const handleColorSchemeChange = (event) => {
const theme = event.matches ? 'dark' : 'light';
// 更新页面主题
$('html').attr('data-theme', theme);
OJBetter.common.realDarkMode = theme;
const updateMonacoTheme = (theme) => {
const intervalId = setInterval(() => {
if (OJBetter?.monaco?.editor) {
monaco.editor.setTheme(theme);
clearInterval(intervalId);
}
}, 100);
OJBetter.monaco.beautifyEditor.forEach((editor) => {
editor.updateOptions({ theme });
});
};
if (event.matches) {
updateMonacoTheme('vs-dark');
} else {
const originalColor = $(this).data("original-color");
$(this).css("background-color", originalColor);
updateMonacoTheme('vs');
}
};
// 黑暗模式
(function setDark() {
// 初始化
function setDarkTheme() {
const htmlElement = document.querySelector('html');
if (htmlElement) {
htmlElement.setAttribute('data-theme', 'dark');
const intervalId = setInterval(() => {
if (OJBetter.monaco && OJBetter.monaco.editor) {
monaco.editor.setTheme('vs-dark');
clearInterval(intervalId);
}
}, 100);
} else {
setTimeout(setDarkTheme, 100);
}
}
// 设置黑暗模式和监听器
OJBetter.basic.darkMode = OJB_getGMValue("darkMode", "follow");
if (OJBetter.basic.darkMode == "dark") {
OJBetter.common.realDarkMode = 'dark';
setDarkTheme();
} else if (OJBetter.basic.darkMode == "light") {
OJBetter.common.realDarkMode = 'light';
} else if (OJBetter.basic.darkMode == "follow") {
OJBetter.common.realDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// 添加事件监听器
changeEventListeners.push(handleColorSchemeChange);
mediaQueryList.addEventListener('change', handleColorSchemeChange);
if (window.matchMedia('(prefers-color-scheme: dark)').matches) setDarkTheme();
}
// 定义全局变量
GM_addStyle(`
/* 黑暗支持 */
html[data-theme=dark]:root {
color-scheme: light dark;
}
/* 颜色 */
:root {
/* 文字颜色 */
--ojb-color-text-primary: #a0adb9; /* 主要文字颜色 */
--ojb-color-text-secondary: #9AA4B1; /* 次要文字颜色 */
--ojb-color-text-tertiary: #9BA5B2; /* 第三级文字颜色 */
--ojb-color-text-success: #43A047; /* 成功状态文字颜色 */
--ojb-color-text-highlight: #cbd6e2; /* 高亮文字颜色 */
--ojb-color-text-disabled: #506778; /* 禁用状态文字颜色 */
--ojb-color-text-icon-success: #2e7d32; /* 成功状态图标颜色 */
--ojb-color-text-link: #4b8eda; /* 链接颜色 */
/* 背景颜色 */
--ojb-color-bg-primary: #22272e; /* 主背景颜色 */
--ojb-color-bg-secondary: #2d333b; /* 次级背景颜色 */
--ojb-color-bg-disabled: #24292e; /* 禁用元素背景颜色 */
/* 边框颜色 */
--ojb-color-border-primary: #48535F; /* 主要边框颜色 */
--ojb-color-border-disabled: #404950; /* 禁用状态边框颜色 */
--ojb-color-border-dashed-hover: #03A9F4; /* 虚线边框悬浮颜色 */
--ojb-color-border-radio-checked: #326154; /* 选中的单选框边框颜色 */
/* 阴影颜色 */
--ojb-shadow-standard: 0px 0px 0.5px 0.5px #3A4048; /* 标准阴影 */
--ojb-shadow-menu-modal: 0px 0px 0px 4px #2d333b; /* 菜单和模态框阴影 */
/* 区域遮罩颜色 */
--ojb-overlay-background: repeating-linear-gradient(135deg, #49525f6e, #49525f6e 30px, #49525f29 0px, #49525f29 55px); /* 区域遮罩背景 */
/* 文字阴影 */
--ojb-text-shadow-icon: 1px 1px 0px #2d333b, 1px -1px 0px #2d333b, -1px -1px 0px #2d333b, -1px 1px 0px #2d333b; /* 图标文字阴影 */
}
/* 边框样式 */
:root {
/* 边框样式 */
--ojb-border-width: 1px; /* 边框宽度 */
--ojb-border-style-solid: solid; /* 实线样式 */
--ojb-border-style-dashed: dashed; /* 虚线样式 */
--ojb-border-radius-small: 4px; /* 小圆角 */
--ojb-border-radius-medium: 8px; /* 中圆角 */
--ojb-border-radius-large: 12px; /* 大圆角 */
/* 组合边框样式 */
--ojb-border-solid-primary: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-primary); /* 主要实线边框 */
--ojb-border-dashed: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-primary); /* 主要虚线边框 */
--ojb-border-dashed-hover: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-dashed-hover); /* 悬浮虚线边框 */
--ojb-border-solid-disabled: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-disabled); /* 禁用状态实线边框 */
}
`);
// OJBetter界面样式
GM_addStyle(`
/* 主要文字颜色 */
html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
html[data-theme=dark] .alert-warning, html[data-theme=dark] .markItUpEditor,
html[data-theme=dark] .translate-problem-statement, html[data-theme=dark] .OJBetter_setting_menu,
html[data-theme=dark] .help_tip .tip_text,
html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
html[data-theme=dark] #items-per-page, html[data-theme=dark] #pagBar,
html[data-theme=dark] .OJBetter_setting_sidebar li a:link,
html[data-theme=dark] .popup .content{
color: var(--ojb-color-text-primary);
}
/* 次要文字颜色 */
html[data-theme=dark] .ojb_btn:hover, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_statusBar,
html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #addCustomTest,
html[data-theme=dark] #customTestBlock, html[data-theme=dark] .OJBetter_setting_list.alert_info{
color: var(--ojb-color-text-secondary);
}
/* 文字颜色3 */
html[data-theme=dark] .ojb_btn{
color: var(--ojb-color-text-tertiary);
}
/* 文字颜色 浅绿 */
html[data-theme=dark] #SubmitButton{
color: var(--ojb-color-text-success);
}
/* 禁止文字颜色 */
html[data-theme=dark] .ojb_btn[disabled]{
color: var(--ojb-color-text-disabled);
}
/* 主要背景层次 */
html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] li#add_button:hover,
html[data-theme=dark] .ojb_btn:hover,
html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
html[data-theme=dark] #OJBetter_SubmitForm input,
html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
html[data-theme=dark] .OJBetter_setting_sidebar li a.active, html[data-theme=dark] .OJBetter_setting_sidebar li,
html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-track, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-track,
html[data-theme=dark] .OJBetter_modal, html[data-theme=dark] .OJBetter_modal button:hover,
html[data-theme=dark] .popup .content,
html[data-theme=dark] .config_bar_list, html[data-theme=dark] #LSPLog,
html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before,
html[data-theme=dark] .OJBetter_setting_menu a, html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button:hover,
html[data-theme=dark] .OJBetter_setting_menu select{
background-color: var(--ojb-color-bg-primary);