ParaTranz diff

ParaTranz enhanced

当前为 2025-01-03 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ParaTranz diff
// @namespace    https://paratranz.cn/users/44232
// @version      0.9.0
// @description  ParaTranz enhanced
// @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
// @license      MIT
// ==/UserScript==

(async function() {
    'use strict';

    // #region 主要功能

    // #region 自动跳过空白页 initSkip TODO 疑似失效
    function initSkip() {
        if (document.querySelector('.string-list .empty-sign') &&
            location.search.match(/(\?|&)page=\d+/g)) {
            document.querySelector('.pagination .page-item a')?.click();
        }
    }
    // #endregion

    // #region 添加快捷键 addHotkeys
    function addHotkeys() {
        document.addEventListener('keydown', (event) => {
            if (event.ctrlKey && event.shiftKey && event.key === 'V') {
                event.preventDefault();
                mockInput(document.querySelector('.editor-core .original')?.textContent);
            }
        });
    }
    // #endregion

    // #region 更多搜索高亮 markSearchParams
    let markSearchParams = () => {};
    function updMark() {
        const params = new URLSearchParams(location.search);
        const text = params.get('text');
        const original = params.get('original');
        const translation = params.get('translation');
        const context = params.get('context');

        if (text) {
            markSearchParams = (isOriginUpd) => {
                if (isOriginUpd) markNorm('.editor-core .original', text);
                return markEditing(text);
            }
        } else if (original) {
            markSearchParams = (isOriginUpd) => {
                if (isOriginUpd) markNorm('.editor-core .original', original);
            }
        } else if (translation) {
            markSearchParams = () => {
                return markEditing(translation);
            }
        } else if (context) {
            markSearchParams = () => {
                markNorm('.context', context);
            }
        } else {
            markSearchParams = () => {};
        }
    }
    let dropLastMark = updMark();

    function markNorm(selector, toMark) {
        const container = document.querySelector(selector);
        if (!container) return;

        let toMarkPattern = toMark;
        if (document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked) { // 忽略大小写
            toMarkPattern = new RegExp(`(${toMark.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig');
        }

        const HTML = container.innerHTML;
        const currentMark = `<mark class="PZS">${toMark}</mark>`;
        if (HTML.includes(currentMark)) return;
        container.innerHTML = HTML.replaceAll('<mark class="PZS">', '').replace(/(?<=>|^)([^<]*?)(?=<|$)/g, (match) => {
            if (typeof toMarkPattern === 'string') {
                return match.replaceAll(toMarkPattern, currentMark);
            } else {
                return match.replace(toMarkPattern, '<mark class="PZS">$1</mark>');
            }
        });
    }

    function markEditing(toMark) {
        const 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['overflow-y'] = 'hidden';
        overlay.style.resize = 'none';

        textarea.parentNode.appendChild(overlay);

        const updOverlay = () => {
            let toMarkPattern = toMark.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('\\n', '<br>');
            if (document.querySelector('.sidebar .custom-checkbox')?.__vue__.$data.localChecked) { // 忽略大小写
                toMarkPattern = new RegExp(`(${toMark.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig');
            }
            overlay.innerText = textarea.value;
            if (typeof toMarkPattern === 'string') {
                overlay.innerHTML = overlay.innerHTML.replaceAll(toMarkPattern, `<mark class="PZS" style="-webkit-text-fill-color:${
                    window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color')
                };opacity:.5">${toMarkPattern}</mark>`);
            } else {
                overlay.innerHTML = overlay.innerHTML.replace(toMarkPattern, `<mark class="PZS" style="-webkit-text-fill-color:${
                    window.getComputedStyle(textarea).getPropertyValue('-webkit-text-fill-color')
                };opacity:.5">$1</mark>`);
            }
            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, childList: true, subtree: true });

        window.addEventListener('resize', updOverlay);

        const cancelOverlay = () => {
            observer.disconnect();
            textarea.removeEventListener('input', updOverlay);
            window.removeEventListener('resize', updOverlay);
        }
        return cancelOverlay;
    }
    // #endregion

    // #region 高亮上下文 markContext(originTxt)
    function markContext(originTxt) {
        const contextBox = document.querySelector('.context');
        if (!contextBox) return;

        const context = contextBox.innerHTML.replaceAll(/<a.*?>(.*?)<\/a>/g, '$1').replaceAll(/<(\/?)(li|b|u|h\d|span)>/g, '&lt;$1$2&gt;');
        originTxt = originTxt.replaceAll('<br>', '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
        if (contextBox.querySelector('#PZmark')?.textContent === originTxt) return;
        contextBox.innerHTML = context.replace('<mark id="PZmark" class="mark">', '').replace(originTxt, `<mark id="PZmark" class="mark">${originTxt}</mark>`);
    }
    // #endregion

    // #region 修复原文排版崩坏和<<>> fixOrigin(originElem)
    function fixOrigin(originElem) {
        originElem.innerHTML = originElem.innerHTML
        .replaceAll('<abbr title="noun.>" data-value=">">&gt;</abbr>', '&gt;')
        .replaceAll(/<var>(&lt;&lt;[^<]*?&gt;)<\/var>&gt;/g, '<var class="PZvar">$1&gt;</var>')
        .replaceAll('<i class="lf" <abbr="" title="noun.>" data-value=">">&gt;&gt;', '')
        .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;&gt;', '')
        .replaceAll('<i class="<abbr" title="noun.“”" data-value="“”">"lf<abbr title="noun.“”" data-value="“”">"</abbr>&gt;', '');
    }
    // #endregion

    // #region 修复 Ctrl 唤起菜单的<<>> fixTagSelect
    const insertTag = debounce((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);

        mockInput(before.slice(0, Math.max(before.length - tag.length + 1, 0)) + tag + after);

        textarea.selectionStart = startPos + 1;
        textarea.selectionEnd = endPos + 1;
    })

    let activeTag = null;
    let modifiedTags = [];
    const tagSelectController = new AbortController();
    const { tagSelectSignal } = tagSelectController;

    function tagSelectHandler(event) {
        if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
            activeTag &&= document.querySelector('.list-group-item.tag.active');
        }
        if (event.key === 'Enter') {
            if (!activeTag) return;
            if (!modifiedTags.includes(activeTag)) return;
            event.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('&lt;&lt;') && !tag.innerHTML.endsWith('&gt;&gt;')) {
                tag.innerHTML += '&gt;';
                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 点击对比差异绿色文字粘贴其中文本 clickDiff
    function clickDiff() {
        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 快速搜索原文 copySearch
    async function copySearch() {
        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-clone"></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.mt-3 input[type=search]');
            if (!input) {
                await (() => new Promise(resolve => resolve(originSch.click())))();
                input = document.querySelector('.search-form.mt-3 input[type=search]');
            }
            input.value = original;
            input.dispatchEvent(new Event('input', {
                bubbles: true,
                cancelable: true,
            }));
        });
    }
    // #endregion

    // #region 搜索结果对比差异 searchDiff
    function searchDiff() {
        const strings = document.querySelectorAll('.original.mb-1 span:not(:has(+a)');
        if (!strings[0]) return;

        const original = document.querySelector('.editor-core .original')?.textContent;
        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() {
                string.innerHTML = this.isShown ? strHTML : $diff(string.textContent, original);
                this.isShown = !this.isShown;
            })
        }
    }

    // #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,
                }));
            }
        }
    }

    // #region 自动保存全部相同词条 autoSaveAll
    const autoSave = localStorage.getItem('pzdiffautosave');
    function autoSaveAll() {
        const button = document.querySelector('.modal-dialog .btn-primary');
        if (autoSave && button.textContent === '保存全部') button.click();
    }

    // #region 自动填充100%相似译文 autoFill100
    function autoFill100() {
        const suggests = document.querySelectorAll('.string-item');
        const getSim = (suggest) => +suggest.querySelector('header span span').textContent.split('\n')?.[2].trim().slice(0, -1);
        const getTranslation = (suggest) => suggest.querySelector('.translation').textContent;
        for (const suggest of suggests) {
            if ([100, 101].includes(getSim(suggest))) {
                mockInput(getTranslation(suggest));
                break;
            }
        }
    }

    // #region 重新排序历史词条 reSortSuggestions
    function reSortSuggestions() {
        const suggests = document.querySelectorAll('.string-item');
        const getSim = (suggest) => +suggest?.querySelector('header span span').textContent.split('\n')?.[2].trim().slice(0, -1);
        if (!getSim(suggests[0])) return;
        const sorted = [...suggests].sort((a, b) => getSim(b) - getSim(a));
        const parent = suggests[0].parentNode;
        const frag = document.createDocumentFragment();
        frag.append(...sorted);
        parent.innerHTML = '';
        parent.appendChild(frag);
    }

    // #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]
                    ];
                    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] !== 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')) {
                            suggest = (await waitForElems('.translation-memory .translation, .empty-sign'))[0].textContent;
                            suggestSim = +(await waitForElems('.translation-memory header span span'))[0].textContent.split('\n')?.[2].trim().slice(0, -1);
                        }
                        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

    addHotkeys();
    initAuto();

    let lastPath = location.pathname;
    async function actByPath() {
        lastPath = location.pathname;
        if (location.pathname.split('/').pop() === 'strings') {

            let original;
            let lastOriginText = '';
            let toObserve = document.body;

            let observer = new MutationObserver((mutations) => {

                fixAdvSch();

                original = document.querySelector('.editor-core .original');
                if (!original) return;
                const isOriginUpd = lastOriginText && original.textContent !== lastOriginText;
                lastOriginText = original.textContent;

                observer.disconnect();
                initSkip();
                markContext(original.textContent);
                markSearchParams(isOriginUpd);
                fixOrigin(original);
                tweakButtons();
                clickDiff();
                extractDiff();
                copySearch();

                if (isOriginUpd) {
                    const input = document.querySelector('.search-form.mt-3 input[type=search]');
                    if (input) document.querySelectorAll('.btn-sm')[1]?.click();
                }

                for (const mutation of mutations) {
                    const { addedNodes, removedNodes } = mutation;
                    for (const node of addedNodes) {
                        console.debug('added', node);
                        if (node.matches?.('.list-group.tags')) updFixedTags();
                        if (node.matches?.('.string-item a.small')) node.remove();
                        if (node.matches?.('.modal-backdrop')) autoSaveAll();
                    }
                    for (const node of removedNodes) {
                        console.debug('removed ', node);
                        if (node.matches?.('.loading')) { // 历史加载完成
                            searchDiff();
                            autoFill100();
                            reSortSuggestions();
                        }
                        if (node.matches?.('.list-group.tags')) tagSelectController.abort();
                    }
                }

                observer.observe(toObserve, {
                    childList: true,
                    subtree: true,
                });
            });

            observer.observe(toObserve, {
                childList: true,
                subtree: true,
            });

            return observer;

        } else if (location.pathname.split('/').at(-2) === 'issues') {
            waitForElems('.text-content p img').then((imgs) => {
                imgs.forEach(mediumZoom);
            });
        } else if (location.pathname.split('/').pop() === 'history') {
            let observer = new MutationObserver(() => {

                observer.disconnect();
                extractDiff();

                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                });
            });
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
            return observer;
        }
    }
    let cancelAct = await actByPath();
    (await waitForElems('main'))[0].__vue__.$router.afterHooks.push(async ()=>{
        dropLastMark?.();
        dropLastMark = updMark();
        if (lastPath === location.pathname) return;
        cancelAct?.disconnect();
        console.debug('path changed');
        cancelAct = await actByPath();
    });

    // #region utils
    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;
    }

    function debounce(func, timeout = 300) {
        let called = false;
        return (...args) => {
          if (!called) {
            func.apply(this, args);
            called = true;
            setTimeout(() => {
              called = false;
            }, timeout);
          }
        };
    }
    // #endregion

})();