ParaTranz diff

ParaTranz enhanced

目前为 2024-12-31 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name ParaTranz diff
  3. // @namespace https://paratranz.cn/users/44232
  4. // @version 0.8.1
  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
  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. tagSelectController.abort();
  208. }
  209. }
  210.  
  211. function updFixedTags() {
  212.  
  213. const tags = document.querySelectorAll('.list-group-item.tag');
  214. activeTag = document.querySelector('.list-group-item.tag.active');
  215. modifiedTags = [];
  216.  
  217. for (const tag of tags) {
  218. tag.innerHTML = tag.innerHTML.trim();
  219. if (tag.innerHTML.startsWith('&lt;&lt;') && !tag.innerHTML.endsWith('&gt;&gt;')) {
  220. tag.innerHTML += '&gt;';
  221. modifiedTags.push(tag);
  222. }
  223. }
  224.  
  225. document.addEventListener('keyup', tagSelectHandler, { tagSelectSignal });
  226.  
  227. }
  228. // #endregion
  229.  
  230. // #region 将填充原文移到右边,增加填充原文并保存 tweakButtons
  231. function tweakButtons() {
  232. const copyButton = document.querySelector('button.btn-secondary:has(.fa-clone)');
  233. const rightButtons = document.querySelector('.right .btn-group');
  234.  
  235. if (rightButtons) {
  236. if (copyButton) {
  237. rightButtons.insertBefore(copyButton, rightButtons.firstChild);
  238. }
  239. if (document.querySelector('#PZpaste')) return;
  240. const pasteSave = document.createElement('button');
  241. rightButtons.appendChild(pasteSave);
  242. pasteSave.id = 'PZpaste';
  243. pasteSave.type = 'button';
  244. pasteSave.classList.add('btn', 'btn-secondary');
  245. pasteSave.title = '填充原文并保存';
  246. pasteSave.innerHTML = '<i aria-hidden="true" class="far fa-save"></i>';
  247. pasteSave.addEventListener('click', async () => {
  248. await mockInput(document.querySelector('.editor-core .original')?.textContent);
  249. document.querySelector('.right .btn-primary')?.click();
  250. });
  251. }
  252. }
  253. // #endregion
  254.  
  255. // #region 缩略对比差异中过长无差异文本 extractDiff
  256. function extractDiff() {
  257. document.querySelectorAll('.diff-wrapper:not(.PZedited)').forEach(wrapper => {
  258. wrapper.childNodes.forEach(node => {
  259. if (node.nodeType !== Node.TEXT_NODE || node.length < 200) return;
  260.  
  261. const text = node.cloneNode();
  262. const expand = document.createElement('span');
  263. expand.textContent = `${node.textContent.slice(0, 100)} ... ${node.textContent.slice(-100)}`;
  264. expand.style.cursor = 'pointer';
  265. expand.style.background = 'linear-gradient(to right, transparent, #aaf6, transparent)';
  266. expand.style.borderRadius = '2px';
  267.  
  268. let time = 0;
  269. let isMoving = false;
  270.  
  271. const start = () => {
  272. time = Date.now()
  273. isMoving = false;
  274. }
  275. const end = () => {
  276. if (isMoving || Date.now() - time > 500) return;
  277. expand.replaceWith(text);
  278. }
  279.  
  280. expand.addEventListener('mousedown', start);
  281. expand.addEventListener('mouseup', end);
  282. expand.addEventListener('mouseleave', () => time = 0);
  283.  
  284. expand.addEventListener('touchstart', start);
  285. expand.addEventListener('touchend', end);
  286. expand.addEventListener('touchcancel', () => time = 0);
  287. expand.addEventListener('touchmove', () => isMoving = true);
  288.  
  289. node.replaceWith(expand);
  290. });
  291. wrapper.classList.add('PZedited');
  292. });
  293. }
  294. // #endregion
  295.  
  296. // #region 点击对比差异绿色文字粘贴其中文本 clickDiff
  297. function clickDiff() {
  298. const addeds = document.querySelectorAll('.diff.added:not(.PZPedited)');
  299. for (const added of addeds) {
  300. added.classList.add('PZPedited');
  301. const text = added.textContent.replaceAll('\\n', '\n');
  302. added.style.cursor = 'pointer';
  303. added.addEventListener('click', () => {
  304. mockInsert(text);
  305. });
  306. }
  307. }
  308. // #endregion
  309.  
  310. // #region 快速搜索原文 copySearch
  311. async function copySearch() {
  312. if (document.querySelector('#PZsch')) return;
  313. const originSch = document.querySelector('.btn-sm');
  314. if (!originSch) return;
  315. originSch.insertAdjacentHTML('beforebegin', '<button id="PZsch" type="button" class="btn btn-secondary btn-sm"><i aria-hidden="true" class="far fa-clone"></i></button>');
  316. const newSch = document.querySelector('#PZsch');
  317. newSch.addEventListener('click', async () => {
  318. const original = document.querySelector('.editor-core .original')?.textContent;
  319. let input = document.querySelector('.search-form.mt-3 input[type=search]');
  320. if (!input) {
  321. await (() => new Promise(resolve => resolve(originSch.click())))();
  322. input = document.querySelector('.search-form.mt-3 input[type=search]');
  323. }
  324. input.value = original;
  325. input.dispatchEvent(new Event('input', {
  326. bubbles: true,
  327. cancelable: true,
  328. }));
  329. });
  330. }
  331. // #endregion
  332.  
  333. // #region 搜索结果对比差异 searchDiff
  334. function searchDiff() {
  335. const strings = document.querySelectorAll('.original.mb-1 span:not(:has(+a)');
  336. if (!strings[0]) return;
  337.  
  338. const original = document.querySelector('.editor-core .original')?.textContent;
  339. const { $diff } = document.querySelector('main').__vue__;
  340.  
  341. for (const string of strings) {
  342. const strHTML = string.innerHTML;
  343. const showDiff = document.createElement('a');
  344. showDiff.title = '查看差异';
  345. showDiff.href = '#';
  346. showDiff.target = '_self';
  347. showDiff.classList.add('small');
  348. showDiff.innerHTML = '<i aria-hidden="true" class="far fa-right-left-large"></i>';
  349.  
  350. string.after(' ', showDiff);
  351. showDiff.addEventListener('click', function() {
  352. string.innerHTML = this.isShown ? strHTML : $diff(string.textContent, original);
  353. this.isShown = !this.isShown;
  354. })
  355. }
  356. }
  357.  
  358. // #region 高级搜索空格变+修复 fixAdvSch
  359. function fixAdvSch() {
  360. const inputs = document.querySelectorAll('#advancedSearch table input');
  361. if (!inputs[0]) return;
  362. const params = new URLSearchParams(location.search);
  363. const values = [...params.entries()].filter(([key, _]) => /(text|original|translation).?/.test(key)).map(([_, value]) => value.replaceAll(' ', '+'));
  364. for (const input of inputs) {
  365. if (values.includes(input.value)) {
  366. input.value = input.value.replaceAll('+', ' ');
  367. input.dispatchEvent(new Event('input', {
  368. bubbles: true,
  369. cancelable: true,
  370. }));
  371. }
  372. }
  373. }
  374.  
  375. // #region 自动保存全部相同词条 autoSaveAll
  376. const autoSave = localStorage.getItem('pzdiffautosave');
  377. function autoSaveAll() {
  378. const button = document.querySelector('.modal-dialog .btn-primary');
  379. if (autoSave && button.textContent === '保存全部') button.click();
  380. }
  381.  
  382. // #region 初始化自动编辑 initAuto
  383. async function initAuto() {
  384. const avatars = await waitForElems('.nav-item.user-info');
  385. avatars.forEach(async (avatar) => {
  386. let harvesting = false;
  387. let translationPattern, skipPattern, userTime;
  388. avatar.insertAdjacentHTML('afterend', `<li class="nav-item"><a href="javascript:;" target="_self" class="PZpp nav-link" role="button">PP收割机</a></li>`);
  389. document.querySelectorAll('.PZpp').forEach(btn => btn.addEventListener('click', async (e) => {
  390. if (location.pathname.split('/')[3] !== 'strings') return;
  391. harvesting = !harvesting;
  392. if (harvesting) {
  393. e.target.style.color = '#dc3545';
  394. translationPattern = prompt(`请确认译文模板代码,字符串用'包裹;常用代码:
  395. original(原文)
  396. oldTrans(现有译文)
  397. suggest(第1条翻译建议)
  398. suggestSim(上者匹配度,最大100)`, 'original');
  399. if (translationPattern === null) return cancel();
  400. skipPattern = prompt(`请确认跳过条件代码,多个条件用逻辑运算符相连;常用代码:
  401. original.match(/^(\s|\n|<<.*?>>|<.*?>)*/gm)[0] !== original(跳过并非只包含标签)
  402. oldTrans(现有译文)
  403. suggest(第1条翻译建议)
  404. suggestSim(上者匹配度,最大100
  405. context(上下文内容)`, '');
  406. if (skipPattern === null) return cancel();
  407. if (skipPattern === '') skipPattern = 'false';
  408. userTime = prompt('请确认生成译文后等待时间(单位:ms)', '500');
  409. if (userTime === null) return cancel();
  410. function cancel() {
  411. harvesting = false;
  412. e.target.style.color = '';
  413. }
  414. } else {
  415. e.target.style.color = '';
  416. return;
  417. }
  418.  
  419. const hideAlert = document.createElement('style');
  420. document.head.appendChild(hideAlert);
  421. hideAlert.innerHTML = '.alert-success.alert-global{display:none}';
  422.  
  423. const checkboxs = [...document.querySelectorAll('.right .custom-checkbox')].slice(0, 2);
  424. const checkboxValues = checkboxs.map(e => e.__vue__.$data.localChecked);
  425. checkboxs.forEach(e => e.__vue__.$data.localChecked = true);
  426.  
  427. const print = {
  428. waiting: () => console.log('%cWAITING...', 'background: #007BFF; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  429. skip: () => console.log('%cSKIP', 'background: #FFC107; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  430. click: () => console.log('%cCLICK', 'background: #20C997; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  431. end: () => console.log('%cTHE END', 'background: #DE065B; color: white; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'),
  432. }
  433.  
  434. const INTERVAL = 100;
  435. let interval = INTERVAL;
  436. let lastInfo = null;
  437.  
  438. function prepareWait() {
  439. print.waiting();
  440. interval = INTERVAL;
  441. lastInfo = null;
  442. return true;
  443. }
  444.  
  445. function skipOrFin(originElem, nextButton) {
  446. if (nextString(nextButton)) return false;
  447. print.skip();
  448. interval = 50;
  449. lastInfo = [
  450. originElem,
  451. location.search.match(/(?<=(\?|&)page=)\d+/g)[0]
  452. ];
  453. return true;
  454. }
  455.  
  456. function nextString(button) {
  457. if (button.disabled) {
  458. print.end();
  459. harvesting = false;
  460. e.target.style.color = '';
  461. return true;
  462. }
  463. button.click();
  464. return false;
  465. }
  466.  
  467. try {
  468. while (true) {
  469. await sleep(interval);
  470.  
  471. if (lastInfo) {
  472. const [ lastOrigin, lastPage ] = lastInfo;
  473. // 已点击翻页,但原文未发生改变
  474. const skipWaiting = location.search.match(/(?<=(\?|&)page=)\d+/g)[0] !== lastPage
  475. && document.querySelector('.editor-core .original') === lastOrigin;
  476. if (skipWaiting && prepareWait()) continue;
  477. }
  478.  
  479. const originElem = document.querySelector('.editor-core .original');
  480. if (!originElem && prepareWait()) continue;
  481. const nextButton = document.querySelectorAll('.navigation .btn-secondary')[1];
  482. if (!nextButton && prepareWait()) continue;
  483.  
  484. const original = originElem.textContent;
  485. const oldTrans = document.querySelector('textarea.translation').value;
  486. let suggest = null, suggestSim = 0;
  487. if (translationPattern.includes('suggest') || skipPattern.includes('suggest')) {
  488. suggest = (await waitForElems('.translation-memory .translation, .empty-sign'))[0].textContent;
  489. suggestSim = +(await waitForElems('.translation-memory header span span'))[0].textContent.split('\n')?.[2].trim().slice(0, -1);
  490. }
  491. const context = document.querySelector('.context')?.textContent;
  492.  
  493. if (eval(skipPattern)) {
  494. if (skipOrFin(originElem, nextButton)) continue; else break;
  495. }
  496.  
  497. const translation = eval(translationPattern);
  498. if (!translation && prepareWait()) continue;
  499.  
  500. await mockInput(translation);
  501. await sleep(userTime);
  502. if (!harvesting) break; // 放在等待后,以便在等待间隔点击取消
  503.  
  504. const translateButton = document.querySelector('.right .btn-primary');
  505. if (!translateButton) {
  506. if (skipOrFin(originElem, nextButton)) continue; else break;
  507. } else {
  508. translateButton.click();
  509. print.click();
  510. interval = INTERVAL;
  511. lastInfo = null;
  512. continue;
  513. }
  514. }
  515. } catch (e) {
  516. console.error(e);
  517. alert('出错了!');
  518. } finally {
  519. hideAlert.remove();
  520. checkboxs.forEach((e, i) => { e.__vue__.$data.localChecked = checkboxValues[i] });
  521. }
  522.  
  523. }));
  524. });
  525. }
  526. // #endregion
  527.  
  528. // #endregion
  529.  
  530. addHotkeys();
  531. initAuto();
  532.  
  533. let lastPath = location.pathname;
  534. async function actByPath() {
  535. lastPath = location.pathname;
  536. if (location.pathname.split('/').pop() === 'strings') {
  537.  
  538. let original;
  539. let lastOriginText = '';
  540. let toObserve = document.body;
  541.  
  542. let observer = new MutationObserver((mutations) => {
  543.  
  544. const savedRange = saveSelection();
  545. if (savedRange) restoreSelection(savedRange);
  546.  
  547. fixAdvSch();
  548.  
  549. original = document.querySelector('.editor-core .original');
  550. if (!original) return;
  551. const isOriginUpd = lastOriginText && original.textContent !== lastOriginText;
  552. lastOriginText = original.textContent;
  553.  
  554. observer.disconnect();
  555. initSkip();
  556. markContext(original.textContent);
  557. markSearchParams(isOriginUpd);
  558. fixOrigin(original);
  559. tweakButtons();
  560. clickDiff();
  561. extractDiff();
  562. copySearch();
  563.  
  564. if (isOriginUpd) {
  565. const input = document.querySelector('.search-form.mt-3 input[type=search]');
  566. if (input) document.querySelectorAll('.btn-sm')[1]?.click();
  567. }
  568.  
  569. for (const mutation of mutations) {
  570. const { addedNodes, removedNodes } = mutation;
  571. for (const node of addedNodes) {
  572. // console.log('added', node);
  573. if (node.matches?.('.list-group.tags')) updFixedTags();
  574. if (node.matches?.('.string-item a.small')) node.remove();
  575. if (node.matches?.('.modal-backdrop')) autoSaveAll();
  576. }
  577. for (const node of removedNodes) {
  578. // console.log('removed ', node);
  579. if (node.matches?.('.loading')) searchDiff();
  580. }
  581. }
  582.  
  583. observer.observe(toObserve, {
  584. childList: true,
  585. subtree: true,
  586. });
  587. });
  588.  
  589. observer.observe(toObserve, {
  590. childList: true,
  591. subtree: true,
  592. });
  593.  
  594. return observer;
  595.  
  596. } else if (location.pathname.split('/').at(-2) === 'issues') {
  597. waitForElems('.text-content p img').then((imgs) => {
  598. imgs.forEach(mediumZoom);
  599. });
  600. } else if (location.pathname.split('/').pop() === 'history') {
  601. let observer = new MutationObserver(() => {
  602.  
  603. observer.disconnect();
  604. extractDiff();
  605.  
  606. observer.observe(document.body, {
  607. childList: true,
  608. subtree: true,
  609. });
  610. });
  611. observer.observe(document.body, {
  612. childList: true,
  613. subtree: true,
  614. });
  615. return observer;
  616. }
  617. }
  618. let cancelAct = await actByPath();
  619. (await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async ()=>{
  620. dropLastMark?.();
  621. dropLastMark = updMark();
  622. if (lastPath === location.pathname) return;
  623. cancelAct?.disconnect();
  624. console.debug('path changed');
  625. cancelAct = await actByPath();
  626. });
  627.  
  628. // #region utils
  629. function waitForElems(selector) {
  630. return new Promise(resolve => {
  631. if (document.querySelector(selector)) {
  632. return resolve(document.querySelectorAll(selector));
  633. }
  634.  
  635. const observer = new MutationObserver(() => {
  636. if (document.querySelector(selector)) {
  637. resolve(document.querySelectorAll(selector));
  638. observer.disconnect();
  639. }
  640. });
  641.  
  642. observer.observe(document.body, {
  643. childList: true,
  644. subtree: true
  645. });
  646. });
  647. }
  648.  
  649. function sleep(delay) {
  650. return new Promise((resolve) => setTimeout(resolve, delay));
  651. }
  652.  
  653. function mockInput(text) {
  654. return new Promise((resolve) => {
  655. const textarea = document.querySelector('textarea.translation');
  656. if (!textarea) return;
  657. textarea.value = text;
  658. textarea.dispatchEvent(new Event('input', {
  659. bubbles: true,
  660. cancelable: true,
  661. }));
  662. return resolve(0);
  663. })
  664. }
  665.  
  666. function mockInsert(text) {
  667. const textarea = document.querySelector('textarea.translation');
  668. if (!textarea) return;
  669. const startPos = textarea.selectionStart;
  670. const endPos = textarea.selectionEnd;
  671. const currentText = textarea.value;
  672.  
  673. const before = currentText.slice(0, startPos);
  674. const after = currentText.slice(endPos);
  675.  
  676. mockInput(before + text + after);
  677.  
  678. textarea.selectionStart = startPos + text.length;
  679. textarea.selectionEnd = endPos + text.length;
  680. }
  681.  
  682. function debounce(func, timeout = 300) {
  683. let called = false;
  684. return (...args) => {
  685. if (!called) {
  686. func.apply(this, args);
  687. called = true;
  688. setTimeout(() => {
  689. called = false;
  690. }, timeout);
  691. }
  692. };
  693. }
  694.  
  695. function saveSelection() {
  696. const selection = window.getSelection();
  697. if (selection.rangeCount > 0) {
  698. return selection.getRangeAt(0);
  699. }
  700. return null;
  701. }
  702.  
  703. function restoreSelection(range) {
  704. const selection = window.getSelection();
  705. selection.removeAllRanges();
  706. selection.addRange(range);
  707. }
  708. // #endregion
  709.  
  710. })();