您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
对回复进行翻译
当前为
// ==UserScript== // @name Linux Do Translate // @namespace linux-do-translate // @version 0.1.5 // @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 // @require https://cdn.jsdelivr.net/npm/[email protected]/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', translateModelInput: 'translate-model-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.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(`${segment}\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 translateModelInput = document.getElementById(uiIDs.translateModelInput); config.translateModel = translateModelInput.value; 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('translateModel', config.translateModel); 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('maxRetryTimes', config.maxRetryTimes); GM_setValue('deeplxUrl', config.deeplxUrl); GM_setValue('deeplxAuthKey', config.deeplxAuthKey); GM_setValue('authKeyPrefix', config.authKeyPrefix); GM_setValue('enableTranslate', config.enableTranslate); GM_setValue('translateTargetLang', config.translateTargetLang); GM_setValue('translateModel', config.translateModel); GM_setValue('translateLayout', config.translateLayout); 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, labelText, 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 = labelText; 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, 'URL', '填写自定义请求地址')[0]); panel.appendChild( createTextInput(uiIDs.deeplxAuthKeyInput, config.deeplxAuthKey, 'Auth Key', 'connect的key/deepl官key/oai的key')[0] ); panel.appendChild(createTextInput(uiIDs.translateModelInput, config.translateModel, 'Model', '填写可用模型')[0]); panel.appendChild( createTextInput( uiIDs.translateSizeInput, config.translateSize, '翻译字体大小(百分比)', '默认值为150(字体大小为原始的150%)', 'number' )[0] ); panel.appendChild( createSelect( 'translateLayout', '翻译布局(上/下)', [ { text: '上', value: 'up' }, { text: '下', value: 'down' }, ], config.translateLayout ) ); 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); })();