Linux Do Translate

对回复进行翻译

目前為 2024-11-01 提交的版本,檢視 最新版本

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