// ==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/[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',
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);
})();