- // ==UserScript==
- // @name Linux Do Translate
- // @namespace linux-do-translate
- // @version 0.1.3
- // @author delph1s
- // @license MIT
- // @description 对回复进行翻译
- // @match https://linux.do/t/topic/*
- // @connect api.deeplx.org
- // @connect api.deepl.com
- // @icon https://cdn.linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png
- // @grant unsafeWindow
- // @grant window.close
- // @grant window.focus
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_xmlhttpRequest
- // @require https://cdn.jsdelivr.net/npm/pinyin-pro@3.25.0/dist/index.min.js
- // @run-at document-end
- // ==/UserScript==
-
- (function () {
- 'use strict';
- const REQUIRED_CHARS = 6;
- const SPACE_PRESS_COUNT = 3; // 连按次数
- const SPACE_PRESS_TIMEOUT = 1500; // 连续按键的最大时间间隔(毫秒)
- const DEFAULT_CONFIG = {
- maxRetryTimes: 5,
- deeplxUrl: '',
- deeplxAuthKey: '',
- authKeyPrefix: {
- deeplx: '',
- deeplOfficial: 'deepl-auth-key:',
- openAIOfficial: 'oai-at:',
- openAIProxy: 'oai-at-proxy:',
- },
- enableTranslate: false,
- translateTargetLang: {
- deeplx: 'EN',
- deeplOfficial: 'EN',
- openAIOfficial: 'English',
- openAIProxy: 'English',
- },
- translateModel: 'gpt-3.5-turbo',
- translateLayout: 'up',
- translateSize: 150,
- enablePinYin: false,
- closeConfigAfterSave: true,
- };
-
- const uiIDs = {
- replyControl: 'reply-control',
- configButton: 'multi-lang-say-config-button',
- configPanel: 'multi-lang-say-config-panel',
- deeplxUrlInput: 'deeplx-url-input',
- deeplxAuthKeyInput: 'deeplx-authkey-input',
- translateSizeInput: 'translate-size-input',
- };
-
- let config = {
- maxRetryTimes: GM_getValue('maxRetryTimes', DEFAULT_CONFIG.maxRetryTimes),
- deeplxUrl: GM_getValue('deeplxUrl', DEFAULT_CONFIG.deeplxUrl),
- deeplxAuthKey: GM_getValue('deeplxAuthKey', DEFAULT_CONFIG.deeplxAuthKey),
- authKeyPrefix: GM_getValue('authKeyPrefix', DEFAULT_CONFIG.authKeyPrefix),
- enableTranslate: GM_getValue('enableTranslate', DEFAULT_CONFIG.enableTranslate),
- translateTargetLang: GM_getValue('translateTargetLang', DEFAULT_CONFIG.translateTargetLang),
- translateModel: GM_getValue('translateModel', DEFAULT_CONFIG.translateModel),
- translateLayout: GM_getValue('translateLayout', DEFAULT_CONFIG.translateLayout),
- translateSize: GM_getValue('translateSize', DEFAULT_CONFIG.translateSize),
- enablePinYin: GM_getValue('enablePinYin', DEFAULT_CONFIG.enablePinYin),
- closeConfigAfterSave: GM_getValue('closeConfigAfterSave', DEFAULT_CONFIG.closeConfigAfterSave),
- };
-
- const checkPinYinRequire = () => {
- // 检查是否加载了 pinyin 库
- if (typeof pinyinPro === 'undefined') {
- console.error('pinyin 库未正确加载!');
- config.enablePinYin = false;
- GM_setValue('enablePinYin', false);
- return false;
- }
- return true;
- };
-
- const genFormatDateTime = d => {
- return d.toLocaleString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false,
- });
- };
-
- const genFormatNow = () => {
- return genFormatDateTime(new Date());
- };
-
- /**
- * 获取随机整数
- *
- * @param {number} start 范围开始
- * @param {number} end 范围结束
- * @returns
- */
- const randInt = (start, end) => {
- return Math.floor(Math.random() * (end - start + 1)) + start;
- };
-
- /**
- * 随机睡眠(毫秒)
- *
- * @param {number} start 范围开始
- * @param {number} end 范围结束
- */
- const randSleep = async (start = 2000, end = 3000) => {
- // 生成随机整数 randSleepTime,范围在 start 到 end 之间
- const randSleepTime = getRandomInt(start, end);
- // 睡眠时间
- return await new Promise(resolve => setTimeout(resolve, randSleepTime));
- };
-
- /**
- * 是否相同
- *
- * @param a
- * @param b
- * @returns
- */
- const isEqual = (a, b) => {
- if (a === null || a === undefined || b === null || b === undefined) {
- return a === b;
- }
-
- if (typeof a !== typeof b) {
- return false;
- }
-
- if (typeof a === 'string' || typeof a === 'number' || typeof a === 'boolean') {
- return a === b;
- }
-
- if (Array.isArray(a) && Array.isArray(b)) {
- if (a.length !== b.length) {
- return false;
- }
-
- return a.every((item, index) => isEqual(item, b[index]));
- }
-
- if (typeof a === 'object' && typeof b === 'object') {
- const keysA = Object.keys(a || {});
- const keysB = Object.keys(b || {});
-
- if (keysA.length !== keysB.length) {
- return false;
- }
-
- return keysA.every(key => isEqual(a[key], b[key]));
- }
-
- return false;
- };
-
- /**
- * 判断字符串中是否包含中文字符
- * @param {string} text
- * @returns {boolean}
- */
- const containsChinese = text => {
- return /[\u4e00-\u9fa5]/.test(text);
- };
-
- /**
- * 将中文字符串转换为带拼音的ruby标签字符串
- * @param {string} text
- * @returns {string}
- */
- const convertToRuby = text => {
- // 使用tiny-pinyin将中文转换为拼音,拼音之间用空格分隔
- const pinyin = pinyinPro.pinyin(text);
- // 构建ruby标签
- return `<ruby><rb>${text}</rb><rt>${pinyin}</rt></ruby>`;
- };
-
- const getInvertColor = hex => {
- // 去掉前面的“#”字符
- hex = hex.replace('#', '');
-
- // 如果输入的是3位的hex值,转换为6位的
- if (hex.length === 3) {
- hex = hex
- .split('')
- .map(c => c + c)
- .join('');
- }
-
- // 计算相反的颜色
- const r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16).padStart(2, '0');
- const g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16).padStart(2, '0');
- const b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16).padStart(2, '0');
-
- return `#${r}${g}${b}`;
- };
-
- const deeplxReq = text => {
- return {
- url: config.deeplxUrl,
- headers: {
- 'Content-Type': 'application/json',
- },
- data: JSON.stringify({
- text: text,
- target_lang: 'EN',
- source_lang: 'auto',
- }),
- responseType: 'json',
- };
- };
-
- const deeplxLinuxdoReq = text => {
- return {
- url: `https://api.deeplx.org/${config.deeplxAuthKey}/translate`,
- headers: {
- 'Content-Type': 'application/json',
- },
- data: JSON.stringify({
- text: text,
- target_lang: 'EN',
- source_lang: 'auto',
- }),
- responseType: 'json',
- };
- };
-
- const deeplOfficialReq = text => {
- const authKey = config.deeplxAuthKey.replace(config.authKeyPrefix.deeplOfficial, '');
- const params = new URLSearchParams();
- params.append('text', text);
- params.append('target_lang', 'EN');
- params.append('source_lang', 'ZH');
- return {
- url: 'https://api.deepl.com/v2/translate', // DeepL Pro API
- headers: {
- Authorization: `DeepL-Auth-Key ${authKey}`,
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- data: params.toString(),
- responseType: 'json',
- };
- };
-
- const deeplOfficialRes = res => {
- return res?.translations?.[0]?.text;
- };
-
- const oaiOffcialReq = (
- text,
- model = 'gpt-3.5-turbo',
- url = 'https://api.openai.com/v1/chat/completions',
- temperature = 0.5,
- maxTokens = 32000
- ) => {
- const authKey = config.deeplxAuthKey.replace(config.authKeyPrefix.openAIOfficial, '');
- return {
- url: url,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${authKey}`,
- },
- data: JSON.stringify({
- model: model, // 或者您订阅的其他模型,例如 'gpt-4'
- messages: [
- {
- role: 'system',
- content:
- 'You are a highly skilled translation engine. Your function is to translate texts accurately into the target {{to}}, maintaining the original format, technical terms, and abbreviations. Do not add any explanations or annotations to the translated text.',
- },
- {
- role: 'user',
- content: `Translate the following source text to ${config.translateTargetLang.openAIOfficial}, Output translation directly without any additional text.\nSource Text: ${text}\nTranslated Text:`,
- },
- ],
- temperature: temperature, // 控制生成内容的随机性,范围是 0 到 1
- max_tokens: maxTokens, // 响应的最大标记数
- }),
- responseType: 'json',
- };
- };
-
- const oaiOfficalRes = res => {
- return res.data.choices[0].message.content.trim();
- };
-
- const translateText = text => {
- const isDeeplOfficial = config.deeplxAuthKey.startsWith(config.authKeyPrefix.deeplOfficial);
- const isOpenAIOfficial = config.deeplxAuthKey.startsWith(config.authKeyPrefix.openAIOfficial);
-
- let reqData;
-
- if (!config.deeplxAuthKey) {
- if (!config.deeplxUrl) return '';
- reqData = deeplxReq(text);
- } else if (isDeeplOfficial) {
- reqData = deeplOfficialReq(text);
- } else if (isOpenAIOfficial) {
- reqData = oaiOffcialReq(
- text,
- config.translateModel,
- config.deeplxUrl || 'https://api.openai.com/v1/chat/completions',
- 0.5,
- 1600
- );
- } else {
- reqData = deeplxLinuxdoReq(text);
- }
-
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: 'POST',
- url: reqData.url,
- headers: reqData.headers,
- data: reqData.data,
- responseType: reqData.responseType,
- onload: function (res) {
- console.log('Translation response:', res);
- console.log('Request details:', reqData);
-
- if (res.status === 200) {
- try {
- const response = typeof res.response === 'string' ? JSON.parse(res.response) : res.response;
- console.log('Parsed response:', response);
-
- let translation;
-
- if (isDeeplOfficial) {
- // Pro API 返回格式
- translation = deeplOfficialRes(response);
- console.log('DeepL translation:', translation);
- } else if (isOpenAIOfficial) {
- translation = oaiOfficalRes(response);
- console.log('OAI translation:', translation);
- } else {
- translation = response?.data;
- console.log('DeepLX translation:', translation);
- }
-
- resolve(translation || '');
- } catch (error) {
- console.error('Error parsing response:', error);
- resolve('');
- }
- } else {
- console.error('Translation failed:', {
- status: res.status,
- statusText: res.statusText,
- response: res.response,
- responseText: res.responseText,
- finalUrl: res.finalUrl,
- headers: res.responseHeaders,
- });
- resolve('');
- }
- },
- onerror: function (err) {
- console.error('Translation error details:', {
- error: err,
- errorText: err.toString(),
- status: err.status,
- statusText: err.statusText,
- responseText: err.responseText,
- });
- resolve('');
- },
- });
- });
-
- // return GM_xmlhttpRequest({
- // method: 'POST',
- // url,
- // data: JSON.stringify(data),
- // responseType: 'json',
- // onload: res => {
- // console.log(res);
- // return res.response;
- // },
- // onerror: err => {
- // console.log(err);
- // return '';
- // },
- // });
-
- // return GM.xmlHttpRequest({
- // method: 'POST',
- // url,
- // data: JSON.stringify(data),
- // responseType: 'json',
- // })
- // .then(res => {
- // console.log(res);
- // console.log(`[翻译结果] ${res.response.data}`)
- // return res.response.data;
- // })
- // .catch(err => {
- // console.log(err);
- // return '';
- // });
- };
-
- const processTranslateText = async text => {
- // 定义需要保护的块的正则表达式
- const protectedBlocks = [
- // Markdown 代码块
- {
- regex: /```[\s\S]*?```/g,
- type: 'code'
- },
- // BBCode 标签块 (处理嵌套标签)
- {
- regex: /\[(size|spoiler|center|color|grid).*?\][\s\S]*?\[\/\1\]/g,
- type: 'bbcode'
- },
- // 已有的 ruby 标签
- {
- regex: /<ruby>[\s\S]*?<\/ruby>/g,
- type: 'ruby'
- },
- // HTML 标签块
- {
- regex: /<[^>]+>[\s\S]*?<\/[^>]+>/g,
- type: 'html'
- },
- // 图片标签
- {
- regex: /\[image\]\(.*?\)/g,
- type: 'image'
- }
- ];
-
- // 创建占位符映射
- let placeholders = new Map();
- let placeholderCounter = 0;
-
- // 保护特殊块
- let processedText = text;
- for (const block of protectedBlocks) {
- processedText = processedText.replace(block.regex, match => {
- const placeholder = `__PROTECTED_${block.type}_${placeholderCounter++}__`;
- placeholders.set(placeholder, match);
- return placeholder;
- });
- }
-
- // 处理剩余文本
- const segments = processedText.split(/(\n)/);
- let translatedSegments = [];
-
- for (const segment of segments) {
- if (!segment.trim() || segment === '\n') {
- translatedSegments.push(segment);
- continue;
- }
-
- // 检查是否是占位符
- if (segment.startsWith('__PROTECTED_')) {
- translatedSegments.push(placeholders.get(segment));
- continue;
- }
-
- // 翻译普通文本
- const segmentTranslate = await translateText(segment);
- if (config.translateLayout === 'down') {
- translatedSegments.push(
- `${line}\n[size=${config.translateSize}]${segmentTranslate}[/size]`
- );
- } else {
- translatedSegments.push(
- `[size=${config.translateSize}]<ruby><rb>${segment}</rb><rt>${segmentTranslate}</rt></ruby>[/size]`
- );
- }
- }
-
- // 合并结果
- return translatedSegments.join('');
- };
-
- const processTextArea = () => {
- let textarea = document.querySelector(`#${uiIDs.replyControl} textarea`);
- let text = textarea.value.trim();
- let originalLength = text.length;
-
- if (text.length !== 0 && originalLength >= REQUIRED_CHARS) {
- // 检查是否已存在拼音
- const rubyRegex = /(<ruby>[\s\S]*?<\/ruby>)/g;
-
- // 为中文加入翻译
- if (config.enableTranslate) {
- textarea.value = '开始翻译...';
-
- processTranslateText(text).then(res => {
- textarea.value = res;
-
- // 创建并触发 input 事件
- const inputEvent = new Event('input', {
- bubbles: true,
- cancelable: true,
- });
- // 触发事件
- textarea.dispatchEvent(inputEvent);
- });
- return;
- }
-
- // 为中文加入拼音
- if (!config.enableTranslate && config.enablePinYin) {
- if (containsChinese(text)) {
- // 使用正则表达式将文本分割为已被 <ruby> 包裹的部分和未被包裹的部分
- const parts = text.split(rubyRegex);
-
- // 处理每一部分
- text = parts
- .map(part => {
- if (rubyRegex.test(part)) {
- // 已被 <ruby> 包裹,保持原样
- return part;
- } else {
- // 未被包裹,进一步分割为中文和非中文
- if (containsChinese(part)) {
- const segments = part.split(/([\u4e00-\u9fa5]+)/g);
- return segments
- .map(segment => {
- if (containsChinese(segment)) {
- return convertToRuby(segment);
- } else {
- return segment;
- }
- })
- .join('');
- } else {
- return part;
- }
- }
- })
- .join('');
- }
- }
-
- textarea.value = text;
-
- // 创建并触发 input 事件
- const inputEvent = new Event('input', {
- bubbles: true,
- cancelable: true,
- });
- // 触发事件
- textarea.dispatchEvent(inputEvent);
- }
- };
-
- const handleClick = event => {
- // 修复翻译两次的 BUG
- if (config.enableTranslate) {
- return;
- }
-
- if (event.target && event.target.closest('button.create')) {
- processTextArea();
- }
- };
-
- let spacePresses = 0;
- let lastKeyTime = 0;
- let timeoutHandle = null;
-
- const handleKeydown = event => {
- // console.log(`KeyboardEvent: key='${event.key}' | code='${event.code}'`);
-
- if (event.ctrlKey && event.key === 'Enter') {
- processTextArea();
- return;
- }
-
- // 使用 Alt+D 触发翻译
- if (event.altKey && event.keyCode === 68) {
- event.preventDefault(); // 阻止默认行为
- processTextArea();
- return;
- }
-
- const currentTime = Date.now();
- if (event.code === 'Space') {
- // 如果时间间隔太长,重置计数
- if (currentTime - lastKeyTime > SPACE_PRESS_TIMEOUT) {
- spacePresses = 1;
- } else {
- spacePresses += 1;
- }
-
- lastKeyTime = currentTime;
-
- // 清除之前的定时器
- if (timeoutHandle) {
- clearTimeout(timeoutHandle);
- }
-
- // 设置新的定时器,如果在 SPACE_PRESS_TIMEOUT 毫秒内没有新的按键,则重置计数
- timeoutHandle = setTimeout(() => {
- spacePresses = 0;
- }, SPACE_PRESS_TIMEOUT);
-
- // 检查是否达到了按键次数
- if (spacePresses === SPACE_PRESS_COUNT) {
- spacePresses = 0; // 重置计数
-
- // 执行翻译操作
- processTextArea();
- }
- } else {
- // 如果按下了其他键,重置计数
- spacePresses = 0;
- if (timeoutHandle) {
- clearTimeout(timeoutHandle);
- timeoutHandle = null;
- }
- }
- };
-
- const saveConfig = () => {
- const deeplxUrlInput = document.getElementById(uiIDs.deeplxUrlInput);
- config.deeplxUrl = deeplxUrlInput.value.trim();
- const deeplxAuthKeyInput = document.getElementById(uiIDs.deeplxAuthKeyInput);
- config.deeplxAuthKey = deeplxAuthKeyInput.value.trim();
- const transalteSizeInput = document.getElementById(uiIDs.translateSizeInput);
- config.translateSize = transalteSizeInput.value;
- console.log(config);
-
- GM_setValue('deeplxUrl', config.deeplxUrl);
- GM_setValue('deeplxAuthKey', config.deeplxAuthKey);
- GM_setValue('enableTranslate', config.enableTranslate);
- GM_setValue('translateSize', config.translateSize);
- GM_setValue('enablePinYin', config.enablePinYin);
- GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);
-
- if (config.closeConfigAfterSave) {
- document.getElementById(uiIDs.configPanel).style.display = 'none';
- }
- };
-
- const restoreDefaults = () => {
- if (confirm('确定要将所有设置恢复为默认值吗?')) {
- config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
- GM_setValue('deeplxUrl', config.deeplxUrl);
- GM_setValue('deeplxAuthKey', config.deeplxAuthKey);
- GM_setValue('enableTranslate', config.enableTranslate);
- GM_setValue('translateSize', config.translateSize);
- GM_setValue('enablePinYin', config.enablePinYin);
- GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);
-
- const panel = document.getElementById(uiIDs.configPanel);
- if (panel) {
- updateConfigPanelContent(panel);
- }
- }
- };
-
- const createCheckbox = (id, text, checked, onChange = undefined) => {
- const label = document.createElement('label');
- label.style.display = 'flex';
- label.style.alignItems = 'center';
- label.style.marginBottom = '10px';
- label.style.cursor = 'pointer';
-
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.id = id;
- checkbox.checked = checked;
- checkbox.style.marginRight = '10px';
- if (onChange !== undefined) {
- checkbox.addEventListener('change', e => onChange(e));
- } else {
- checkbox.addEventListener('change', e => {
- config[id] = e.target.checked;
- GM_setValue(id, config[id]);
- });
- }
-
- label.appendChild(checkbox);
- label.appendChild(document.createTextNode(text));
-
- return label;
- };
-
- const createSelect = (id, text, options, defaultValue) => {
- const container = document.createElement('div');
- container.style.display = 'flex';
- container.style.alignItems = 'center';
- container.style.marginBottom = '10px';
-
- const label = document.createElement('label');
- label.htmlFor = id;
- label.textContent = text;
- label.style.marginRight = '10px';
-
- const select = document.createElement('select');
- select.id = id;
- select.style.flex = '1';
-
- options.forEach(option => {
- const optionElement = document.createElement('option');
- optionElement.value = option.value;
- optionElement.textContent = option.text;
- select.appendChild(optionElement);
- });
-
- select.value = defaultValue;
-
- select.addEventListener('change', e => {
- config[id] = e.target.value;
- GM_setValue(id, config[id]);
- });
-
- container.appendChild(select);
- container.appendChild(label);
-
- return container;
- };
-
- const createTextInput = (id, value, labelText, placeholder, type = 'text') => {
- const container = document.createElement('div');
- container.style.marginBottom = '15px';
-
- const label = document.createElement('label');
- label.textContent = labelText;
- label.style.display = 'block';
- label.style.marginBottom = '5px';
- container.appendChild(label);
-
- const inputPlace = document.createElement('input');
- inputPlace.id = id;
- inputPlace.type = type;
- inputPlace.value = value;
- inputPlace.placeholder = placeholder;
- inputPlace.style.width = '100%';
- inputPlace.style.padding = '5px';
- inputPlace.style.border = '1px solid var(--panel-border)';
- inputPlace.style.borderRadius = '4px';
- inputPlace.style.backgroundColor = 'var(--panel-bg)';
- inputPlace.style.color = 'var(--panel-text)';
- container.appendChild(inputPlace);
-
- return [container, inputPlace];
- };
-
- const createTextArea = (id, value, labelText, placeholder) => {
- const container = document.createElement('div');
- container.style.marginBottom = '15px';
-
- const label = document.createElement('label');
- label.textContent = labelText;
- label.style.display = 'block';
- label.style.marginBottom = '5px';
- container.appendChild(label);
-
- const textarea = document.createElement('textarea');
- textarea.id = id;
- if (typeof value === 'string') {
- textarea.value = value;
- } else {
- textarea.value = JSON.stringify(value, null, 2);
- }
- textarea.placeholder = placeholder;
- textarea.rows = 5;
- textarea.style.width = '100%';
- textarea.style.padding = '5px';
- textarea.style.border = '1px solid var(--panel-border)';
- textarea.style.borderRadius = '4px';
- textarea.style.backgroundColor = 'var(--panel-bg)';
- textarea.style.color = 'var(--panel-text)';
- container.appendChild(textarea);
-
- return [container, textarea];
- };
-
- const createButton = (text, onClick, primary = false) => {
- const button = document.createElement('button');
- button.textContent = text;
- button.style.padding = '8px 16px';
- button.style.border = 'none';
- button.style.borderRadius = '4px';
- button.style.cursor = 'pointer';
- button.style.backgroundColor = primary ? '#0078d7' : '#f0f0f0';
- button.style.color = primary ? '#ffffff' : '#333333';
- button.addEventListener('click', onClick);
- return button;
- };
-
- const updateConfigPanelContent = panel => {
- panel.innerHTML = '';
-
- const title = document.createElement('h3');
- title.textContent = '配置';
- title.style.marginTop = '0';
- title.style.marginBottom = '15px';
- panel.appendChild(title);
-
- panel.appendChild(
- createTextInput(uiIDs.deeplxUrlInput, config.deeplxUrl, 'Deeplx URL', '填写 Deeplx 的请求地址')[0]
- );
- panel.appendChild(
- createTextInput(uiIDs.deeplxAuthKeyInput, config.deeplxAuthKey, 'Deeplx Auth Key', '填写 connect 的 key')[0]
- );
- panel.appendChild(
- createTextInput(
- uiIDs.translateSizeInput,
- config.translateSize,
- '翻译字体大小(百分比)',
- '默认值为150(字体大小为原始的150%)',
- 'number'
- )[0]
- );
- panel.appendChild(createCheckbox('enableTranslate', '启用翻译', config.enableTranslate));
- if (checkPinYinRequire()) {
- panel.appendChild(createCheckbox('enablePinYin', '启用拼音注音', config.enablePinYin));
- }
- panel.appendChild(createCheckbox('closeConfigAfterSave', '保存后自动关闭配置页面', config.closeConfigAfterSave));
-
- const buttonContainer = document.createElement('div');
- buttonContainer.style.display = 'flex';
- buttonContainer.style.justifyContent = 'space-between';
- buttonContainer.style.marginTop = '20px';
-
- const restoreDefaultsButton = createButton('恢复默认设置', restoreDefaults);
- restoreDefaultsButton.style.marginRight = '10px';
-
- const saveButton = createButton('保存设置', saveConfig, true);
- const closeButton = createButton('关闭', () => {
- panel.style.display = 'none';
- });
-
- buttonContainer.appendChild(restoreDefaultsButton);
- buttonContainer.appendChild(saveButton);
- buttonContainer.appendChild(closeButton);
- panel.appendChild(buttonContainer);
- };
-
- const createConfigPanel = () => {
- // 获取页面的 <meta name="theme-color"> 标签
- const themeColorMeta = document.querySelector('meta[name="theme-color"]');
- let themeColor = '#ffffff'; // 默认白色背景
- let invertedColor = '#000000'; // 默认黑色字体
-
- if (themeColorMeta) {
- themeColor = themeColorMeta.getAttribute('content');
- invertedColor = getInvertColor(themeColor); // 计算相反颜色
- }
-
- // 设置样式变量
- const style = document.createElement('style');
- style.textContent = `
- :root {
- --panel-bg: ${themeColor};
- --panel-text: ${invertedColor};
- --panel-border: ${invertedColor};
- --button-bg: ${invertedColor};
- --button-text: ${themeColor};
- --button-hover-bg: ${getInvertColor(invertedColor)};
- }`;
- document.head.appendChild(style);
-
- const panel = document.createElement('div');
- panel.id = uiIDs.configPanel;
- panel.style.position = 'fixed';
- panel.style.top = '80px';
- panel.style.right = '360px';
- panel.style.padding = '20px';
- panel.style.border = `1px solid var(--panel-border)`;
- panel.style.borderRadius = '8px';
- panel.style.zIndex = '10000';
- panel.style.width = '300px';
- panel.style.maxHeight = '80%';
- panel.style.overflowY = 'auto';
- panel.style.display = 'none'; // 默认隐藏面板
- panel.style.backgroundColor = 'var(--panel-bg)';
- panel.style.color = 'var(--panel-text)';
- panel.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
-
- updateConfigPanelContent(panel);
-
- document.body.appendChild(panel);
- return panel;
- };
-
- const toggleConfigPanel = () => {
- let panel = document.getElementById(uiIDs.configPanel);
- panel = panel || createConfigPanel();
- panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
- };
-
- const createConfigButton = () => {
- const toolbar = document.querySelector('.d-editor-button-bar');
- if (!toolbar || document.getElementById(uiIDs.configButton)) return;
-
- const configButton = document.createElement('button');
- configButton.id = uiIDs.configButton;
- configButton.className = 'btn btn-flat btn-icon no-text user-menu-tab active';
- configButton.title = '配置';
- configButton.innerHTML =
- '<svg class="fa d-icon d-icon-discourse-other-tab svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#discourse-other-tab"></use></svg>';
- configButton.onclick = toggleConfigPanel;
-
- toolbar.appendChild(configButton);
- };
-
- const watchReplyControl = () => {
- const replyControl = document.getElementById(uiIDs.replyControl);
- if (!replyControl) return;
-
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
- if (replyControl.classList.contains('closed')) {
- const panel = document.getElementById(uiIDs.configPanel);
- if (panel) {
- panel.style.display = 'none';
- }
- } else {
- // 当 reply-control 重新打开时,尝试添加配置按钮
- setTimeout(createConfigButton, 500); // 给予一些时间让编辑器完全加载
- }
- }
- });
- });
-
- observer.observe(replyControl, { attributes: true });
- };
-
- const watchForEditor = () => {
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- const addedNodes = mutation.addedNodes;
- for (let node of addedNodes) {
- if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('d-editor')) {
- createConfigButton();
- return;
- }
- }
- }
- });
- });
-
- observer.observe(document.body, { childList: true, subtree: true });
- };
-
- const init = () => {
- const container = document.getElementById(uiIDs.replyControl);
- container.addEventListener('click', handleClick, true);
- document.addEventListener('keydown', handleKeydown, true);
- if (!document.getElementById(uiIDs.configButton)) {
- createConfigButton();
- }
- watchReplyControl();
- watchForEditor();
- };
-
- // 初始化
- setTimeout(() => {
- init();
- }, 1000);
- })();