test
// ==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", "");
});
})();