Background Translator

test

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Background Translator
// @match        https://wtr-lab.com/*/chapter-*?service=web*
// @match        https://translate.google.com/?sl=auto&tl=en&op=translate
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @description  test
// @version 0.0.1.20260510071109
// @namespace https://greasyfork.org/users/1527990
// ==/UserScript==

(function() {
    'use strict';

    let lastUrl = location.href;
    const TARGET = '.chapter-body';

    const isChineseChapter = (t) => /第\s*\d+\s*章/.test(t) || /[\u4e00-\u9fa5]/.test(t);

// --- GOOGLE TRANSLATE SIDE ---
    if (location.href.includes('translate.google.com')) {
        GM_addValueChangeListener("request_translate", function(name, old_val, new_val) {
            if (!new_val) return;

            // 1. Extract the specific number from the Chinese source (e.g., 86)
            const sourceMatch = new_val.match(/第\s*(\d+)\s*章/);
            const targetNum = sourceMatch ? sourceMatch[1] : null;

            if (!targetNum) {
                console.error("Could not find Chapter Number in source text.");
                return;
            }

            let inputField = document.querySelector('textarea[aria-label="Source text"]');
            if (!inputField) return;

            inputField.focus();
            inputField.value = new_val;
            inputField.dispatchEvent(new Event('input', { bubbles: true }));

            const startTime = Date.now();
            let stabilityCounter = 0;
            let lastFoundText = "";

            let checkResult = setInterval(() => {
                // Get all translation parts
                let parts = document.querySelectorAll('.ryNqvb, span[jsname="W297wb"]');
                if (parts.length === 0) return;

                // The FIRST part should be our Chapter header
                let firstLine = parts[0].innerText.trim();

                // 2. THE FORMAT CHECK: Must be "Chapter [Number]"
                // We use a Regex to ensure "Chapter" is there and the number matches exactly
                const chapterPattern = new RegExp(`^Chapter\\s*${targetNum}`, 'i');
                const isCorrectChapter = chapterPattern.test(firstLine);

                let currentFullText = Array.from(parts).map(p => p.innerText.trim()).filter(t => t.length > 0).join('\n\n');

                // DEBUG LOGGING
                console.log(`Waiting for: Chapter ${targetNum} | Currently seeing: "${firstLine}"`);

                // 3. VALIDATION GATES
                if (isCorrectChapter && currentFullText.length > (new_val.length * 0.3) && currentFullText === lastFoundText) {
                    stabilityCounter++;
                } else {
                    stabilityCounter = 0; // Reset if the number disappears or text flickers
                }

                lastFoundText = currentFullText;

                // Return only after passing all checks for 2 cycles (10 seconds total)
                if (stabilityCounter >= 2) {
                    console.log("MATCH CONFIRMED. Returning Chapter " + targetNum);
                    GM_setValue("receive_translate", currentFullText);
                    GM_setValue("request_translate", "");
                    clearInterval(checkResult);
                } else if (Date.now() - startTime > 60000) {
                    GM_setValue("receive_translate", "Error: Timeout waiting for Chapter " + targetNum);
                    GM_setValue("request_translate", "");
                    clearInterval(checkResult);
                }
            }, 5000);
        });
        return;
    }

// --- HELPER: Get Chapter from URL ---
    const getUrlChapter = () => {
        const match = location.href.match(/chapter-(\d+)/);
        return match ? match[1] : null;
    };

    // --- NOVEL SITE SIDE ---

    function runTranslation() {
        const urlNum = getUrlChapter();
        if (!urlNum) return;

        // 1. Target the specific container for the current chapter
        const container = document.getElementById(`chapter-${urlNum}`) || document.querySelector(`.chapter-container#chapter-${urlNum}`);
        const div = container ? container.querySelector(TARGET) : document.querySelector(TARGET);

        if (!div || div.dataset.status === "processing" || div.dataset.status === "done" || !isChineseChapter(div.innerText)) return;

        div.dataset.status = "processing";

        setTimeout(() => {
            // Verify we haven't clicked 'next' again during the 3s wait
            if (getUrlChapter() !== urlNum) return;

            const lines = div.querySelectorAll('.wtr-line');
            let textArray = [];
            lines.forEach(line => {
                let lineClone = line.cloneNode(true);
                lineClone.querySelectorAll('.para-number-overlay').forEach(e => e.remove());
                let t = lineClone.innerText.trim();
                if (t) textArray.push(t);
            });

            const textToSend = textArray.join('\n\n');
            if (textToSend.length < 10) {
                div.dataset.status = "";
                return;
            }

            div.style.opacity = "0.5";
            GM_setValue("receive_translate", "");
            GM_setValue("request_translate", textToSend);
        }, 3000);
    }

    // --- SPA ENGINE (PRE-LOAD AWARE) ---
    setInterval(() => {
        const currentUrl = location.href;
        const urlNum = getUrlChapter();

        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            console.log("SPA Navigated to chapter:", urlNum);

            // Cleanup: The SPA keeps old chapters, so we remove the OLD overlay
            // but keep the current one if it exists.
            document.querySelectorAll('#ai-overlay').forEach(el => {
                // If the overlay text doesn't match the new chapter number, kill it
                if (!el.innerText.includes(urlNum)) el.remove();
            });
        }

        // Trigger translation
        runTranslation();
    }, 1500);

    // --- HANDLE TRIP BACK ---
    GM_addValueChangeListener("receive_translate", function(n, o, val) {
        if (!val) return;

        // Find the div matching the current URL
        const urlNum = getUrlChapter();
        const container = document.getElementById(`chapter-${urlNum}`);
        const div = container ? container.querySelector(TARGET) : document.querySelector(TARGET);

        if (!div) return;

        // Check if overlay already exists for this specific container
        let overlay = div.parentNode.querySelector('#ai-overlay');
        if (!overlay) {
            overlay = document.createElement('div');
            overlay.id = 'ai-overlay';
            overlay.className = div.className;
            overlay.style.cssText = `
                display: block !important;
                opacity: 1 !important;
                line-height: 1.4 !important;
                word-break: break-word !important;
            `;
            div.parentNode.insertBefore(overlay, div);
        }

        overlay.innerHTML = '';
        const rawLines = val.replace(/waerdygjfkstlhujhgfd/g, 'awdrsgyjekfthuhgf').split('\n');
        let lastLineWasEmpty = false;

        rawLines.forEach(lineText => {
            const trimmed = lineText.trim();
            if (trimmed === '' && lastLineWasEmpty) return;

            const p = document.createElement('div');
            if (trimmed === '') {
                p.style.height = "0.4em";
                lastLineWasEmpty = true;
            } else {
                p.innerText = trimmed;
                p.style.marginBottom = "0.4em";
                lastLineWasEmpty = false;
            }
            overlay.appendChild(p);
        });

        div.style.display = "none";
        div.setAttribute('data-status', 'done');
        GM_setValue("receive_translate", "");
    });
})();