您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
添加了很多小功能,希望以后官方能添加
// ==UserScript== // @name ParaTranz Enhanced // @namespace https://paratranz.cn/users/44232 // @version 0.12.0 // @description 添加了很多小功能,希望以后官方能添加 // @author ooo // @match http*://paratranz.cn/* // @icon https://paratranz.cn/favicon.png // @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 // @grant GM_addStyle // @license MIT // ==/UserScript== /* global Mark mediumZoom GM_addStyle */ (async function() { 'use strict'; GM_addStyle(/* css */` .strings .string :is(.original, .translation) { text-overflow: ellipsis ellipsis !important; } #PZSoverlay { position: absolute !important; pointer-events: none !important; background: transparent !important; -webkit-text-fill-color: transparent !important; overflow-y: hidden !important; resize: none !important; } #PZSoverlay mark { mix-blend-mode: multiply; } .pz-expandable { cursor: pointer; background: linear-gradient(to right, transparent, #aaf6, transparent); borderRadius: 2px; } .add-clickable { cursor: pointer; } `); // #region 主要功能函数 // #region 自动跳过空白页 shouldSkip function shouldSkip() { if (document.querySelector('.string-list .empty-sign') && location.search.match(/(\?|&)page=\d+/g)) { document.querySelector('.pagination button')?.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 = () => {}, markSearchParamsInList = () => {}; 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 translationKeywords = mergeObjects(texts, translations); const contextKeywords = contexts; markSearchParams = () => { markOrigin(originKeywords); markContext(contextKeywords); }; markSearchParamsInList = () => { markListOriginal(originKeywords); markListTranslation(translationKeywords); }; if (Object.values(translationKeywords).filter(v => v).length) { const dropMark = markEditing(translationKeywords); return dropMark; } } let dropLastTextareaMark; const initDropMark = () => dropLastTextareaMark = updMark(); let originMark, contextMark, listOriginalMark, listTranslationMark; 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); const getUnmark = m => document.querySelectorAll(`.string-list .${m}:not(:has( mark:not([data-markjs])))`); listOriginalMark = new Mark(getUnmark('original')); listTranslationMark = new Mark(getUnmark('translation')); markSearchParamsInList(); ensureListMarkInView(); } 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 ensureListMarkInView() { document.querySelectorAll(`.string-list :is(.original, .translation) mark:first-of-type`).forEach(mark => { const container = mark.parentElement; const containerRect = container.getBoundingClientRect(); const markRect = mark.getBoundingClientRect(); const markLeft = markRect.left - containerRect.left; const containerWidth = container.clientWidth; const containerCenter = containerWidth / 2; if (markLeft >= 0 && markLeft <= containerCenter) return; const targetScrollLeft = markLeft - containerCenter + (markRect.width / 2); container.scrollLeft = targetScrollLeft; }); } function mark(target, keywords, options, caseSensitive) { if (!target) return; target.unmark(); 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 markList(markObject, keywords) { mark(markObject, keywords, { acrossElements: false }); } function markListOriginal(keywords) { markList(listOriginalMark, keywords); } function markListTranslation(keywords) { markList(listTranslationMark, 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); } textarea.parentNode.appendChild(overlay); const updOverlay = () => { overlay.innerText = textarea.value; mark(new Mark(overlay), keywords); 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 }); const resizeObserver = new ResizeObserver(updOverlay); resizeObserver.observe(textarea); const cancelOverlay = () => { observer.disconnect(); textarea.removeEventListener('input', updOverlay); resizeObserver.disconnect(); overlay.remove(); } return cancelOverlay; } // #endregion // #region 修复原文排版崩坏和<<>> fixOrigin(originElem) // function fixOriginTwins(originElem) { // fixOriginTerms(originElem); // originElem.innerHTML = originElem.innerHTML // .replaceAll(/<var data-type="(\d)">(<<[^<]*?>)<\/var>>/g, '<var data-type="$1">$2></var>'); // } // function fixOriginTerms(originElem) { // originElem.innerHTML = originElem.innerHTML // .replaceAll('<abbr title="noun.>" data-value=">">></abbr>', '>') // .replaceAll('<abbr="" title="noun.>" data-value=">">>\n>', '>\n</var>') // .replaceAll('<var class="<abbr" title="noun.“”" data-value="“”">"line-break<abbr title="noun.“”" data-value="“”">"</abbr>>\n</var>', '<var class="line-break">\n</var>') // .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 .well'); if (!context) return; // eslint-disable-next-line no-self-assign context.textContent = context.textContent; } // #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 extractText function extractDiff() { document.querySelectorAll('.diff-wrapper:not(.extracted-diff)').forEach(wrapper => { [...wrapper.childNodes].filter(node => node.length >= 200).forEach(node => extractText(node)); wrapper.classList.add('extracted-diff'); }); } function extractHist() { extractDiff(); const nonDiff = [...document.querySelectorAll(':is(.original, .translation):not(.extracted-diff)')]; nonDiff.forEach(node => { if (node.textContent.length >= 200) { // eslint-disable-next-line no-self-assign node.textContent = node.textContent; extractText(node.firstChild, true); } node.classList.add('extracted-diff'); }); } function extractText(node, dual = false) { const text = node.cloneNode(); const expand = document.createElement('span'); expand.classList.add('pz-expandable'); expand.textContent = `${node.textContent.slice(0, 100)} ... ${node.textContent.slice(-100)}`; let time = 0; let isMoving = false; expand.expandEvent = () => expand.replaceWith(text); const start = () => { time = Date.now() isMoving = false; } const end = () => { if (isMoving || Date.now() - time > 500) return; if (dual) { const wrapper = expand.parentElement; const counter = wrapper.previousElementSibling || wrapper.nextElementSibling; counter.firstElementChild?.expandEvent?.(); } expand.expandEvent(); } 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); } // #endregion // #region 点击对比差异绿色文字粘贴其中文本 initDiffClick function initDiffClick() { const addeds = document.querySelectorAll('.diff.added:not(.add-clickable)'); for (const added of addeds) { added.classList.add('add-clickable'); const text = added.textContent.replaceAll('\\n', '\n').replaceAll('↵', '\n'); 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 页内搜索结果高亮 renderSearchResult(originTxt) let inPageKeyword = ''; let resultMark; function renderSearchResult() { const results = document.querySelectorAll('.string-item :is(.original, .translation)'); resultMark = new Mark(results); mark(resultMark, inPageKeyword, { separateWordSearch: true, ignorePunctuation: ":;.,-–—‒_(){}[]!'\"+=".split(""), }, false); } // #endregion // #region 建议对比差异选择模式 newDiff unDiffAll let unDiffAll = () => {}; function newDiff(originTxt) { unDiffAll(); document.querySelectorAll('.original.mb-1 span + a, .pz-diff-cog').forEach(a => a.remove()); const strings = document.querySelectorAll('.original.mb-1 span'); const { $diff } = document.querySelector('main').__vue__; for (const string of strings) { string.oriHTML = string.innerHTML; const strText = string.textContent; 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>'; const diffCog = makeDiffCog(() => { string.innerHTML = diffByMode(); string.isShown = true; }); string.after(' ', showDiff, ' ', diffCog); showDiff.addEventListener('click', function(e) { e.preventDefault(); string.innerHTML = string.isShown ? string.oriHTML : diffByMode(); string.isShown = !string.isShown; }); function diffByMode() { return $diff(strText, originTxt, { mode: localStorage.getItem('pzdiffmode') || 'auto' }); } } unDiffAll = () => { [...strings].filter(string => string.isShown).forEach(string => string.innerHTML = string.oriHTML); }; } function makeDiffCog(onDiffModeChange) { const button = document.createElement('a'); button.href = '#'; button.className = 'hover mr-1 pz-diff-cog'; button.innerHTML = '<i aria-hidden="true" class="far fa-cog"></i>'; // 从 localStorage 获取配置 const savedMode = localStorage.getItem('pzdiffmode') || 'auto'; // 定义选项配置 const modeOptions = [ { value: 'auto', label: '自动' }, { value: 'chars', label: '按字' }, { value: 'words', label: '按词' }, { value: 'sentences', label: '按句' } ]; // 生成单选按钮组 HTML const radioButtons = modeOptions.map(option => ` <div class="custom-control custom-radio"> <input type="radio" class="custom-control-input" value="${option.value}" id="diff${option.value.charAt(0).toUpperCase() + option.value.slice(1)}" name="diffMode" ${savedMode === option.value ? 'checked' : ''}> <label class="custom-control-label" for="diff${option.value.charAt(0).toUpperCase() + option.value.slice(1)}"> ${option.label} </label> </div> `).join(''); // 创建菜单元素 const menu = document.createElement('div'); menu.className = 'popover b-popover bs-popover-bottom'; menu.style.cssText = 'position: absolute; will-change: transform;'; menu.innerHTML = /* html */` <div class="arrow"></div> <div class="popover-body"> <fieldset class="form-group" id="diffModeGroup"> <legend tabindex="-1" class="bv-no-focus-ring col-form-label pt-0">差分模式</legend> <div> <div role="radiogroup" tabindex="-1" class="bv-no-focus-ring"> ${radioButtons} </div> </div> </fieldset> </div> `; // 菜单位置计算 function positionMenu() { const buttonRect = button.getBoundingClientRect(); const menuWidth = menu.offsetWidth; const buttonCenterX = buttonRect.left + buttonRect.width / 2 + window.scrollX; menu.style.top = `${buttonRect.bottom + window.scrollY}px`; menu.style.left = `${buttonCenterX - menuWidth / 2}px`; const arrow = menu.querySelector('.arrow'); const arrowWidth = arrow.offsetWidth; arrow.style.left = `${(menu.offsetWidth - arrowWidth) / 2}px`; } // 状态管理 let isMenuVisible = false; let isMenuLocked = false; let menuTimer = null; // 显示菜单 function showMenu() { if (!document.body.contains(menu)) { document.body.appendChild(menu); } positionMenu(); isMenuVisible = true; button.classList.add('active'); } // 隐藏菜单 function hideMenu() { if (document.body.contains(menu)) { menu.remove(); isMenuVisible = false; isMenuLocked = false; button.classList.remove('active'); } } // 悬浮事件 - 修改这里 button.addEventListener('mouseenter', () => { if (!isMenuLocked) { showMenu(); } clearTimeout(menuTimer); }); // 修改这里:当鼠标离开按钮时,如果菜单可见且没有被锁定,则设置延迟隐藏 button.addEventListener('mouseleave', () => { if (!isMenuLocked && isMenuVisible) { menuTimer = setTimeout(hideMenu, 500); } }); // 菜单的鼠标进入事件 - 新增 menu.addEventListener('mouseenter', () => { if (isMenuVisible) { clearTimeout(menuTimer); // 清除延迟隐藏的计时器 } }); // 菜单的鼠标离开事件 - 新增 menu.addEventListener('mouseleave', () => { if (isMenuVisible && !isMenuLocked) { menuTimer = setTimeout(hideMenu, 500); // 设置延迟隐藏 } }); // 点击事件 button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); isMenuLocked = !isMenuLocked; if (isMenuLocked) { showMenu(); } else { hideMenu(); } }); // 点击外部关闭 document.addEventListener('click', (e) => { if (isMenuVisible && !button.contains(e.target) && !menu.contains(e.target)) { hideMenu(); } }); // 滚动更新位置 window.addEventListener('scroll', () => { if (isMenuVisible) { positionMenu(); } }, true); // 模式变更处理 menu.querySelectorAll('input[name="diffMode"]').forEach(input => { input.addEventListener('change', () => { localStorage.setItem('pzdiffmode', input.value); onDiffModeChange(); }); }); return button; } // #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 getTranslation = (suggest) => suggest.querySelector('.translation').firstChild.textContent; for (const suggest of suggests) { const sim = getSim(suggest); const equalOrigin = [100, 101].includes(sim) || isEqualWithOneCharDifference(originTxt, getOriginalFromSuggest(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, searching = false) { if (!suggests[0]) return; originTxt = normalizeString(originTxt); const getHeaderSpanIfNotSame = searching ? (header, original) => { const headerSpan = document.createElement('span'); header.prepend(headerSpan); if (original === originTxt) { headerSpan.textContent = '匹配率 100%'; return null; } return headerSpan; } : (header) => { return header.querySelector('span'); }; for (const suggest of suggests) { const suggestOri = getOriginalFromSuggest(suggest); const header = suggest.querySelector('header'); let headerSpan; if (!(headerSpan = getHeaderSpanIfNotSame(header, suggestOri))) continue; if (suggestOri.includes(originTxt)) { suggest.parentNode.prepend(suggest); if (headerSpan.textContent.includes('100%') || headerSpan.textContent.includes('101%')) return; headerSpan.textContent = '文本在中'; } } } 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 99.999; // 在文本中 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, searching = false) => { defaultSortedSuggests[+searching] = suggests; }; function recoverDefaultSort() { const parent = document.querySelector('.translation-memory .list:not(.mt-list)'); if (!parent) return; const searching = !!document.querySelector('.search-form'); parent.append(...defaultSortedSuggests[+searching]); } 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; 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 修改历史词条按钮 tweakHist function tweakHist() { addCopySearchBtn(); addReSortBtn(); } // #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; // eslint-disable-next-line no-unused-vars const original = originElem.textContent; // eslint-disable-next-line no-unused-vars 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; } // eslint-disable-next-line no-unused-vars 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; } } // eslint-disable-next-line no-unused-vars 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 originalEle; let lastOriginHTML = ''; let toObserve = document.body; const observer = new MutationObserver((mutations) => { if (shouldSkip()) return; originalEle = document.querySelector('.editor-core .original'); if (!originalEle) return; const originUpded = originalEle.innerHTML !== lastOriginHTML; lastOriginHTML = originalEle.innerHTML; observer.disconnect(); initDiffClick(); extractDiff(); const markAll = () => { // fixOriginTwins(original); removeContextTags(); markSearchParams(); markContext(originalEle.textContent); }; if (stringPageTurned) { if (!originUpded) { connectObserve(); return; } console.debug('framework loaded'); initDropMark(); initMarkJS(); tweakHist(); watchContextBtn(); markAll(); tweakButtons(); stringPageTurned = false; connectObserve(); return; } if (originUpded) { console.debug('origin upded'); cancelSearchResult(); markAll(); if (!document.getElementById('PZpaste')) tweakButtons(); // 防止他人占用按钮消失 } mutations = mutations.filter(({ addedNodes, removedNodes }) => !( [addedNodes[0]?.nodeName, removedNodes[0]?.nodeName].includes('#comment') || (addedNodes.length === 1 && (addedNodes[0].classList?.contains('square-image') || addedNodes[0].classList?.contains('fa-copy') || addedNodes[0].href === 'javascript:')) || (removedNodes.length === 1 && removedNodes[0].classList?.contains('fa-copy')) )); 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; } if (node.classList?.contains('search-form')) { console.debug('search expanded'); renderSearchResult(originalEle.textContent); newDiff(originalEle.textContent); continue; } if (node.classList?.contains('translation-memory')) { console.debug('suggests layout switched'); tweakHist(); } // if (node.classList?.contains('results')) { // console.debug('terms loaded'); // fixOriginTerms(original); // } // if (node.classList?.contains('ghost-textarea')) { // fixOriginTwins(node); // 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'); const searchInput = document.querySelector('.search-form input'); if (searchInput) { inPageKeyword = searchInput.value; console.debug('search loaded: ', inPageKeyword); renderSearchResult(originalEle.textContent); } findTextWithin(suggests, originalEle.textContent, !!searchInput); getDefaultSorted(suggests, !!searchInput); autoFill100(suggests, originalEle.textContent); reSortSuggestsByMem(suggests); newDiff(originalEle.textContent); continue; } else if (node.classList?.contains('search-form')) { console.debug('search collapsed'); const suggests = document.querySelectorAll('.string-item'); resultMark?.unmark?.(); findTextWithin(suggests, originalEle.textContent); reSortSuggestsByMem(suggests); newDiff(originalEle.textContent); } // if (node.matches?.('.list-group.tags')) { // tagSelectController.abort(); // continue; // } } // if (addedNodes[0]?.parentElement?.classList.contains('ghost-textarea')) { // fixOriginTwins(document.querySelector('.ghost-textarea')); // } } 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(); extractHist(); 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 normalizeString(str) { if (!str) return ''; return str .replace(/[,.;'"-]/g, '') .replace(/\s+/g, '') .toLowerCase(); } function getOriginalFromSuggest(suggest) { return normalizeString(suggest.querySelector('.original')?.firstChild.textContent); } // #endregion })();