Linux Do Translate

对回复进行翻译

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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