Linux Do Translate

对回复进行翻译

当前为 2024-10-31 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Linux Do Translate
  3. // @namespace linux-do-translate
  4. // @version 0.1.2
  5. // @author delph1s
  6. // @license MIT
  7. // @description 对回复进行翻译
  8. // @match https://linux.do/t/topic/*
  9. // @connect api.deeplx.org
  10. // @connect api.deepl.com
  11. // @icon https://cdn.linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png
  12. // @grant unsafeWindow
  13. // @grant window.close
  14. // @grant window.focus
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_xmlhttpRequest
  18. // @require https://cdn.jsdelivr.net/npm/pinyin-pro@3.25.0/dist/index.min.js
  19. // @run-at document-end
  20. // ==/UserScript==
  21.  
  22. (function () {
  23. 'use strict';
  24. const REQUIRED_CHARS = 6;
  25. const SPACE_PRESS_COUNT = 3; // 连按次数
  26. const SPACE_PRESS_TIMEOUT = 1500; // 连续按键的最大时间间隔(毫秒)
  27. const DEFAULT_CONFIG = {
  28. maxRetryTimes: 5,
  29. deeplxUrl: '',
  30. deeplxAuthKey: '',
  31. authKeyPrefix: {
  32. deeplx: '',
  33. deeplOfficial: 'deepl-auth-key:',
  34. openAIOfficial: 'oai-at:',
  35. openAIProxy: 'oai-at-proxy:',
  36. },
  37. enableTranslate: false,
  38. translateTargetLang: {
  39. deeplx: 'EN',
  40. deeplOfficial: 'EN',
  41. openAIOfficial: 'English',
  42. openAIProxy: 'English',
  43. },
  44. translateModel: 'gpt-3.5-turbo',
  45. translateSize: 150,
  46. enablePinYin: false,
  47. closeConfigAfterSave: true,
  48. };
  49.  
  50. const uiIDs = {
  51. replyControl: 'reply-control',
  52. configButton: 'multi-lang-say-config-button',
  53. configPanel: 'multi-lang-say-config-panel',
  54. deeplxUrlInput: 'deeplx-url-input',
  55. deeplxAuthKeyInput: 'deeplx-authkey-input',
  56. translateSizeInput: 'translate-size-input',
  57. };
  58.  
  59. let config = {
  60. maxRetryTimes: GM_getValue('maxRetryTimes', DEFAULT_CONFIG.maxRetryTimes),
  61. deeplxUrl: GM_getValue('deeplxUrl', DEFAULT_CONFIG.deeplxUrl),
  62. deeplxAuthKey: GM_getValue('deeplxAuthKey', DEFAULT_CONFIG.deeplxAuthKey),
  63. authKeyPrefix: GM_getValue('authKeyPrefix', DEFAULT_CONFIG.authKeyPrefix),
  64. enableTranslate: GM_getValue('enableTranslate', DEFAULT_CONFIG.enableTranslate),
  65. translateTargetLang: GM_getValue('translateTargetLang', DEFAULT_CONFIG.translateTargetLang),
  66. translateModel: GM_getValue('translateModel', DEFAULT_CONFIG.translateModel),
  67. translateSize: GM_getValue('translateSize', DEFAULT_CONFIG.translateSize),
  68. enablePinYin: GM_getValue('enablePinYin', DEFAULT_CONFIG.enablePinYin),
  69. closeConfigAfterSave: GM_getValue('closeConfigAfterSave', DEFAULT_CONFIG.closeConfigAfterSave),
  70. };
  71.  
  72. const checkPinYinRequire = () => {
  73. // 检查是否加载了 pinyin 库
  74. if (typeof pinyinPro === 'undefined') {
  75. console.error('pinyin 库未正确加载!');
  76. config.enablePinYin = false;
  77. GM_setValue('enablePinYin', false);
  78. return false;
  79. }
  80. return true;
  81. };
  82.  
  83. const genFormatDateTime = d => {
  84. return d.toLocaleString('zh-CN', {
  85. year: 'numeric',
  86. month: '2-digit',
  87. day: '2-digit',
  88. hour: '2-digit',
  89. minute: '2-digit',
  90. second: '2-digit',
  91. hour12: false,
  92. });
  93. };
  94.  
  95. const genFormatNow = () => {
  96. return genFormatDateTime(new Date());
  97. };
  98.  
  99. /**
  100. * 获取随机整数
  101. *
  102. * @param {number} start 范围开始
  103. * @param {number} end 范围结束
  104. * @returns
  105. */
  106. const randInt = (start, end) => {
  107. return Math.floor(Math.random() * (end - start + 1)) + start;
  108. };
  109.  
  110. /**
  111. * 随机睡眠(毫秒)
  112. *
  113. * @param {number} start 范围开始
  114. * @param {number} end 范围结束
  115. */
  116. const randSleep = async (start = 2000, end = 3000) => {
  117. // 生成随机整数 randSleepTime,范围在 start 到 end 之间
  118. const randSleepTime = getRandomInt(start, end);
  119. // 睡眠时间
  120. return await new Promise(resolve => setTimeout(resolve, randSleepTime));
  121. };
  122.  
  123. /**
  124. * 是否相同
  125. *
  126. * @param a
  127. * @param b
  128. * @returns
  129. */
  130. const isEqual = (a, b) => {
  131. if (a === null || a === undefined || b === null || b === undefined) {
  132. return a === b;
  133. }
  134.  
  135. if (typeof a !== typeof b) {
  136. return false;
  137. }
  138.  
  139. if (typeof a === 'string' || typeof a === 'number' || typeof a === 'boolean') {
  140. return a === b;
  141. }
  142.  
  143. if (Array.isArray(a) && Array.isArray(b)) {
  144. if (a.length !== b.length) {
  145. return false;
  146. }
  147.  
  148. return a.every((item, index) => isEqual(item, b[index]));
  149. }
  150.  
  151. if (typeof a === 'object' && typeof b === 'object') {
  152. const keysA = Object.keys(a || {});
  153. const keysB = Object.keys(b || {});
  154.  
  155. if (keysA.length !== keysB.length) {
  156. return false;
  157. }
  158.  
  159. return keysA.every(key => isEqual(a[key], b[key]));
  160. }
  161.  
  162. return false;
  163. };
  164.  
  165. /**
  166. * 判断字符串中是否包含中文字符
  167. * @param {string} text
  168. * @returns {boolean}
  169. */
  170. const containsChinese = text => {
  171. return /[\u4e00-\u9fa5]/.test(text);
  172. };
  173.  
  174. /**
  175. * 将中文字符串转换为带拼音的ruby标签字符串
  176. * @param {string} text
  177. * @returns {string}
  178. */
  179. const convertToRuby = text => {
  180. // 使用tiny-pinyin将中文转换为拼音,拼音之间用空格分隔
  181. const pinyin = pinyinPro.pinyin(text);
  182. // 构建ruby标签
  183. return `<ruby><rb>${text}</rb><rt>${pinyin}</rt></ruby>`;
  184. };
  185.  
  186. const getInvertColor = hex => {
  187. // 去掉前面的“#”字符
  188. hex = hex.replace('#', '');
  189.  
  190. // 如果输入的是3位的hex值,转换为6位的
  191. if (hex.length === 3) {
  192. hex = hex
  193. .split('')
  194. .map(c => c + c)
  195. .join('');
  196. }
  197.  
  198. // 计算相反的颜色
  199. const r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16).padStart(2, '0');
  200. const g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16).padStart(2, '0');
  201. const b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16).padStart(2, '0');
  202.  
  203. return `#${r}${g}${b}`;
  204. };
  205.  
  206. const deeplxReq = text => {
  207. return {
  208. url: config.deeplxUrl,
  209. headers: {
  210. 'Content-Type': 'application/json',
  211. },
  212. data: JSON.stringify({
  213. text: text,
  214. target_lang: 'EN',
  215. source_lang: 'auto',
  216. }),
  217. responseType: 'json',
  218. };
  219. };
  220.  
  221. const deeplxLinuxdoReq = text => {
  222. return {
  223. url: `https://api.deeplx.org/${config.deeplxAuthKey}/translate`,
  224. headers: {
  225. 'Content-Type': 'application/json',
  226. },
  227. data: JSON.stringify({
  228. text: text,
  229. target_lang: 'EN',
  230. source_lang: 'auto',
  231. }),
  232. responseType: 'json',
  233. };
  234. };
  235.  
  236. const deeplOfficialReq = text => {
  237. const authKey = config.deeplxAuthKey.replace(config.authKeyPrefix.deeplOfficial, '');
  238. const params = new URLSearchParams();
  239. params.append('text', text);
  240. params.append('target_lang', 'EN');
  241. params.append('source_lang', 'ZH');
  242. return {
  243. url: 'https://api.deepl.com/v2/translate', // DeepL Pro API
  244. headers: {
  245. Authorization: `DeepL-Auth-Key ${authKey}`,
  246. 'Content-Type': 'application/x-www-form-urlencoded',
  247. },
  248. data: params.toString(),
  249. responseType: 'json',
  250. };
  251. };
  252.  
  253. const deeplOfficialRes = res => {
  254. return res?.translations?.[0]?.text;
  255. };
  256.  
  257. const oaiOffcialReq = (
  258. text,
  259. model = 'gpt-3.5-turbo',
  260. url = 'https://api.openai.com/v1/chat/completions',
  261. temperature = 0.5,
  262. maxTokens = 32000
  263. ) => {
  264. const authKey = config.deeplxAuthKey.replace(config.authKeyPrefix.openAIOfficial, '');
  265. return {
  266. url: url,
  267. headers: {
  268. 'Content-Type': 'application/json',
  269. Authorization: `Bearer ${authKey}`,
  270. },
  271. data: JSON.stringify({
  272. model: model, // 或者您订阅的其他模型,例如 'gpt-4'
  273. messages: [
  274. {
  275. role: 'system',
  276. content:
  277. '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.',
  278. },
  279. {
  280. role: 'user',
  281. content: `Translate the following source text to ${config.translateTargetLang.openAIOfficial}, Output translation directly without any additional text.\nSource Text: ${text}\nTranslated Text:`,
  282. },
  283. ],
  284. temperature: temperature, // 控制生成内容的随机性,范围是 0 到 1
  285. max_tokens: maxTokens, // 响应的最大标记数
  286. }),
  287. responseType: 'json',
  288. };
  289. };
  290.  
  291. const oaiOfficalRes = res => {
  292. return res.data.choices[0].message.content.trim();
  293. };
  294.  
  295. const translateText = text => {
  296. const isDeeplOfficial = config.deeplxAuthKey.startsWith(config.authKeyPrefix.deeplOfficial);
  297. const isOpenAIOfficial = config.deeplxAuthKey.startsWith(config.authKeyPrefix.openAIOfficial);
  298.  
  299. let reqData;
  300.  
  301. if (!config.deeplxAuthKey) {
  302. if (!config.deeplxUrl) return '';
  303. reqData = deeplxReq(text);
  304. } else if (isDeeplOfficial) {
  305. reqData = deeplOfficialReq(text);
  306. } else if (isOpenAIOfficial) {
  307. reqData = oaiOffcialReq(
  308. text,
  309. config.translateModel,
  310. config.deeplxUrl || 'https://api.openai.com/v1/chat/completions',
  311. 0.5,
  312. 1600
  313. );
  314. } else {
  315. reqData = deeplxLinuxdoReq(text);
  316. }
  317.  
  318. return new Promise((resolve, reject) => {
  319. GM_xmlhttpRequest({
  320. method: 'POST',
  321. url: reqData.url,
  322. headers: reqData.headers,
  323. data: reqData.data,
  324. responseType: reqData.responseType,
  325. onload: function (res) {
  326. console.log('Translation response:', res);
  327. console.log('Request details:', reqData);
  328.  
  329. if (res.status === 200) {
  330. try {
  331. const response = typeof res.response === 'string' ? JSON.parse(res.response) : res.response;
  332. console.log('Parsed response:', response);
  333.  
  334. let translation;
  335.  
  336. if (isDeeplOfficial) {
  337. // Pro API 返回格式
  338. translation = deeplOfficialRes(response);
  339. console.log('DeepL translation:', translation);
  340. } else if (isOpenAIOfficial) {
  341. translation = oaiOfficalRes(response);
  342. console.log('OAI translation:', translation);
  343. } else {
  344. translation = response?.data;
  345. console.log('DeepLX translation:', translation);
  346. }
  347.  
  348. resolve(translation || '');
  349. } catch (error) {
  350. console.error('Error parsing response:', error);
  351. resolve('');
  352. }
  353. } else {
  354. console.error('Translation failed:', {
  355. status: res.status,
  356. statusText: res.statusText,
  357. response: res.response,
  358. responseText: res.responseText,
  359. finalUrl: res.finalUrl,
  360. headers: res.responseHeaders,
  361. });
  362. resolve('');
  363. }
  364. },
  365. onerror: function (err) {
  366. console.error('Translation error details:', {
  367. error: err,
  368. errorText: err.toString(),
  369. status: err.status,
  370. statusText: err.statusText,
  371. responseText: err.responseText,
  372. });
  373. resolve('');
  374. },
  375. });
  376. });
  377.  
  378. // return GM_xmlhttpRequest({
  379. // method: 'POST',
  380. // url,
  381. // data: JSON.stringify(data),
  382. // responseType: 'json',
  383. // onload: res => {
  384. // console.log(res);
  385. // return res.response;
  386. // },
  387. // onerror: err => {
  388. // console.log(err);
  389. // return '';
  390. // },
  391. // });
  392.  
  393. // return GM.xmlHttpRequest({
  394. // method: 'POST',
  395. // url,
  396. // data: JSON.stringify(data),
  397. // responseType: 'json',
  398. // })
  399. // .then(res => {
  400. // console.log(res);
  401. // console.log(`[翻译结果] ${res.response.data}`)
  402. // return res.response.data;
  403. // })
  404. // .catch(err => {
  405. // console.log(err);
  406. // return '';
  407. // });
  408. };
  409.  
  410. const processTranslateText = async text => {
  411. // 检查是否已存在拼音
  412. const rubyRegex = /(<ruby>[\s\S]*?<\/ruby>)/g;
  413. // 使用正则表达式将文本分割为已被 <ruby> 包裹的部分和未被包裹的部分
  414. const parts = text.split(rubyRegex);
  415.  
  416. // 处理每一部分
  417. let newText = [];
  418. for (let i = 0; i < parts.length; i += 1) {
  419. if (rubyRegex.test(parts[i])) {
  420. // 已被 <ruby> 包裹,保持原样
  421. return parts[i];
  422. } else {
  423. if (parts[i].indexOf('\n') > -1) {
  424. let newLine = parts[i].split('\n');
  425. for (let j = 0; j < newLine.length; j += 1) {
  426. if (!newLine[j].trim()) {
  427. newLine[j] = '\n';
  428. } else {
  429. const segmentTranslate = await translateText(newLine[j]);
  430. newLine[
  431. j
  432. ] = `[size=${config.translateSize}]<ruby><rb>${newLine[j]}</rb><rt>${segmentTranslate}</rt></ruby>[/size]`;
  433. }
  434. }
  435. newText.push(newLine.join('\n'));
  436. } else {
  437. const segmentTranslate = await translateText(parts[i]);
  438. // 构建ruby标签
  439. newText.push(
  440. `[size=${config.translateSize}]<ruby><rb>${parts[i]}</rb><rt>${segmentTranslate}</rt></ruby>[/size]`
  441. );
  442. }
  443. }
  444. }
  445. return newText.join('');
  446. };
  447.  
  448. const processTextArea = () => {
  449. let textarea = document.querySelector(`#${uiIDs.replyControl} textarea`);
  450. let text = textarea.value.trim();
  451. let originalLength = text.length;
  452.  
  453. if (text.length !== 0 && originalLength >= REQUIRED_CHARS) {
  454. // 检查是否已存在拼音
  455. const rubyRegex = /(<ruby>[\s\S]*?<\/ruby>)/g;
  456.  
  457. // 为中文加入翻译
  458. if (config.enableTranslate) {
  459. textarea.value = '开始翻译...';
  460. // // 创建并触发 input 事件
  461. // const inputEvent = new Event('input', {
  462. // bubbles: true,
  463. // cancelable: true,
  464. // });
  465. // // 触发事件
  466. // textarea.dispatchEvent(inputEvent);
  467.  
  468. processTranslateText(text).then(res => {
  469. textarea.value = res;
  470.  
  471. // 创建并触发 input 事件
  472. const inputEvent = new Event('input', {
  473. bubbles: true,
  474. cancelable: true,
  475. });
  476. // 触发事件
  477. textarea.dispatchEvent(inputEvent);
  478. });
  479. return;
  480. }
  481.  
  482. // 为中文加入拼音
  483. if (!config.enableTranslate && config.enablePinYin) {
  484. if (containsChinese(text)) {
  485. // 使用正则表达式将文本分割为已被 <ruby> 包裹的部分和未被包裹的部分
  486. const parts = text.split(rubyRegex);
  487.  
  488. // 处理每一部分
  489. text = parts
  490. .map(part => {
  491. if (rubyRegex.test(part)) {
  492. // 已被 <ruby> 包裹,保持原样
  493. return part;
  494. } else {
  495. // 未被包裹,进一步分割为中文和非中文
  496. if (containsChinese(part)) {
  497. const segments = part.split(/([\u4e00-\u9fa5]+)/g);
  498. return segments
  499. .map(segment => {
  500. if (containsChinese(segment)) {
  501. return convertToRuby(segment);
  502. } else {
  503. return segment;
  504. }
  505. })
  506. .join('');
  507. } else {
  508. return part;
  509. }
  510. }
  511. })
  512. .join('');
  513. }
  514. }
  515.  
  516. textarea.value = text;
  517.  
  518. // 创建并触发 input 事件
  519. const inputEvent = new Event('input', {
  520. bubbles: true,
  521. cancelable: true,
  522. });
  523. // 触发事件
  524. textarea.dispatchEvent(inputEvent);
  525. }
  526. };
  527.  
  528. const handleClick = event => {
  529. // 修复翻译两次的 BUG
  530. if (config.enableTranslate) {
  531. return;
  532. }
  533.  
  534. if (event.target && event.target.closest('button.create')) {
  535. processTextArea();
  536. }
  537. };
  538.  
  539. let spacePresses = 0;
  540. let lastKeyTime = 0;
  541. let timeoutHandle = null;
  542.  
  543. const handleKeydown = event => {
  544. // console.log(`KeyboardEvent: key='${event.key}' | code='${event.code}'`);
  545.  
  546. if (event.ctrlKey && event.key === 'Enter') {
  547. processTextArea();
  548. return;
  549. }
  550.  
  551. // 使用 Alt+D 触发翻译
  552. if (event.altKey && event.key.toLowerCase() === 'd') {
  553. event.preventDefault(); // 阻止默认行为
  554. processTextArea();
  555. return;
  556. }
  557.  
  558. const currentTime = Date.now();
  559. if (event.code === 'Space') {
  560. // 如果时间间隔太长,重置计数
  561. if (currentTime - lastKeyTime > SPACE_PRESS_TIMEOUT) {
  562. spacePresses = 1;
  563. } else {
  564. spacePresses += 1;
  565. }
  566.  
  567. lastKeyTime = currentTime;
  568.  
  569. // 清除之前的定时器
  570. if (timeoutHandle) {
  571. clearTimeout(timeoutHandle);
  572. }
  573.  
  574. // 设置新的定时器,如果在 SPACE_PRESS_TIMEOUT 毫秒内没有新的按键,则重置计数
  575. timeoutHandle = setTimeout(() => {
  576. spacePresses = 0;
  577. }, SPACE_PRESS_TIMEOUT);
  578.  
  579. // 检查是否达到了按键次数
  580. if (spacePresses === SPACE_PRESS_COUNT) {
  581. spacePresses = 0; // 重置计数
  582.  
  583. // 执行翻译操作
  584. processTextArea();
  585. }
  586. } else {
  587. // 如果按下了其他键,重置计数
  588. spacePresses = 0;
  589. if (timeoutHandle) {
  590. clearTimeout(timeoutHandle);
  591. timeoutHandle = null;
  592. }
  593. }
  594. };
  595.  
  596. const saveConfig = () => {
  597. const deeplxUrlInput = document.getElementById(uiIDs.deeplxUrlInput);
  598. config.deeplxUrl = deeplxUrlInput.value.trim();
  599. const deeplxAuthKeyInput = document.getElementById(uiIDs.deeplxAuthKeyInput);
  600. config.deeplxAuthKey = deeplxAuthKeyInput.value.trim();
  601. const transalteSizeInput = document.getElementById(uiIDs.translateSizeInput);
  602. config.translateSize = transalteSizeInput.value;
  603. console.log(config);
  604.  
  605. GM_setValue('deeplxUrl', config.deeplxUrl);
  606. GM_setValue('deeplxAuthKey', config.deeplxAuthKey);
  607. GM_setValue('enableTranslate', config.enableTranslate);
  608. GM_setValue('translateSize', config.translateSize);
  609. GM_setValue('enablePinYin', config.enablePinYin);
  610. GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);
  611.  
  612. if (config.closeConfigAfterSave) {
  613. document.getElementById(uiIDs.configPanel).style.display = 'none';
  614. }
  615. };
  616.  
  617. const restoreDefaults = () => {
  618. if (confirm('确定要将所有设置恢复为默认值吗?')) {
  619. config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
  620. GM_setValue('deeplxUrl', config.deeplxUrl);
  621. GM_setValue('deeplxAuthKey', config.deeplxAuthKey);
  622. GM_setValue('enableTranslate', config.enableTranslate);
  623. GM_setValue('translateSize', config.translateSize);
  624. GM_setValue('enablePinYin', config.enablePinYin);
  625. GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);
  626.  
  627. const panel = document.getElementById(uiIDs.configPanel);
  628. if (panel) {
  629. updateConfigPanelContent(panel);
  630. }
  631. }
  632. };
  633.  
  634. const createCheckbox = (id, text, checked, onChange = undefined) => {
  635. const label = document.createElement('label');
  636. label.style.display = 'flex';
  637. label.style.alignItems = 'center';
  638. label.style.marginBottom = '10px';
  639. label.style.cursor = 'pointer';
  640.  
  641. const checkbox = document.createElement('input');
  642. checkbox.type = 'checkbox';
  643. checkbox.id = id;
  644. checkbox.checked = checked;
  645. checkbox.style.marginRight = '10px';
  646. if (onChange !== undefined) {
  647. checkbox.addEventListener('change', e => onChange(e));
  648. } else {
  649. checkbox.addEventListener('change', e => {
  650. config[id] = e.target.checked;
  651. GM_setValue(id, config[id]);
  652. });
  653. }
  654.  
  655. label.appendChild(checkbox);
  656. label.appendChild(document.createTextNode(text));
  657.  
  658. return label;
  659. };
  660.  
  661. const createSelect = (id, text, options, defaultValue) => {
  662. const container = document.createElement('div');
  663. container.style.display = 'flex';
  664. container.style.alignItems = 'center';
  665. container.style.marginBottom = '10px';
  666.  
  667. const label = document.createElement('label');
  668. label.htmlFor = id;
  669. label.textContent = text;
  670. label.style.marginRight = '10px';
  671.  
  672. const select = document.createElement('select');
  673. select.id = id;
  674. select.style.flex = '1';
  675.  
  676. options.forEach(option => {
  677. const optionElement = document.createElement('option');
  678. optionElement.value = option.value;
  679. optionElement.textContent = option.text;
  680. select.appendChild(optionElement);
  681. });
  682.  
  683. select.value = defaultValue;
  684.  
  685. select.addEventListener('change', e => {
  686. config[id] = e.target.value;
  687. GM_setValue(id, config[id]);
  688. });
  689.  
  690. container.appendChild(select);
  691. container.appendChild(label);
  692.  
  693. return container;
  694. };
  695.  
  696. const createTextInput = (id, value, labelText, placeholder, type = 'text') => {
  697. const container = document.createElement('div');
  698. container.style.marginBottom = '15px';
  699.  
  700. const label = document.createElement('label');
  701. label.textContent = labelText;
  702. label.style.display = 'block';
  703. label.style.marginBottom = '5px';
  704. container.appendChild(label);
  705.  
  706. const inputPlace = document.createElement('input');
  707. inputPlace.id = id;
  708. inputPlace.type = type;
  709. inputPlace.value = value;
  710. inputPlace.placeholder = placeholder;
  711. inputPlace.style.width = '100%';
  712. inputPlace.style.padding = '5px';
  713. inputPlace.style.border = '1px solid var(--panel-border)';
  714. inputPlace.style.borderRadius = '4px';
  715. inputPlace.style.backgroundColor = 'var(--panel-bg)';
  716. inputPlace.style.color = 'var(--panel-text)';
  717. container.appendChild(inputPlace);
  718.  
  719. return [container, inputPlace];
  720. };
  721.  
  722. const createTextArea = (id, value, labelText, placeholder) => {
  723. const container = document.createElement('div');
  724. container.style.marginBottom = '15px';
  725.  
  726. const label = document.createElement('label');
  727. label.textContent = labelText;
  728. label.style.display = 'block';
  729. label.style.marginBottom = '5px';
  730. container.appendChild(label);
  731.  
  732. const textarea = document.createElement('textarea');
  733. textarea.id = id;
  734. if (typeof value === 'string') {
  735. textarea.value = value;
  736. } else {
  737. textarea.value = JSON.stringify(value, null, 2);
  738. }
  739. textarea.placeholder = placeholder;
  740. textarea.rows = 5;
  741. textarea.style.width = '100%';
  742. textarea.style.padding = '5px';
  743. textarea.style.border = '1px solid var(--panel-border)';
  744. textarea.style.borderRadius = '4px';
  745. textarea.style.backgroundColor = 'var(--panel-bg)';
  746. textarea.style.color = 'var(--panel-text)';
  747. container.appendChild(textarea);
  748.  
  749. return [container, textarea];
  750. };
  751.  
  752. const createButton = (text, onClick, primary = false) => {
  753. const button = document.createElement('button');
  754. button.textContent = text;
  755. button.style.padding = '8px 16px';
  756. button.style.border = 'none';
  757. button.style.borderRadius = '4px';
  758. button.style.cursor = 'pointer';
  759. button.style.backgroundColor = primary ? '#0078d7' : '#f0f0f0';
  760. button.style.color = primary ? '#ffffff' : '#333333';
  761. button.addEventListener('click', onClick);
  762. return button;
  763. };
  764.  
  765. const updateConfigPanelContent = panel => {
  766. panel.innerHTML = '';
  767.  
  768. const title = document.createElement('h3');
  769. title.textContent = '配置';
  770. title.style.marginTop = '0';
  771. title.style.marginBottom = '15px';
  772. panel.appendChild(title);
  773.  
  774. panel.appendChild(
  775. createTextInput(uiIDs.deeplxUrlInput, config.deeplxUrl, 'Deeplx URL', '填写 Deeplx 的请求地址')[0]
  776. );
  777. panel.appendChild(
  778. createTextInput(uiIDs.deeplxAuthKeyInput, config.deeplxAuthKey, 'Deeplx Auth Key', '填写 connect 的 key')[0]
  779. );
  780. panel.appendChild(
  781. createTextInput(
  782. uiIDs.translateSizeInput,
  783. config.translateSize,
  784. '翻译字体大小(百分比)',
  785. '默认值为150(字体大小为原始的150%)',
  786. 'number'
  787. )[0]
  788. );
  789. panel.appendChild(createCheckbox('enableTranslate', '启用翻译', config.enableTranslate));
  790. if (checkPinYinRequire()) {
  791. panel.appendChild(createCheckbox('enablePinYin', '启用拼音注音', config.enablePinYin));
  792. }
  793. panel.appendChild(createCheckbox('closeConfigAfterSave', '保存后自动关闭配置页面', config.closeConfigAfterSave));
  794.  
  795. const buttonContainer = document.createElement('div');
  796. buttonContainer.style.display = 'flex';
  797. buttonContainer.style.justifyContent = 'space-between';
  798. buttonContainer.style.marginTop = '20px';
  799.  
  800. const restoreDefaultsButton = createButton('恢复默认设置', restoreDefaults);
  801. restoreDefaultsButton.style.marginRight = '10px';
  802.  
  803. const saveButton = createButton('保存设置', saveConfig, true);
  804. const closeButton = createButton('关闭', () => {
  805. panel.style.display = 'none';
  806. });
  807.  
  808. buttonContainer.appendChild(restoreDefaultsButton);
  809. buttonContainer.appendChild(saveButton);
  810. buttonContainer.appendChild(closeButton);
  811. panel.appendChild(buttonContainer);
  812. };
  813.  
  814. const createConfigPanel = () => {
  815. // 获取页面的 <meta name="theme-color"> 标签
  816. const themeColorMeta = document.querySelector('meta[name="theme-color"]');
  817. let themeColor = '#ffffff'; // 默认白色背景
  818. let invertedColor = '#000000'; // 默认黑色字体
  819.  
  820. if (themeColorMeta) {
  821. themeColor = themeColorMeta.getAttribute('content');
  822. invertedColor = getInvertColor(themeColor); // 计算相反颜色
  823. }
  824.  
  825. // 设置样式变量
  826. const style = document.createElement('style');
  827. style.textContent = `
  828. :root {
  829. --panel-bg: ${themeColor};
  830. --panel-text: ${invertedColor};
  831. --panel-border: ${invertedColor};
  832. --button-bg: ${invertedColor};
  833. --button-text: ${themeColor};
  834. --button-hover-bg: ${getInvertColor(invertedColor)};
  835. }`;
  836. document.head.appendChild(style);
  837.  
  838. const panel = document.createElement('div');
  839. panel.id = uiIDs.configPanel;
  840. panel.style.position = 'fixed';
  841. panel.style.top = '80px';
  842. panel.style.right = '360px';
  843. panel.style.padding = '20px';
  844. panel.style.border = `1px solid var(--panel-border)`;
  845. panel.style.borderRadius = '8px';
  846. panel.style.zIndex = '10000';
  847. panel.style.width = '300px';
  848. panel.style.maxHeight = '80%';
  849. panel.style.overflowY = 'auto';
  850. panel.style.display = 'none'; // 默认隐藏面板
  851. panel.style.backgroundColor = 'var(--panel-bg)';
  852. panel.style.color = 'var(--panel-text)';
  853. panel.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
  854.  
  855. updateConfigPanelContent(panel);
  856.  
  857. document.body.appendChild(panel);
  858. return panel;
  859. };
  860.  
  861. const toggleConfigPanel = () => {
  862. let panel = document.getElementById(uiIDs.configPanel);
  863. panel = panel || createConfigPanel();
  864. panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  865. };
  866.  
  867. const createConfigButton = () => {
  868. const toolbar = document.querySelector('.d-editor-button-bar');
  869. if (!toolbar || document.getElementById(uiIDs.configButton)) return;
  870.  
  871. const configButton = document.createElement('button');
  872. configButton.id = uiIDs.configButton;
  873. configButton.className = 'btn btn-flat btn-icon no-text user-menu-tab active';
  874. configButton.title = '配置';
  875. configButton.innerHTML =
  876. '<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>';
  877. configButton.onclick = toggleConfigPanel;
  878.  
  879. toolbar.appendChild(configButton);
  880. };
  881.  
  882. const watchReplyControl = () => {
  883. const replyControl = document.getElementById(uiIDs.replyControl);
  884. if (!replyControl) return;
  885.  
  886. const observer = new MutationObserver(mutations => {
  887. mutations.forEach(mutation => {
  888. if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
  889. if (replyControl.classList.contains('closed')) {
  890. const panel = document.getElementById(uiIDs.configPanel);
  891. if (panel) {
  892. panel.style.display = 'none';
  893. }
  894. } else {
  895. // 当 reply-control 重新打开时,尝试添加配置按钮
  896. setTimeout(createConfigButton, 500); // 给予一些时间让编辑器完全加载
  897. }
  898. }
  899. });
  900. });
  901.  
  902. observer.observe(replyControl, { attributes: true });
  903. };
  904.  
  905. const watchForEditor = () => {
  906. const observer = new MutationObserver(mutations => {
  907. mutations.forEach(mutation => {
  908. if (mutation.type === 'childList') {
  909. const addedNodes = mutation.addedNodes;
  910. for (let node of addedNodes) {
  911. if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('d-editor')) {
  912. createConfigButton();
  913. return;
  914. }
  915. }
  916. }
  917. });
  918. });
  919.  
  920. observer.observe(document.body, { childList: true, subtree: true });
  921. };
  922.  
  923. const init = () => {
  924. const container = document.getElementById(uiIDs.replyControl);
  925. container.addEventListener('click', handleClick, true);
  926. document.addEventListener('keydown', handleKeydown, true);
  927. if (!document.getElementById(uiIDs.configButton)) {
  928. createConfigButton();
  929. }
  930. watchReplyControl();
  931. watchForEditor();
  932. };
  933.  
  934. // 初始化
  935. setTimeout(() => {
  936. init();
  937. }, 1000);
  938. })();