ParaTranz diff

ParaTranz enhanced

目前为 2025-01-03 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name ParaTranz diff
  3. // @namespace https://paratranz.cn/users/44232
  4. // @version 0.9.0
  5. // @description ParaTranz enhanced
  6. // @author ooo
  7. // @match http*://paratranz.cn/*
  8. // @icon https://paratranz.cn/favicon.png
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/medium-zoom/1.1.0/medium-zoom.min.js
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (async function() {
  14. 'use strict';
  15.  
  16. // #region 主要功能
  17.  
  18. // #region 自动跳过空白页 initSkip TODO 疑似失效
  19. function initSkip() {
  20. if (document.querySelector('.string-list .empty-sign') &&
  21. location.search.match(/(\?|&)page=\d+/g)) {
  22. document.querySelector('.pagination .page-item a')?.click();
  23. }
  24. }
  25. // #endregion
  26.  
  27. // #region 添加快捷键 addHotkeys
  28. function addHotkeys() {
  29. document.addEventListener('keydown', (event) => {
  30. if (event.ctrlKey && event.shiftKey && event.key === 'V') {
  31. event.preventDefault();
  32. mockInput(document.querySelector('.editor-core .original')?.textContent);
  33. }
  34. });
  35. }
  36. // #endregion
  37.  
  38. // #region 更多搜索高亮 markSearchParams
  39. let markSearchParams = () => {};
  40. function updMark() {
  41. const params = new URLSearchParams(location.search);
  42. const text = params.get('text');
  43. const original = params.get('original');
  44. const translation = params.get('translation');
  45. const context = params.get('context');
  46.  
  47. if (text) {
  48. markSearchParams = (isOriginUpd) => {
  49. if (isOriginUpd) markNorm('.editor-core .original', text);
  50. return markEditing(text);
  51. }
  52. } else if (original) {
  53. markSearchParams = (isOriginUpd) => {
  54. if (isOriginUpd) markNorm('.editor-core .original', original);
  55. }
  56. } else if (translation) {
  57. markSearchParams = () => {
  58. return markEditing(translation);
  59. }
  60. } else if (context) {
  61. markSearchParams = () => {
  62. markNorm('.context', context);
  63. }
  64. } else {
  65. markSearchParams = () => {};
  66. }
  67. }
  68. let dropLastMark = updMark();
  69.  
  70. function markNorm(selector, toMark) {
  71. const container = document.querySelector(selector);
  72. if (!container) return;
  73.  
  74. let toMarkPattern = toMark;
  75. if (document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked) { // 忽略大小写
  76. toMarkPattern = new RegExp(`(${toMark.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig');
  77. }
  78.  
  79. const HTML = container.innerHTML;
  80. const currentMark = `<mark class="PZS">${toMark}</mark>`;
  81. if (HTML.includes(currentMark)) return;
  82. container.innerHTML = HTML.replaceAll('<mark class="PZS">', '').replace(/(?<=>|^)([^<]*?)(?=<|$)/g, (match) => {
  83. if (typeof toMarkPattern === 'string') {
  84. return match.replaceAll(toMarkPattern, currentMark);
  85. } else {
  86. return match.replace(toMarkPattern, '<mark class="PZS">$1</mark>');
  87. }
  88. });
  89. }
  90.  
  91. function markEditing(toMark) {
  92. const textarea = document.querySelector('textarea.translation');
  93. if (!textarea) return;
  94. const lastOverlay = document.getElementById('PZSoverlay');
  95. if (lastOverlay) return;
  96.  
  97. const overlay = document.createElement('div');
  98. overlay.id = 'PZSoverlay';
  99. overlay.className = textarea.className;
  100. const textareaStyle = window.getComputedStyle(textarea);
  101. for (let i = 0; i < textareaStyle.length; i++) {
  102. const property = textareaStyle[i];
  103. overlay.style[property] = textareaStyle.getPropertyValue(property);
  104. }
  105. overlay.style.position = 'absolute';
  106. overlay.style.pointerEvents = 'none';
  107. overlay.style.setProperty('background', 'transparent', 'important');
  108. overlay.style['-webkit-text-fill-color'] = 'transparent';
  109. overlay.style['overflow-y'] = 'hidden';
  110. overlay.style.resize = 'none';
  111.  
  112. textarea.parentNode.appendChild(overlay);
  113.  
  114. const updOverlay = () => {
  115. let toMarkPattern = toMark.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('\\n', '<br>');
  116. if (document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked) { // 忽略大小写
  117. toMarkPattern = new RegExp(`(${toMark.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig');
  118. }
  119. overlay.innerText = textarea.value;
  120. if (typeof toMarkPattern === 'string') {
  121. overlay.innerHTML = overlay.innerHTML.replaceAll(toMarkPattern, `<mark class="PZS" style="-webkit-text-fill-color:${
  122. window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color')
  123. };opacity:.5">${toMarkPattern}</mark>`);
  124. } else {
  125. overlay.innerHTML = overlay.innerHTML.replace(toMarkPattern, `<mark class="PZS" style="-webkit-text-fill-color:${
  126. window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color')
  127. };opacity:.5">$1</mark>`);
  128. }
  129. overlay.style.top = textarea.offsetTop + 'px';
  130. overlay.style.left = textarea.offsetLeft + 'px';
  131. overlay.style.width = textarea.offsetWidth + 'px';
  132. overlay.style.height = textarea.offsetHeight + 'px';
  133. };
  134.  
  135. updOverlay();
  136.  
  137. textarea.addEventListener('input', updOverlay);
  138.  
  139. const observer = new MutationObserver(updOverlay);
  140. observer.observe(textarea, { attributes: true, childList: true, subtree: true });
  141.  
  142. window.addEventListener('resize', updOverlay);
  143.  
  144. const cancelOverlay = () => {
  145. observer.disconnect();
  146. textarea.removeEventListener('input', updOverlay);
  147. window.removeEventListener('resize', updOverlay);
  148. }
  149. return cancelOverlay;
  150. }
  151. // #endregion
  152.  
  153. // #region 高亮上下文 markContext(originTxt)
  154. function markContext(originTxt) {
  155. const contextBox = document.querySelector('.context');
  156. if (!contextBox) return;
  157.  
  158. const context = contextBox.innerHTML.replaceAll(/<a.*?>(.*?)<\/a>/g, '$1').replaceAll(/<(\/?)(li|b|u|h\d|span)>/g, '&lt;$1$2&gt;');
  159. originTxt = originTxt.replaceAll('<br>', '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
  160. if (contextBox.querySelector('#PZmark')?.textContent === originTxt) return;
  161. contextBox.innerHTML = context.replace('<mark id="PZmark" class="mark">', '').replace(originTxt, `<mark id="PZmark" class="mark">${originTxt}</mark>`);
  162. }
  163. // #endregion
  164.  
  165. // #region 修复原文排版崩坏和<<>> fixOrigin(originElem)
  166. function fixOrigin(originElem) {
  167. originElem.innerHTML = originElem.innerHTML
  168. .replaceAll('<abbr title="noun.>" data-value=">">&gt;</abbr>', '&gt;')
  169. .replaceAll(/<var>(&lt;&lt;[^<]*?&gt;)<\/var>&gt;/g, '<var class="PZvar">$1&gt;</var>')
  170. .replaceAll('<i class="lf" <abbr="" title="noun.>" data-value=">">&gt;&gt;', '')
  171. .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;&gt;', '')
  172. .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;', '');
  173. }
  174. // #endregion
  175.  
  176. // #region 修复 Ctrl 唤起菜单的<<>> fixTagSelect
  177. const insertTag = debounce((tag) => {
  178. const textarea = document.querySelector('textarea.translation');
  179. const startPos = textarea.selectionStart;
  180. const endPos = textarea.selectionEnd;
  181. const currentText = textarea.value;
  182.  
  183. const before = currentText.slice(0, startPos);
  184. const after = currentText.slice(endPos);
  185.  
  186. mockInput(before.slice(0, Math.max(before.length - tag.length + 1, 0)) + tag + after);
  187.  
  188. textarea.selectionStart = startPos + 1;
  189. textarea.selectionEnd = endPos + 1;
  190. })
  191.  
  192. let activeTag = null;
  193. let modifiedTags = [];
  194. const tagSelectController = new AbortController();
  195. const { tagSelectSignal } = tagSelectController;
  196.  
  197. function tagSelectHandler(event) {
  198. if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
  199. activeTag &&= document.querySelector('.list-group-item.tag.active');
  200. }
  201. if (event.key === 'Enter') {
  202. if (!activeTag) return;
  203. if (!modifiedTags.includes(activeTag)) return;
  204. event.preventDefault();
  205. insertTag(activeTag?.textContent);
  206. activeTag = null;
  207. }
  208. }
  209.  
  210. function updFixedTags() {
  211.  
  212. const tags = document.querySelectorAll('.list-group-item.tag');
  213. activeTag = document.querySelector('.list-group-item.tag.active');
  214. modifiedTags = [];
  215.  
  216. for (const tag of tags) {
  217. tag.innerHTML = tag.innerHTML.trim();
  218. if (tag.innerHTML.startsWith('&lt;&lt;') && !tag.innerHTML.endsWith('&gt;&gt;')) {
  219. tag.innerHTML += '&gt;';
  220. modifiedTags.push(tag);
  221. }
  222. }
  223.  
  224. document.addEventListener('keyup', tagSelectHandler, { tagSelectSignal });
  225.  
  226. }
  227. // #endregion
  228.  
  229. // #region 将填充原文移到右边,增加填充原文并保存 tweakButtons
  230. function tweakButtons() {
  231. const copyButton = document.querySelector('button.btn-secondary:has(.fa-clone)');
  232. const rightButtons = document.querySelector('.right .btn-group');
  233.  
  234. if (rightButtons) {
  235. if (copyButton) {
  236. rightButtons.insertBefore(copyButton, rightButtons.firstChild);
  237. }
  238. if (document.querySelector('#PZpaste')) return;
  239. const pasteSave = document.createElement('button');
  240. rightButtons.appendChild(pasteSave);
  241. pasteSave.id = 'PZpaste';
  242. pasteSave.type = 'button';
  243. pasteSave.classList.add('btn', 'btn-secondary');
  244. pasteSave.title = '填充原文并保存';
  245. pasteSave.innerHTML = '<i aria-hidden="true" class="far fa-save"></i>';
  246. pasteSave.addEventListener('click', async () => {
  247. await mockInput(document.querySelector('.editor-core .original')?.textContent);
  248. document.querySelector('.right .btn-primary')?.click();
  249. });
  250. }
  251. }
  252. // #endregion
  253.  
  254. // #region 缩略对比差异中过长无差异文本 extractDiff
  255. function extractDiff() {
  256. document.querySelectorAll('.diff-wrapper:not(.PZedited)').forEach(wrapper => {
  257. wrapper.childNodes.forEach(node => {
  258. if (node.nodeType !== Node.TEXT_NODE || node.length < 200) return;
  259.  
  260. const text = node.cloneNode();
  261. const expand = document.createElement('span');
  262. expand.textContent = `${node.textContent.slice(0, 100)} ... ${node.textContent.slice(-100)}`;
  263. expand.style.cursor = 'pointer';
  264. expand.style.background = 'linear-gradient(to right, transparent, #aaf6, transparent)';
  265. expand.style.borderRadius = '2px';
  266.  
  267. let time = 0;
  268. let isMoving = false;
  269.  
  270. const start = () => {
  271. time = Date.now()
  272. isMoving = false;
  273. }
  274. const end = () => {
  275. if (isMoving || Date.now() - time > 500) return;
  276. expand.replaceWith(text);
  277. }
  278.  
  279. expand.addEventListener('mousedown', start);
  280. expand.addEventListener('mouseup', end);
  281. expand.addEventListener('mouseleave', () => time = 0);
  282.  
  283. expand.addEventListener('touchstart', start);
  284. expand.addEventListener('touchend', end);
  285. expand.addEventListener('touchcancel', () => time = 0);
  286. expand.addEventListener('touchmove', () => isMoving = true);
  287.  
  288. node.replaceWith(expand);
  289. });
  290. wrapper.classList.add('PZedited');
  291. });
  292. }
  293. // #endregion
  294.  
  295. // #region 点击对比差异绿色文字粘贴其中文本 clickDiff
  296. function clickDiff() {
  297. const addeds = document.querySelectorAll('.diff.added:not(.PZPedited)');
  298. for (const added of addeds) {
  299. added.classList.add('PZPedited');
  300. const text = added.textContent.replaceAll('\\n', '\n');
  301. added.style.cursor = 'pointer';
  302. added.addEventListener('click', () => {
  303. mockInsert(text);
  304. });
  305. }
  306. }
  307. // #endregion
  308.  
  309. // #region 快速搜索原文 copySearch
  310. async function copySearch() {
  311. if (document.querySelector('#PZsch')) return;
  312. const originSch = document.querySelector('.btn-sm');
  313. if (!originSch) return;
  314. originSch.insertAdjacentHTML('beforebegin', '<button id="PZsch" type="button" class="btn btn-secondary btn-sm"><i aria-hidden="true" class="far fa-clone"></i></button>');
  315. const newSch = document.querySelector('#PZsch');
  316. newSch.addEventListener('click', async () => {
  317. const original = document.querySelector('.editor-core .original')?.textContent;
  318. let input = document.querySelector('.search-form.mt-3 input[type=search]');
  319. if (!input) {
  320. await (() => new Promise(resolve => resolve(originSch.click())))();
  321. input = document.querySelector('.search-form.mt-3 input[type=search]');
  322. }
  323. input.value = original;
  324. input.dispatchEvent(new Event('input', {
  325. bubbles: true,
  326. cancelable: true,
  327. }));
  328. });
  329. }
  330. // #endregion
  331.  
  332. // #region 搜索结果对比差异 searchDiff
  333. function searchDiff() {
  334. const strings = document.querySelectorAll('.original.mb-1 span:not(:has(+a)');
  335. if (!strings[0]) return;
  336.  
  337. const original = document.querySelector('.editor-core .original')?.textContent;
  338. const { $diff } = document.querySelector('main').__vue__;
  339.  
  340. for (const string of strings) {
  341. const strHTML = string.innerHTML;
  342. const showDiff = document.createElement('a');
  343. showDiff.title = '查看差异';
  344. showDiff.href = '#';
  345. showDiff.target = '_self';
  346. showDiff.classList.add('small');
  347. showDiff.innerHTML = '<i aria-hidden="true" class="far fa-right-left-large"></i>';
  348.  
  349. string.after(' ', showDiff);
  350. showDiff.addEventListener('click', function() {
  351. string.innerHTML = this.isShown ? strHTML : $diff(string.textContent, original);
  352. this.isShown = !this.isShown;
  353. })
  354. }
  355. }
  356.  
  357. // #region 高级搜索空格变+修复 fixAdvSch
  358. function fixAdvSch() {
  359. const inputs = document.querySelectorAll('#advancedSearch table input');
  360. if (!inputs[0]) return;
  361. const params = new URLSearchParams(location.search);
  362. const values = [...params.entries()].filter(([key, _]) => /(text|original|translation).?/.test(key)).map(([_, value]) => value.replaceAll(' ', '+'));
  363. for (const input of inputs) {
  364. if (values.includes(input.value)) {
  365. input.value = input.value.replaceAll('+', ' ');
  366. input.dispatchEvent(new Event('input', {
  367. bubbles: true,
  368. cancelable: true,
  369. }));
  370. }
  371. }
  372. }
  373.  
  374. // #region 自动保存全部相同词条 autoSaveAll
  375. const autoSave = localStorage.getItem('pzdiffautosave');
  376. function autoSaveAll() {
  377. const button = document.querySelector('.modal-dialog .btn-primary');
  378. if (autoSave && button.textContent === '保存全部') button.click();
  379. }
  380.  
  381. // #region 自动填充100%相似译文 autoFill100
  382. function autoFill100() {
  383. const suggests = document.querySelectorAll('.string-item');
  384. const getSim = (suggest) => +suggest.querySelector('header span span').textContent.split('\n')?.[2].trim().slice(0, -1);
  385. const getTranslation = (suggest) => suggest.querySelector('.translation').textContent;
  386. for (const suggest of suggests) {
  387. if ([100, 101].includes(getSim(suggest))) {
  388. mockInput(getTranslation(suggest));
  389. break;
  390. }
  391. }
  392. }
  393.  
  394. // #region 重新排序历史词条 reSortSuggestions
  395. function reSortSuggestions() {
  396. const suggests = document.querySelectorAll('.string-item');
  397. const getSim = (suggest) => +suggest?.querySelector('header span span').textContent.split('\n')?.[2].trim().slice(0, -1);
  398. if (!getSim(suggests[0])) return;
  399. const sorted = [...suggests].sort((a, b) => getSim(b) - getSim(a));
  400. const parent = suggests[0].parentNode;
  401. const frag = document.createDocumentFragment();
  402. frag.append(...sorted);
  403. parent.innerHTML = '';
  404. parent.appendChild(frag);
  405. }
  406.  
  407. // #region 初始化自动编辑 initAuto
  408. async function initAuto() {
  409. const avatars = await waitForElems('.nav-item.user-info');
  410. avatars.forEach(async (avatar) => {
  411. let harvesting = false;
  412. let translationPattern, skipPattern, userTime;
  413. avatar.insertAdjacentHTML('afterend', `<li class="nav-item"><a href="javascript:;" target="_self" class="PZpp nav-link" role="button">PP收割机</a></li>`);
  414. document.querySelectorAll('.PZpp').forEach(btn => btn.addEventListener('click', async (e) => {
  415. if (location.pathname.split('/')[3] !== 'strings') return;
  416. harvesting = !harvesting;
  417. if (harvesting) {
  418. e.target.style.color = '#dc3545';
  419. translationPattern = prompt(`请确认译文模板代码,字符串用'包裹;常用代码:
  420. original(原文)
  421. oldTrans(现有译文)
  422. suggest(第1条翻译建议)
  423. suggestSim(上者匹配度,最大100)`, 'original');
  424. if (translationPattern === null) return cancel();
  425. skipPattern = prompt(`请确认跳过条件代码,多个条件用逻辑运算符相连;常用代码:
  426. original.match(/^(\s|\n|<<.*?>>|<.*?>)*/gm)[0] !== original(跳过并非只包含标签)
  427. oldTrans(现有译文)
  428. suggest(第1条翻译建议)
  429. suggestSim(上者匹配度,最大100
  430. context(上下文内容)`, '');
  431. if (skipPattern === null) return cancel();
  432. if (skipPattern === '') skipPattern = 'false';
  433. userTime = prompt('请确认生成译文后等待时间(单位:ms)', '500');
  434. if (userTime === null) return cancel();
  435. function cancel() {
  436. harvesting = false;
  437. e.target.style.color = '';
  438. }
  439. } else {
  440. e.target.style.color = '';
  441. return;
  442. }
  443.  
  444. const hideAlert = document.createElement('style');
  445. document.head.appendChild(hideAlert);
  446. hideAlert.innerHTML = '.alert-success.alert-global{display:none}';
  447.  
  448. const checkboxs = [...document.querySelectorAll('.right .custom-checkbox')].slice(0, 2);
  449. const checkboxValues = checkboxs.map(e => e.__vue__.$data.localChecked);
  450. checkboxs.forEach(e => e.__vue__.$data.localChecked = true);
  451.  
  452. const print = {
  453. waiting: () => console.log('%cWAITING...', 'background: #007BFF; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  454. skip: () => console.log('%cSKIP', 'background: #FFC107; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  455. click: () => console.log('%cCLICK', 'background: #20C997; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  456. end: () => console.log('%cTHE END', 'background: #DE065B; color: white; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  457. }
  458.  
  459. const INTERVAL = 100;
  460. let interval = INTERVAL;
  461. let lastInfo = null;
  462.  
  463. function prepareWait() {
  464. print.waiting();
  465. interval = INTERVAL;
  466. lastInfo = null;
  467. return true;
  468. }
  469.  
  470. function skipOrFin(originElem, nextButton) {
  471. if (nextString(nextButton)) return false;
  472. print.skip();
  473. interval = 50;
  474. lastInfo = [
  475. originElem,
  476. location.search.match(/(?<=(\?|&)page=)\d+/g)[0]
  477. ];
  478. return true;
  479. }
  480.  
  481. function nextString(button) {
  482. if (button.disabled) {
  483. print.end();
  484. harvesting = false;
  485. e.target.style.color = '';
  486. return true;
  487. }
  488. button.click();
  489. return false;
  490. }
  491.  
  492. try {
  493. while (true) {
  494. await sleep(interval);
  495.  
  496. if (lastInfo) {
  497. const [ lastOrigin, lastPage ] = lastInfo;
  498. // 已点击翻页,但原文未发生改变
  499. const skipWaiting = location.search.match(/(?<=(\?|&)page=)\d+/g)[0] !== lastPage
  500. && document.querySelector('.editor-core .original') === lastOrigin;
  501. if (skipWaiting && prepareWait()) continue;
  502. }
  503.  
  504. const originElem = document.querySelector('.editor-core .original');
  505. if (!originElem && prepareWait()) continue;
  506. const nextButton = document.querySelectorAll('.navigation .btn-secondary')[1];
  507. if (!nextButton && prepareWait()) continue;
  508.  
  509. const original = originElem.textContent;
  510. const oldTrans = document.querySelector('textarea.translation').value;
  511. let suggest = null, suggestSim = 0;
  512. if (translationPattern.includes('suggest') || skipPattern.includes('suggest')) {
  513. suggest = (await waitForElems('.translation-memory .translation, .empty-sign'))[0].textContent;
  514. suggestSim = +(await waitForElems('.translation-memory header span span'))[0].textContent.split('\n')?.[2].trim().slice(0, -1);
  515. }
  516. const context = document.querySelector('.context')?.textContent;
  517.  
  518. if (eval(skipPattern)) {
  519. if (skipOrFin(originElem, nextButton)) continue; else break;
  520. }
  521.  
  522. const translation = eval(translationPattern);
  523. if (!translation && prepareWait()) continue;
  524.  
  525. await mockInput(translation);
  526. await sleep(userTime);
  527. if (!harvesting) break; // 放在等待后,以便在等待间隔点击取消
  528.  
  529. const translateButton = document.querySelector('.right .btn-primary');
  530. if (!translateButton) {
  531. if (skipOrFin(originElem, nextButton)) continue; else break;
  532. } else {
  533. translateButton.click();
  534. print.click();
  535. interval = INTERVAL;
  536. lastInfo = null;
  537. continue;
  538. }
  539. }
  540. } catch (e) {
  541. console.error(e);
  542. alert('出错了!');
  543. } finally {
  544. hideAlert.remove();
  545. checkboxs.forEach((e, i) => { e.__vue__.$data.localChecked = checkboxValues[i] });
  546. }
  547.  
  548. }));
  549. });
  550. }
  551. // #endregion
  552.  
  553. // #endregion
  554.  
  555. addHotkeys();
  556. initAuto();
  557.  
  558. let lastPath = location.pathname;
  559. async function actByPath() {
  560. lastPath = location.pathname;
  561. if (location.pathname.split('/').pop() === 'strings') {
  562.  
  563. let original;
  564. let lastOriginText = '';
  565. let toObserve = document.body;
  566.  
  567. let observer = new MutationObserver((mutations) => {
  568.  
  569. fixAdvSch();
  570.  
  571. original = document.querySelector('.editor-core .original');
  572. if (!original) return;
  573. const isOriginUpd = lastOriginText && original.textContent !== lastOriginText;
  574. lastOriginText = original.textContent;
  575.  
  576. observer.disconnect();
  577. initSkip();
  578. markContext(original.textContent);
  579. markSearchParams(isOriginUpd);
  580. fixOrigin(original);
  581. tweakButtons();
  582. clickDiff();
  583. extractDiff();
  584. copySearch();
  585.  
  586. if (isOriginUpd) {
  587. const input = document.querySelector('.search-form.mt-3 input[type=search]');
  588. if (input) document.querySelectorAll('.btn-sm')[1]?.click();
  589. }
  590.  
  591. for (const mutation of mutations) {
  592. const { addedNodes, removedNodes } = mutation;
  593. for (const node of addedNodes) {
  594. console.debug('added', node);
  595. if (node.matches?.('.list-group.tags')) updFixedTags();
  596. if (node.matches?.('.string-item a.small')) node.remove();
  597. if (node.matches?.('.modal-backdrop')) autoSaveAll();
  598. }
  599. for (const node of removedNodes) {
  600. console.debug('removed ', node);
  601. if (node.matches?.('.loading')) { // 历史加载完成
  602. searchDiff();
  603. autoFill100();
  604. reSortSuggestions();
  605. }
  606. if (node.matches?.('.list-group.tags')) tagSelectController.abort();
  607. }
  608. }
  609.  
  610. observer.observe(toObserve, {
  611. childList: true,
  612. subtree: true,
  613. });
  614. });
  615.  
  616. observer.observe(toObserve, {
  617. childList: true,
  618. subtree: true,
  619. });
  620.  
  621. return observer;
  622.  
  623. } else if (location.pathname.split('/').at(-2) === 'issues') {
  624. waitForElems('.text-content p img').then((imgs) => {
  625. imgs.forEach(mediumZoom);
  626. });
  627. } else if (location.pathname.split('/').pop() === 'history') {
  628. let observer = new MutationObserver(() => {
  629.  
  630. observer.disconnect();
  631. extractDiff();
  632.  
  633. observer.observe(document.body, {
  634. childList: true,
  635. subtree: true,
  636. });
  637. });
  638. observer.observe(document.body, {
  639. childList: true,
  640. subtree: true,
  641. });
  642. return observer;
  643. }
  644. }
  645. let cancelAct = await actByPath();
  646. (await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async ()=>{
  647. dropLastMark?.();
  648. dropLastMark = updMark();
  649. if (lastPath === location.pathname) return;
  650. cancelAct?.disconnect();
  651. console.debug('path changed');
  652. cancelAct = await actByPath();
  653. });
  654.  
  655. // #region utils
  656. function waitForElems(selector) {
  657. return new Promise(resolve => {
  658. if (document.querySelector(selector)) {
  659. return resolve(document.querySelectorAll(selector));
  660. }
  661.  
  662. const observer = new MutationObserver(() => {
  663. if (document.querySelector(selector)) {
  664. resolve(document.querySelectorAll(selector));
  665. observer.disconnect();
  666. }
  667. });
  668.  
  669. observer.observe(document.body, {
  670. childList: true,
  671. subtree: true
  672. });
  673. });
  674. }
  675.  
  676. function sleep(delay) {
  677. return new Promise((resolve) => setTimeout(resolve, delay));
  678. }
  679.  
  680. function mockInput(text) {
  681. return new Promise((resolve) => {
  682. const textarea = document.querySelector('textarea.translation');
  683. if (!textarea) return;
  684. textarea.value = text;
  685. textarea.dispatchEvent(new Event('input', {
  686. bubbles: true,
  687. cancelable: true,
  688. }));
  689. return resolve(0);
  690. })
  691. }
  692.  
  693. function mockInsert(text) {
  694. const textarea = document.querySelector('textarea.translation');
  695. if (!textarea) return;
  696. const startPos = textarea.selectionStart;
  697. const endPos = textarea.selectionEnd;
  698. const currentText = textarea.value;
  699.  
  700. const before = currentText.slice(0, startPos);
  701. const after = currentText.slice(endPos);
  702.  
  703. mockInput(before + text + after);
  704.  
  705. textarea.selectionStart = startPos + text.length;
  706. textarea.selectionEnd = endPos + text.length;
  707. }
  708.  
  709. function debounce(func, timeout = 300) {
  710. let called = false;
  711. return (...args) => {
  712. if (!called) {
  713. func.apply(this, args);
  714. called = true;
  715. setTimeout(() => {
  716. called = false;
  717. }, timeout);
  718. }
  719. };
  720. }
  721. // #endregion
  722.  
  723. })();