NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name ParaTranz diff // @namespace https://paratranz.cn/users/44232 // @version 0.11.4 // @description ParaTranz enhanced // @author ooo // @match http*://paratranz.cn/* // @require https://cdnjs.cloudflare.com/ajax/libs/medium-zoom/1.1.0/medium-zoom.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js // @license MIT // ==/UserScript== (async function() { 'use strict'; // #region 主要功能函数 // #region 自动跳过空白页 shouldSkip function shouldSkip() { if (document.querySelector('.string-list .empty-sign') && location.search.match(/(\?|&)page=\d+/g)) { document.querySelector('.pagination .page-item a')?.click(); return true; } } // #endregion // #region 添加快捷键 addHotkeys function addHotkeys() { document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.key === 'V') { e.preventDefault(); mockInput(document.querySelector('.editor-core .original')?.textContent); } }); } // #endregion // #region 更多搜索高亮 initDropMark markSearchParams initMarkJS watchContextBtn let markSearchParams = () => {}; const mergeObjects = (obj1, obj2) => { const merged = {}; for (const key of Object.keys(obj1)) { merged[key] = [...obj1[key], ...obj2[key]]; } return merged; }; function updMark() { const params = new URLSearchParams(location.search); const getParams = (type) => { return { contains: [...params.getAll(type)], startsWith: [...params.getAll(`${type}^`)], endsWith: [...params.getAll(`${type}$`)], match: [...params.getAll(`${type}~`)] }; }; const texts = getParams('text'); const originals = getParams('original'); const translations = getParams('translation'); const contexts = getParams('context'); const originKeywords = mergeObjects(texts, originals); const editingKeywords = mergeObjects(texts, translations); const contextKeywords = contexts; markSearchParams = () => { markOrigin(originKeywords); markContext(contextKeywords); } if (Object.values(editingKeywords).filter(v => v).length) { const dropMark = markEditing(editingKeywords); return dropMark; } } let dropLastTextareaMark; const initDropMark = () => dropLastTextareaMark = updMark(); let originMark; let contextMark; function initMarkJS() { const original = document.querySelector('.editor-core .original'); originMark = new Mark(original); original.addEventListener('click', (e) => { if (e.target.tagName === 'MARK') { const originalElement = e.target.parentElement; originalElement.click(); } }); const context = document.querySelector('.context'); if (context) contextMark = new Mark(context); } function watchContextBtn() { const btn = document.querySelector('.float-right a'); if (!btn) return; btn.addEventListener('click', () => { const context = document.querySelector('.context'); if (!context) return; removeContextTags(); const original = document.querySelector('.editor-core .original').textContent; contextMark = new Mark(context); markContext(original); }); } function mark(target, keywords, options) { if (!target) return; target.unmark(); const caseSensitive = !document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked; const flags = caseSensitive ? 'g' : 'ig'; const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); let patterns; if (typeof keywords === 'string') { patterns = escapeRegExp(keywords); } else { const { contains, startsWith, endsWith, match } = keywords; patterns = [ ...contains.map(keyword => `(${escapeRegExp(keyword)})`), ...startsWith.map(keyword => `^(${escapeRegExp(keyword)})`), ...endsWith.map(keyword => `(${escapeRegExp(keyword)})$`), match ].filter(p => p.length).join('|'); } if (patterns) { const regex = new RegExp(patterns, flags); target.markRegExp(regex, { acrossElements: true, separateWordSearch: false, ...options }); } } function markOrigin(keywords) { mark(originMark, keywords); } function markContext(originTxt) { mark(contextMark, originTxt, { className: 'mark'}); } function markEditing(keywords) { let textarea = document.querySelector('textarea.translation'); if (!textarea) return; const lastOverlay = document.getElementById('PZSoverlay'); if (lastOverlay) return; const overlay = document.createElement('div'); overlay.id = 'PZSoverlay'; overlay.className = textarea.className; const textareaStyle = window.getComputedStyle(textarea); for (let i = 0; i < textareaStyle.length; i++) { const property = textareaStyle[i]; overlay.style[property] = textareaStyle.getPropertyValue(property); } overlay.style.position = 'absolute'; overlay.style.pointerEvents = 'none'; overlay.style.setProperty('background', 'transparent', 'important'); overlay.style['-webkit-text-fill-color'] = 'transparent'; overlay.style.overflowY = 'hidden'; overlay.style.resize = 'none'; textarea.parentNode.appendChild(overlay); const updOverlay = () => { overlay.innerText = textarea.value; const fillColor = window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color'); mark(new Mark(overlay), keywords, { each: (m) => { m.style['-webkit-text-fill-color'] = fillColor; m.style.opacity = .5; } }); overlay.style.top = textarea.offsetTop + 'px'; overlay.style.left = textarea.offsetLeft + 'px'; overlay.style.width = textarea.offsetWidth + 'px'; overlay.style.height = textarea.offsetHeight + 'px'; }; updOverlay(); textarea.addEventListener('input', updOverlay); const observer = new MutationObserver(updOverlay); observer.observe(textarea, { attributes: true }); window.addEventListener('resize', updOverlay); const cancelOverlay = () => { observer.disconnect(); textarea.removeEventListener('input', updOverlay); window.removeEventListener('resize', updOverlay); overlay.remove(); } return cancelOverlay; } // #endregion // #region 修复原文排版崩坏和<<>> fixOrigin(originElem) function fixOrigin(originElem) { originElem.innerHTML = originElem.innerHTML .replaceAll('<abbr title="noun.>" data-value=">">></abbr>', '>') .replaceAll(/<var>(<<[^<]*?>)<\/var>>/g, '<var>$1></var>') .replaceAll('<i class="lf" <abbr="" title="noun.>" data-value=">">>>', '') .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>>>', '') .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>>', ''); } // #endregion // #region 还原上下文HTML源码 removeContextTags function removeContextTags() { const context = document.querySelector('.context'); if (!context) return; context.innerHTML = context.innerHTML.replace(/<a.*?>(.*?)<\/a>/g, '$1').replace(/<(\/?)(li|b|i|u|h\d|span)>/g, '<$1$2>'); } // #endregion // #region 修复 Ctrl 唤起菜单的<<>> fixTagSelect const insertTag = debounce(async (tag) => { const textarea = document.querySelector('textarea.translation'); const startPos = textarea.selectionStart; const endPos = textarea.selectionEnd; const currentText = textarea.value; const before = currentText.slice(0, startPos); const after = currentText.slice(endPos); await mockInput(before.slice(0, Math.max(before.length - tag.length + 1, 0)) + tag + after.slice(0, -2)); // -2 去除\n textarea.selectionStart = startPos + 1; textarea.selectionEnd = endPos + 1; }) let activeTag = null; let modifiedTags = []; const tagSelectController = new AbortController(); const { tagSelectSignal } = tagSelectController; function tagSelectHandler(e) { if (['ArrowUp', 'ArrowDown'].includes(e.key)) { activeTag &&= document.querySelector('.list-group-item.tag.active'); } if (e.key === 'Enter') { if (!activeTag) return; if (!modifiedTags.includes(activeTag)) return; e.preventDefault(); insertTag(activeTag?.textContent); activeTag = null; } } function updFixedTags() { const tags = document.querySelectorAll('.list-group-item.tag'); activeTag = document.querySelector('.list-group-item.tag.active'); modifiedTags = []; for (const tag of tags) { tag.innerHTML = tag.innerHTML.trim(); if (tag.innerHTML.startsWith('<<') && !tag.innerHTML.endsWith('>>')) { tag.innerHTML += '>'; modifiedTags.push(tag); } } document.addEventListener('keyup', tagSelectHandler, { tagSelectSignal }); } // #endregion // #region 将填充原文移到右边,增加填充原文并保存 tweakButtons function tweakButtons() { const copyButton = document.querySelector('button.btn-secondary:has(.fa-clone)'); const rightButtons = document.querySelector('.right .btn-group'); if (rightButtons) { if (copyButton) { rightButtons.insertBefore(copyButton, rightButtons.firstChild); } if (document.querySelector('#PZpaste')) return; const pasteSave = document.createElement('button'); rightButtons.appendChild(pasteSave); pasteSave.id = 'PZpaste'; pasteSave.type = 'button'; pasteSave.classList.add('btn', 'btn-secondary'); pasteSave.title = '填充原文并保存'; pasteSave.innerHTML = '<i aria-hidden="true" class="far fa-save"></i>'; pasteSave.addEventListener('click', async () => { await mockInput(document.querySelector('.editor-core .original')?.textContent); document.querySelector('.right .btn-primary')?.click(); }); } } // #endregion // #region 缩略对比差异中过长无差异文本 extractDiff function extractDiff() { document.querySelectorAll('.diff-wrapper:not(.PZedited)').forEach(wrapper => { wrapper.childNodes.forEach(node => { if (node.nodeType !== Node.TEXT_NODE || node.length < 200) return; const text = node.cloneNode(); const expand = document.createElement('span'); expand.textContent = `${node.textContent.slice(0, 100)} ... ${node.textContent.slice(-100)}`; expand.style.cursor = 'pointer'; expand.style.background = 'linear-gradient(to right, transparent, #aaf6, transparent)'; expand.style.borderRadius = '2px'; let time = 0; let isMoving = false; const start = () => { time = Date.now() isMoving = false; } const end = () => { if (isMoving || Date.now() - time > 500) return; expand.replaceWith(text); } expand.addEventListener('mousedown', start); expand.addEventListener('mouseup', end); expand.addEventListener('mouseleave', () => time = 0); expand.addEventListener('touchstart', start); expand.addEventListener('touchend', end); expand.addEventListener('touchcancel', () => time = 0); expand.addEventListener('touchmove', () => isMoving = true); node.replaceWith(expand); }); wrapper.classList.add('PZedited'); }); } // #endregion // #region 点击对比差异绿色文字粘贴其中文本 initDiffClick function initDiffClick() { const addeds = document.querySelectorAll('.diff.added:not(.PZPedited)'); for (const added of addeds) { added.classList.add('PZPedited'); const text = added.textContent.replaceAll('\\n', '\n'); added.style.cursor = 'pointer'; added.addEventListener('click', () => { mockInsert(text); }); } } // #endregion // #region 快速搜索原文 addCopySearchBtn async function addCopySearchBtn() { if (document.querySelector('#PZsch')) return; const originSch = document.querySelector('.btn-sm'); if (!originSch) return; originSch.insertAdjacentHTML('beforebegin', '<button id="PZsch" type="button" class="btn btn-secondary btn-sm"><i aria-hidden="true" class="far fa-paste"></i></button>'); const newSch = document.querySelector('#PZsch'); newSch.addEventListener('click', async () => { const original = document.querySelector('.editor-core .original')?.textContent; let input = document.querySelector('.search-form input[type=search]'); if (!input) { await (() => new Promise(resolve => resolve(originSch.click())))(); input = document.querySelector('.search-form input[type=search]'); } const submit = document.querySelector('.search-form button'); await (() => new Promise(resolve => { input.value = original; input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true, })); resolve(); }))(); submit.click(); }); } // #endregion // #region 进入下一条时关闭搜索结果 cancelSearchResult function cancelSearchResult() { const input = document.querySelector('.search-form input[type=search]'); if (input) document.querySelectorAll('.btn-sm')[1]?.click(); } // #endregion // #region 搜索结果对比差异 initSearchResultDiff(originTxt) function initSearchResultDiff(originTxt) { const strings = document.querySelectorAll('.original.mb-1 span:not(:has(+a)'); if (!strings[0]) return; const { $diff } = document.querySelector('main').__vue__; for (const string of strings) { const strHTML = string.innerHTML; const showDiff = document.createElement('a'); showDiff.title = '查看差异'; showDiff.href = '#'; showDiff.target = '_self'; showDiff.classList.add('small'); showDiff.innerHTML = '<i aria-hidden="true" class="far fa-right-left-large"></i>'; string.after(' ', showDiff); showDiff.addEventListener('click', function(e) { e.preventDefault(); string.innerHTML = this.isShown ? strHTML : $diff(string.textContent, originTxt); this.isShown = !this.isShown; }); } } // #endregion // #region 高级搜索空格变+修复 fixAdvSch function fixAdvSch() { const inputs = document.querySelectorAll('#advancedSearch table input'); if (!inputs[0]) return; const params = new URLSearchParams(location.search); const values = [...params.entries()].filter(([key, _]) => /(text|original|translation).?/.test(key)).map(([_, value]) => value.replaceAll(' ', '+')); for (const input of inputs) { if (values.includes(input.value)) { input.value = input.value.replaceAll('+', ' '); input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true, })); } } } // #endregion // #region 自动保存全部相同词条 autoSaveAll const autoSave = localStorage.getItem('pzdiffautosave'); function autoSaveAll() { const button = document.querySelector('.modal-dialog .btn-primary'); if (autoSave && button.textContent === '保存全部') button.click(); } // #endregion // #region 自动填充100%相似译文 autoFill100(suggests, originTxt) function autoFill100(suggests, originTxt) { if (!suggests[0]) return; const getSim = (suggest) => +suggest.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1); const getOriginal = (suggest) => normalizeString(suggest.querySelector('.original')?.firstChild.textContent); const getTranslation = (suggest) => suggest.querySelector('.translation').firstChild.textContent; for (const suggest of suggests) { const sim = getSim(suggest); const equalOrigin = [100, 101].includes(sim) || isEqualWithOneCharDifference(originTxt, getOriginal(suggest)); if (equalOrigin) { mockInput(getTranslation(suggest)); break; } } } function isEqualWithOneCharDifference(str1, str2) { if (str1 === str2) return true; if (Math.abs(str1.length - str2.length) > 1) return false; let differences = 0; const len1 = str1.length; const len2 = str2.length; let i = 0, j = 0; while (i < len1 && j < len2) { if (str1[i] !== str2[j]) { differences++; if (differences > 1) return false; if (len1 > len2) i++; else if (len2 > len1) j++; else { i++; j++; } } else { i++; j++; } } if (i < len1 || j < len2) differences++; return differences <= 1; } // #endregion // #region 重新排序历史词条 findTextWithin(suggests, originTxt) getDefaultSorted addReSortBtn function findTextWithin(suggests, originTxt) { if (!suggests[0]) return; originTxt = normalizeString(originTxt); const getOriginal = (suggest) => normalizeString(suggest.querySelector('.original')?.firstChild.textContent); for (const suggest of suggests) { if (getOriginal(suggest).includes(originTxt)) { suggest.parentNode.prepend(suggest); const header = suggest.querySelector('header'); let headerSpan = header.querySelector('span'); if (!headerSpan) { headerSpan = document.createElement('span'); header.prepend(headerSpan); } if (headerSpan.textContent.includes('100%') || headerSpan.textContent.includes('101%')) break; headerSpan.textContent = '文本在中'; break; } } } const reSortSuggests = (compareFn) => (suggests) => { if (!suggests[0]) return; const sorted = [...suggests].sort(compareFn); const parent = suggests[0].parentNode; const frag = document.createDocumentFragment(); frag.append(...sorted); parent.innerHTML = ''; parent.appendChild(frag); }; const reSortSuggestsBySim = reSortSuggests((a, b) => { const getSim = (suggest) => { const simContainer = suggest.querySelector('header span'); if (!simContainer) return 102; // 机器翻译参考 const sim = +simContainer.textContent.split('\n')?.[2]?.trim().slice(0, -1); if (!sim) return 102; // 在文本中 return sim; } return getSim(b) - getSim(a); }); const reSortSuggestsByTime = reSortSuggests((a, b) => { const getTimestamp = (suggest) => { const time = suggest.querySelector('time')?.dateTime; if (!time) return Infinity; return +new Date(time); } return getTimestamp(b) - getTimestamp(a); }); const reSortSuggestsByMem = (suggests) => { const sortType = localStorage.getItem('pzdiffsort') || 'sim'; if (sortType === 'sim') { reSortSuggestsBySim(suggests); } else if (sortType === 'time') { reSortSuggestsByTime(suggests); } }; let defaultSortedSuggests = []; const getDefaultSorted = (suggests) => { defaultSortedSuggests = suggests; }; function recoverDefaultSort() { const parent = document.querySelector('.translation-memory .list'); if (!parent) return; parent.append(...defaultSortedSuggests); } function addReSortBtn() { if (document.querySelector('.pzdiffsort')) return; const btn = document.createElement('a'); btn.href = 'javascript:'; btn.className = 'pzdiffsort'; const icon = type => { const icon = document.createElement('i'); icon.classList.add('far', `fa-${type}`); icon.ariaHidden = true; icon.style.cursor = 'pointer'; return icon; } const simBtn = btn.cloneNode(); simBtn.title = '按相似度排序'; simBtn.append(icon('percentage')); simBtn.addEventListener('click', () => { const suggests = document.querySelectorAll('.string-item'); reSortSuggestsByTime(suggests); localStorage.setItem('pzdiffsort', 'time'); simBtn.replaceWith(timeBtn); }); const timeBtn = btn.cloneNode(); timeBtn.title = '按时间排序'; timeBtn.append(icon('history')); timeBtn.addEventListener('click', () => { recoverDefaultSort(); localStorage.setItem('pzdiffsort', 'default'); timeBtn.replaceWith(defaultBtn); }); const defaultBtn = btn.cloneNode(); defaultBtn.title = '默认排序'; defaultBtn.append(icon('sort-amount-down')); defaultBtn.addEventListener('click', () => { const suggests = document.querySelectorAll('.string-item'); reSortSuggestsBySim(suggests); localStorage.setItem('pzdiffsort', 'sim'); defaultBtn.replaceWith(simBtn); }); const sortType = localStorage.getItem('pzdiffsort') || 'sim'; const initBtn = { sim: simBtn, time: timeBtn, default: defaultBtn, }; document.querySelector('.translation-memory .col-auto').after(initBtn[sortType]); } // #endregion // #region 初始化自动编辑 initAuto async function initAuto() { const avatars = await waitForElems('.nav-item.user-info'); avatars.forEach(async (avatar) => { let harvesting = false; let translationPattern, skipPattern, userTime; avatar.insertAdjacentHTML('afterend', `<li class="nav-item"><a href="javascript:;" target="_self" class="PZpp nav-link" role="button">PP收割机</a></li>`); document.querySelectorAll('.PZpp').forEach(btn => btn.addEventListener('click', async (e) => { if (location.pathname.split('/')[3] !== 'strings') return; harvesting = !harvesting; if (harvesting) { e.target.style.color = '#dc3545'; translationPattern = prompt(`请确认译文模板代码,字符串用'包裹;常用代码: original(原文) oldTrans(现有译文) suggest(第1条翻译建议) suggestSim(上者匹配度,最大100)`, 'original'); if (translationPattern === null) return cancel(); skipPattern = prompt(`请确认跳过条件代码,多个条件用逻辑运算符相连;常用代码: original.match(/^(\s|\n|<<.*?>>|<.*?>)*/gm)[0] !== original(跳过并非只包含标签) oldTrans(现有译文) suggest(第1条翻译建议) suggestSim(上者匹配度,最大100) context(上下文内容)`, ''); if (skipPattern === null) return cancel(); if (skipPattern === '') skipPattern = 'false'; userTime = prompt('请确认生成译文后等待时间(单位:ms)', '500'); if (userTime === null) return cancel(); function cancel() { harvesting = false; e.target.style.color = ''; } } else { e.target.style.color = ''; return; } const hideAlert = document.createElement('style'); document.head.appendChild(hideAlert); hideAlert.innerHTML = '.alert-success.alert-global{display:none}'; const checkboxs = [...document.querySelectorAll('.right .custom-checkbox')].slice(0, 2); const checkboxValues = checkboxs.map(e => e.__vue__.$data.localChecked); checkboxs.forEach(e => e.__vue__.$data.localChecked = true); const print = { waiting: () => console.log('%cWAITING...', 'background: #007BFF; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'), skip: () => console.log('%cSKIP', 'background: #FFC107; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'), click: () => console.log('%cCLICK', 'background: #20C997; color: #282828; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'), end: () => console.log('%cTHE END', 'background: #DE065B; color: white; font-weight: 900; padding: 0 5px; font-size: 12px; border-radius: 2px'), } const INTERVAL = 100; let interval = INTERVAL; let lastInfo = null; function prepareWait() { print.waiting(); interval = INTERVAL; lastInfo = null; return true; } function skipOrFin(originElem, nextButton) { if (nextString(nextButton)) return false; print.skip(); interval = 50; lastInfo = [ originElem, location.search.match(/(?<=(\?|&)page=)\d+/g)?.[0] ?? 1 ]; return true; } function nextString(button) { if (button.disabled) { print.end(); harvesting = false; e.target.style.color = ''; return true; } button.click(); return false; } try { while (true) { await sleep(interval); if (lastInfo) { const [ lastOrigin, lastPage ] = lastInfo; // 已点击翻页,但原文未发生改变 const skipWaiting = (location.search.match(/(?<=(\?|&)page=)\d+/g)?.[0] ?? 1) !== lastPage && document.querySelector('.editor-core .original') === lastOrigin; if (skipWaiting && prepareWait()) continue; } const originElem = document.querySelector('.editor-core .original'); if (!originElem && prepareWait()) continue; const nextButton = document.querySelectorAll('.navigation .btn-secondary')[1]; if (!nextButton && prepareWait()) continue; const original = originElem.textContent; const oldTrans = document.querySelector('textarea.translation').value; let suggest = null, suggestSim = 0; if (translationPattern.includes('suggest') || skipPattern.includes('suggest')) { const suggestEle = (await waitForElems('.translation-memory .string-item .translation, .empty-sign'))[0]; if (suggestEle.classList.contains('empty-sign')) { if (skipOrFin(originElem, nextButton)) continue; else break; } suggest = suggestEle.textContent; suggestSim = +suggestEle.querySelector('header span')?.textContent.split('\n')?.[2]?.trim().slice(0, -1); if ((translationPattern.includes('suggestSim') || skipPattern.includes('suggestSim')) && isNaN(suggestSim)) { if (skipOrFin(originElem, nextButton)) continue; else break; } } const context = document.querySelector('.context')?.textContent; if (eval(skipPattern)) { if (skipOrFin(originElem, nextButton)) continue; else break; } const translation = eval(translationPattern); if (!translation && prepareWait()) continue; await mockInput(translation); await sleep(userTime); if (!harvesting) break; // 放在等待后,以便在等待间隔点击取消 const translateButton = document.querySelector('.right .btn-primary'); if (!translateButton) { if (skipOrFin(originElem, nextButton)) continue; else break; } else { translateButton.click(); print.click(); interval = INTERVAL; lastInfo = null; continue; } } } catch (e) { console.error(e); alert('出错了!'); } finally { hideAlert.remove(); checkboxs.forEach((e, i) => { e.__vue__.$data.localChecked = checkboxValues[i] }); } })); }); } // #endregion // #endregion // #region 函数调用逻辑 addHotkeys(); initAuto(); let stringPageTurned = true; async function actByPath(path) { if (path.split('/').pop() === 'strings') { let original; let lastOriginHTML = ''; let toObserve = document.body; const observer = new MutationObserver((mutations) => { fixAdvSch(); if (shouldSkip()) return; original = document.querySelector('.editor-core .original'); if (!original) return; const originUpded = original.innerHTML !== lastOriginHTML; lastOriginHTML = original.innerHTML; observer.disconnect(); initDiffClick(); extractDiff(); const markAll = () => { fixOrigin(original); removeContextTags(); markSearchParams(); markContext(original.textContent); }; if (stringPageTurned) { if (!originUpded) { connectObserve(); return; } console.debug('framework loaded'); initDropMark(); initMarkJS(); addCopySearchBtn(); addReSortBtn(); watchContextBtn(); markAll(); stringPageTurned = false; connectObserve(); return; } if (originUpded) { console.debug('origin upded'); cancelSearchResult(); markAll(); tweakButtons(); // 防止他人占用按钮消失 } for (const mutation of mutations) { const { addedNodes, removedNodes } = mutation; // console.debug({ addedNodes, removedNodes }); if (addedNodes.length === 1) { const node = addedNodes[0]; if (node.matches?.('.list-group.tags')) { updFixedTags(); continue; } if (node.matches?.('.string-item a.small')) { node.remove(); continue; } if (node.matches?.('.modal-backdrop')) { autoSaveAll(); continue; } } else if (removedNodes.length === 1) { const node = removedNodes[0]; if (mutation.target.classList?.contains('translation-memory') && node.classList?.contains('loading')) { console.debug('suggests loaded'); const suggests = document.querySelectorAll('.string-item'); findTextWithin(suggests, original.textContent); getDefaultSorted(suggests); initSearchResultDiff(original.textContent); autoFill100(suggests, original.textContent); reSortSuggestsByMem(suggests); continue; } if (node.matches?.('.list-group.tags')) tagSelectController.abort(); } } connectObserve(); }); connectObserve(); function connectObserve() { observer.observe(toObserve, { childList: true, subtree: true, }); } return observer; } else if (path.split('/').at(-2) === 'issues') { waitForElems('.text-content p img').then((imgs) => imgs.forEach(mediumZoom)); } else if (path.split('/').pop() === 'history') { let observer = new MutationObserver(() => { observer.disconnect(); extractDiff(); connectObserve(); }); connectObserve(); function connectObserve() { observer.observe(document.body, { childList: true, subtree: true, }); } return observer; } } let cancelAct = await actByPath(location.pathname); (await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async (to, from) => { dropLastTextareaMark?.(); if (JSON.stringify(to.query) !== JSON.stringify(from.query)) { console.debug('query changed'); if (to.path.split('/').pop() === 'strings') { stringPageTurned = true; } } if (to.path === from.path) return; tagSelectController.abort(); cancelAct?.disconnect(); console.debug('path changed'); cancelAct = await actByPath(to.path); }); // #endregion // #region 通用工具函数 function waitForElems(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelectorAll(selector)); } const observer = new MutationObserver(() => { if (document.querySelector(selector)) { resolve(document.querySelectorAll(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } function mockInput(text) { return new Promise((resolve) => { const textarea = document.querySelector('textarea.translation'); if (!textarea) return; textarea.value = text; textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true, })); return resolve(0); }) } function mockInsert(text) { const textarea = document.querySelector('textarea.translation'); if (!textarea) return; const startPos = textarea.selectionStart; const endPos = textarea.selectionEnd; const currentText = textarea.value; const before = currentText.slice(0, startPos); const after = currentText.slice(endPos); mockInput(before + text + after); textarea.selectionStart = startPos + text.length; textarea.selectionEnd = endPos + text.length; textarea.focus(); } function debounce(func, timeout = 300) { let called = false; return (...args) => { if (!called) { func.apply(this, args); called = true; setTimeout(() => called = false, timeout); } }; } function normalizeString(str) { if (!str) return ''; return str .replace(/[,.;'"-]/g, '') .replace(/\s+/g, '') .toLowerCase(); } // #endregion })();