您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
对回复进行翻译
// ==UserScript== // @name Linux Do Translate // @namespace linux-do-translate // @version 0.2.4 // @author delph1s // @license MIT // @description 对回复进行翻译 // @match https://linux.do/t/topic/* // @connect * // @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 // @run-at document-end // ==/UserScript== (function () { 'use strict'; const REQUIRED_CHARS = 6; const SPACE_PRESS_COUNT = 3; // 连按次数 const SPACE_PRESS_TIMEOUT = 1500; // 连续按键的最大时间间隔(毫秒) const TRANSLATE_PROVIDERS = [ { text: 'LinuxDo Deeplx', value: 'deeplx-linuxdo', }, { text: 'Deeplx', value: 'deeplx', }, { text: 'Deepl', value: 'deepl', }, { text: 'OpenAI', value: 'oai', }, { text: 'OpenAI Proxy', value: 'oai-proxy', }, ]; const NOT_CUSTOM_URL_PROVIDERS = ['oai', 'deepl', 'deeplx-linuxdo']; const TRANSLATE_TARGET_LANG = { EN: { oai: 'English', deepl: 'EN' }, ZH: { oai: 'Chinese', deepl: 'ZH' }, }; const TRANSLATE_TARGET_LANG_OPTIONS = [ { text: 'English(英文)', value: 'EN', }, { text: '中文(Chinese)', value: 'ZH', }, ]; const DEFAULT_CONFIG = { maxRetryTimes: 5, customUrl: '', authKey: '', enableTranslate: false, translateSourceLang: 'ZH', translateTargetLang: 'EN', translateProvider: 'deeplx-linuxdo', translateModel: 'gpt-4o', translateLayout: 'top', translateSize: 80, translateItalics: true, translateBold: false, translateReference: false, closeConfigAfterSave: true, }; const uiIDs = { replyControl: 'reply-control', configButton: 'multi-lang-say-config-button', configPanel: 'multi-lang-say-config-panel', customUrlInput: 'custom-url-input', authKeyInput: 'auth-key-input', enableTranslateSwitch: 'enable-translate-switch', translateSourceLangSelect: 'translate-source-lang-select', translateTargetLangSelect: 'translate-target-lang-select', translateProviderSelect: 'translate-provider-select', translateModelInput: 'translate-model-input', translateLayoutSelect: 'translate-layout-select', translateSizeInput: 'translate-size-input', translateItalicsSwitch: 'translate-italics-switch', translateBoldSwitch: 'translate-bold-switch', translateReferenceSwitch: 'translate-reference-switch', closeConfigAfterSaveSwitch: 'close-after-save-switch', }; let config = { maxRetryTimes: GM_getValue('maxRetryTimes', DEFAULT_CONFIG.maxRetryTimes), customUrl: GM_getValue('customUrl', DEFAULT_CONFIG.customUrl), authKey: GM_getValue('authKey', DEFAULT_CONFIG.authKey), enableTranslate: GM_getValue('enableTranslate', DEFAULT_CONFIG.enableTranslate), translateSourceLang: GM_getValue('translateSourceLang', DEFAULT_CONFIG.translateSourceLang), translateTargetLang: GM_getValue('translateTargetLang', DEFAULT_CONFIG.translateTargetLang), translateProvider: GM_getValue('translateProvider', DEFAULT_CONFIG.translateProvider), translateModel: GM_getValue('translateModel', DEFAULT_CONFIG.translateModel), translateLayout: GM_getValue('translateLayout', DEFAULT_CONFIG.translateLayout), translateSize: GM_getValue('translateSize', DEFAULT_CONFIG.translateSize), translateItalics: GM_getValue('translateItalics', DEFAULT_CONFIG.translateItalics), translateBold: GM_getValue('translateBold', DEFAULT_CONFIG.translateBold), translateReference: GM_getValue('translateReference', DEFAULT_CONFIG.translateReference), closeConfigAfterSave: GM_getValue('closeConfigAfterSave', DEFAULT_CONFIG.closeConfigAfterSave), }; 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); }; 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.authKey ? `${config.customUrl}?token=${config.authKey}` : config.customUrl, headers: { 'Content-Type': 'application/json', }, data: JSON.stringify({ text: text, target_lang: TRANSLATE_TARGET_LANG[config.translateTargetLang].deepl, source_lang: 'auto', }), responseType: 'json', }; }; const deeplxLinuxdoReq = text => { return { url: `https://api.deeplx.org/${config.authKey}/translate`, headers: { 'Content-Type': 'application/json', }, data: JSON.stringify({ text: text, target_lang: TRANSLATE_TARGET_LANG[config.translateTargetLang].deepl, source_lang: 'auto', }), responseType: 'json', }; }; const deeplReq = text => { const authKey = config.authKey; const params = new URLSearchParams(); params.append('text', text); params.append('target_lang', TRANSLATE_TARGET_LANG[config.translateTargetLang].deepl); params.append('source_lang', TRANSLATE_TARGET_LANG[config.translateSourceLang].deepl); 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 deeplRes = res => { return res?.translations?.[0]?.text; }; const oaiReq = ( text, model = 'gpt-3.5-turbo', url = 'https://api.openai.com/v1/chat/completions', temperature = 0.5, maxTokens = 32000 ) => { const authKey = config.authKey; 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 ${ TRANSLATE_TARGET_LANG[config.translateTargetLang].oai }, Output translation directly without any additional text.\nSource Text: ${text}\nTranslated Text:`, }, ], temperature: temperature, // 控制生成内容的随机性,范围是 0 到 1 max_tokens: maxTokens, // 响应的最大标记数 }), responseType: 'json', }; }; const oaiRes = res => { return res.choices[0].message.content.trim(); }; const translateText = text => { const isDeepl = config.translateProvider === 'deepl'; const isOAI = config.translateProvider === 'oai' || config.translateProvider === 'oai-proxy'; let reqData; if (!config.authKey) { if (!config.customUrl) return ''; if (config.translateProvider === 'deeplx') { reqData = deeplxReq(text); } else { return ''; } } else if (isDeepl) { reqData = deeplReq(text); } else if (isOAI) { reqData = oaiReq( text, config.translateModel, NOT_CUSTOM_URL_PROVIDERS.includes(config.translateProvider) ? 'https://api.openai.com/v1/chat/completions' : config.customUrl, 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 (isDeepl) { // Pro API 返回格式 translation = deeplRes(response); console.log('DeepL translation:', translation); } else if (isOAI) { translation = oaiRes(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(''); }, }); }); }; 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; } // 翻译普通文本 let segmentTranslate = await translateText(segment); if (segmentTranslate === '') { return segmentTranslate; } if (config.translateItalics) { segmentTranslate = `[i]${segmentTranslate}[/i]`; } if (config.translateBold) { segmentTranslate = `[b]${segmentTranslate}[/b]`; } if (config.translateReference) { segmentTranslate = `> [size=${config.translateSize}]${segmentTranslate}[/size]`; } else { segmentTranslate = `[size=${config.translateSize}]${segmentTranslate}[/size]`; } if (config.translateLayout === 'bottom') { translatedSegments.push(`${segment}\n${config.translateReference ? "\n" : ""}${segmentTranslate}`); } else if (config.translateLayout === 'top') { translatedSegments.push( `${segmentTranslate}\n${config.translateReference ? "\n" : ""}${segment}` ); } } // 合并结果 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; } 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 customUrlInput = document.getElementById(uiIDs.customUrlInput); config.customUrl = customUrlInput.value.trim(); const authKeyInput = document.getElementById(uiIDs.authKeyInput); config.authKey = authKeyInput.value.trim(); const translateModelInput = document.getElementById(uiIDs.translateModelInput); config.translateModel = translateModelInput.value; const transalteSizeInput = document.getElementById(uiIDs.translateSizeInput); config.translateSize = transalteSizeInput.value; console.log(config); GM_setValue('customUrl', config.customUrl); GM_setValue('authKey', config.authKey); GM_setValue('enableTranslate', config.enableTranslate); GM_setValue('translateModel', config.translateModel); GM_setValue('translateSize', config.translateSize); GM_setValue('translateItalics', config.translateItalics); GM_setValue('translateBold', config.translateBold); GM_setValue('translateReference', config.translateReference); GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave); if (config.closeConfigAfterSave) { const panel = document.getElementById(uiIDs.configPanel); toggleConfigPanelAnimation(panel); } }; const restoreDefaults = () => { if (confirm('确定要将所有设置恢复为默认值吗?')) { config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); GM_setValue('maxRetryTimes', config.maxRetryTimes); GM_setValue('customUrl', config.customUrl); GM_setValue('authKey', config.authKey); GM_setValue('enableTranslate', config.enableTranslate); GM_setValue('translateSourceLang', config.translateSourceLang); GM_setValue('translateTargetLang', config.translateTargetLang); GM_setValue('translateModel', config.translateModel); GM_setValue('translateLayout', config.translateLayout); GM_setValue('translateSize', config.translateSize); GM_setValue('translateItalics', config.translateItalics); GM_setValue('translateBold', config.translateBold); GM_setValue('translateReference', config.translateReference); GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave); const panel = document.getElementById(uiIDs.configPanel); if (panel) { updateConfigPanelContent(panel); } } }; const createFormGroup = (labelText, element) => { const group = document.createElement('div'); group.className = 'form-group'; const label = document.createElement('label'); label.className = 'form-label'; label.textContent = labelText; group.appendChild(label); group.appendChild(element); return group; }; const createSelect = (eleId, configId, options, defaultValue, onChange = undefined) => { const select = document.createElement('select'); select.className = 'modern-select'; select.id = eleId; options.forEach(option => { const optionElement = document.createElement('option'); optionElement.value = option.value; optionElement.textContent = option.text; select.appendChild(optionElement); }); select.value = defaultValue; if (onChange !== undefined) { select.addEventListener('change', e => onChange(e)); } else { select.addEventListener('change', e => { config[configId] = e.target.value; console.log(`[存储配置] ${configId}: ${config[configId]}`); GM_setValue(configId, config[configId]); }); } return select; }; const createInput = (eleId, value, type = 'text', placeholder = '') => { const input = document.createElement('input'); input.className = 'modern-input'; input.id = eleId; input.type = type; input.value = value; input.placeholder = placeholder; return input; }; const createSwitch = (eleId, configId, checked, labelText) => { const container = document.createElement('div'); container.className = 'switch-container'; const label = document.createElement('span'); label.className = 'form-label'; label.style.margin = '0'; label.textContent = labelText; const switchEl = document.createElement('div'); switchEl.id = eleId; switchEl.className = `modern-switch${checked ? ' active' : ''}`; switchEl.addEventListener('click', () => { switchEl.classList.toggle('active'); config[configId] = switchEl.classList.contains('active'); console.log(`[存储配置] ${configId}: ${config[configId]}`); GM_setValue(configId, config[configId]); }); container.appendChild(label); container.appendChild(switchEl); return container; }; const createButton = (text, onClick, variant = 'secondary') => { const button = document.createElement('button'); button.className = `modern-button ${variant}`; button.textContent = text; button.addEventListener('click', onClick); return button; }; // 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 updateConfigPanelContent = (panel, panelContent) => { panelContent.innerHTML = ''; // 添加表单元素 const translateProviderSelect = createSelect( uiIDs.translateProviderSelect, 'translateProvider', TRANSLATE_PROVIDERS, config.translateProvider, e => { config.translateProvider = e.target.value; const notCustomUrl = NOT_CUSTOM_URL_PROVIDERS.includes(config.translateProvider); const urlInput = document.getElementById(uiIDs.customUrlInput); if (notCustomUrl) { if (urlInput) { urlInput.disabled = true; } } else { if (urlInput) { urlInput.disabled = false; } } console.log(`[存储配置] translateProvider: ${config.translateProvider}`); GM_setValue('translateProvider', config.translateProvider); } ); panelContent.appendChild(createFormGroup('翻译服务商(Provider)', translateProviderSelect)); const customUrlInput = createInput(uiIDs.customUrlInput, config.customUrl, 'text', '填写自定义请求地址'); const notCustomUrl = NOT_CUSTOM_URL_PROVIDERS.includes(config.translateProvider); if (notCustomUrl) { customUrlInput.disabled = true; } panelContent.appendChild(createFormGroup('自定义链接(Custom URL)', customUrlInput)); const authKeyInput = createInput(uiIDs.authKeyInput, config.authKey, 'password', '输入认证密钥'); panelContent.appendChild(createFormGroup('认证密钥(Auth Key)', authKeyInput)); const modelInput = createInput(uiIDs.translateModelInput, config.translateModel, 'text', '输入翻译模型'); panelContent.appendChild(createFormGroup('翻译模型(AI Model)', modelInput)); const targetSourceSelect = createSelect( uiIDs.translateSourceLangSelect, 'translateSourceLang', TRANSLATE_TARGET_LANG_OPTIONS, config.translateSourceLang ); const targetLangSelect = createSelect( uiIDs.translateTargetLangSelect, 'translateTargetLang', TRANSLATE_TARGET_LANG_OPTIONS, config.translateTargetLang ); panelContent.appendChild(createFormGroup('源语言(Source Language)', targetSourceSelect)); panelContent.appendChild(createFormGroup('目标语言(Target Language)', targetLangSelect)); const sizeInput = createInput( uiIDs.translateSizeInput, config.translateSize, 'number', '默认值为150(字体大小为原始的150%)' ); panelContent.appendChild(createFormGroup('翻译字体大小(百分比)', sizeInput)); const layoutSelect = createSelect( uiIDs.translateLayoutSelect, 'translateLayout', [ { text: '翻译在上(Translation On Top)', value: 'top' }, { text: '翻译在下(Translation On Bottom)', value: 'bottom' }, ], config.translateLayout ); panelContent.appendChild(createFormGroup('翻译布局(Layout)', layoutSelect)); TRANSLATE_TARGET_LANG_OPTIONS; // 添加开关 panelContent.appendChild( createSwitch(uiIDs.enableTranslateSwitch, 'enableTranslate', config.enableTranslate, '启用翻译(Enable Translate)') ); panelContent.appendChild( createSwitch(uiIDs.translateItalicsSwitch, 'translateItalics', config.translateItalics, '启用斜体(Enable Italic)') ); panelContent.appendChild( createSwitch(uiIDs.translateBoldSwitch, 'translateBold', config.translateBold, '启用粗体(Enable Bold)') ); panelContent.appendChild( createSwitch(uiIDs.translateReferenceSwitch, 'translateReference', config.translateReference, '转为引用(Convert to Quote)') ); panelContent.appendChild( createSwitch( uiIDs.closeConfigAfterSaveSwitch, 'closeConfigAfterSave', config.closeConfigAfterSave, '保存后自动关闭(Close Panel After Save)' ) ); // 创建按钮组 const buttonGroup = document.createElement('div'); buttonGroup.className = 'button-group'; buttonGroup.appendChild(createButton('恢复默认', restoreDefaults)); buttonGroup.appendChild(createButton('保存设置', saveConfig, 'primary')); buttonGroup.appendChild( createButton( '翻译(Translate)', processTextArea, // Call translate function directly 'primary' ) ); buttonGroup.appendChild( createButton( '关闭', () => { toggleConfigPanelAnimation(panel); }, 'ghost' ) ); panelContent.appendChild(buttonGroup); }; const createConfigPanel = () => { // 获取页面的 <meta name="theme-color"> 标签 const themeColorMeta = document.querySelector('meta[name="theme-color"]'); let themeColor = '#DDDDDD'; // 默认白色背景 let invertedColor = '#222222'; // 默认黑色字体 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)}; --button-hover-text: ${getInvertColor(themeColor)}; } .modern-panel { position: fixed; top: 80px; right: 20px; width: 360px; background: color-mix(in srgb, var(--panel-bg) 85%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; border: 1px solid color-mix(in srgb, var(--panel-border) 30%, transparent); overflow: hidden; opacity: 0; transform: translateY(-10px); transition: all 0.3s ease; color: var(--panel-text); } .modern-panel.show { opacity: 1; transform: translateY(0); } .modern-panel-header { padding: 20px 24px; border-bottom: 1px solid color-mix(in srgb, var(--panel-border) 10%, transparent); display: flex; justify-content: space-between; align-items: center; } .modern-panel-title { font-size: 18px; font-weight: 600; color: var(--panel-text); margin: 0; } .modern-panel-content { padding: 24px; max-height: calc(80vh - 140px); overflow-y: auto; } .modern-panel-content::-webkit-scrollbar { width: 6px; } .modern-panel-content::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--panel-border) 20%, transparent); border-radius: 3px; } .form-group { margin-bottom: 20px; } .form-label { display: block; font-size: 14px; font-weight: 500; color: var(--panel-text); margin-bottom: 8px; } .modern-select { width: 100%; padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--panel-border) 20%, transparent); border-radius: 8px; background: color-mix(in srgb, var(--panel-bg) 90%, transparent); color: var(--panel-text); font-size: 14px; transition: all 0.2s ease; appearance: none; background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; cursor: pointer; } .modern-select:hover { border-color: color-mix(in srgb, var(--panel-border) 40%, transparent); } .modern-select:focus { outline: none; border-color: var(--button-bg); box-shadow: 0 0 0 3px color-mix(in srgb, var(--button-bg) 10%, transparent); } .modern-input { width: 100%; padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--panel-border) 20%, transparent); border-radius: 8px; background: color-mix(in srgb, var(--panel-bg) 90%, transparent); color: var(--panel-text); font-size: 14px; transition: all 0.2s ease; } .modern-input:hover { border-color: color-mix(in srgb, var(--panel-border) 40%, transparent); } .modern-input:focus { outline: none; border-color: var(--button-bg); box-shadow: 0 0 0 3px color-mix(in srgb, var(--button-bg) 10%, transparent); } .switch-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .modern-switch { position: relative; width: 44px; height: 24px; background: color-mix(in srgb, var(--panel-border) 20%, transparent); border-radius: 12px; padding: 2px; transition: background 0.3s ease; cursor: pointer; } .modern-switch.active { background: var(--button-bg); } .modern-switch::after { content: ''; position: absolute; width: 20px; height: 20px; border-radius: 10px; background: var(--panel-bg); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease; transform: translateY(1px) translateX(2px); } .modern-switch.active::after { transform: translateY(1px) translateX(22px); } .button-group { display: flex; gap: 12px; margin-top: 24px; padding-top: 20px; border-top: 1px solid color-mix(in srgb, var(--panel-border) 10%, transparent); } .modern-button { flex: 1; padding: 2px 8px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; border: none; display: flex; align-items: center; justify-content: center; gap: 6px; } .modern-button.primary { background: var(--button-bg); color: var(--button-text); } .modern-button.primary:hover { background: var(--button-hover-bg); color: var(--button-hover-text); } .modern-button.secondary { background: color-mix(in srgb, var(--panel-border) 10%, transparent); color: var(--panel-text); } .modern-button.secondary:hover { background: color-mix(in srgb, var(--panel-border) 20%, transparent); } .modern-button.ghost { background: transparent; color: var(--panel-text); } .modern-button.ghost:hover { background: color-mix(in srgb, var(--panel-border) 10%, transparent); } }`; document.head.appendChild(style); const panel = document.createElement('div'); panel.id = uiIDs.configPanel; panel.className = 'modern-panel'; panel.style.display = 'none'; // 创建头部 const header = document.createElement('div'); header.className = 'modern-panel-header'; const title = document.createElement('h3'); title.className = 'modern-panel-title'; title.textContent = '设置'; header.appendChild(title); // 创建内容区域 const content = document.createElement('div'); content.className = 'modern-panel-content'; console.log(); updateConfigPanelContent(panel, content); panel.appendChild(header); panel.appendChild(content); document.body.appendChild(panel); return panel; }; const toggleConfigPanelAnimation = panel => { if (panel.style.display === 'none') { panel.classList.add('show'); setTimeout(() => { panel.style.display = 'block'; }, 10); } else { panel.classList.remove('show'); setTimeout(() => { panel.style.display = 'none'; }, 300); } }; const toggleConfigPanel = () => { let panel = document.getElementById(uiIDs.configPanel); panel = panel || createConfigPanel(); toggleConfigPanelAnimation(panel); }; 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); })();