PushGo Auto Commit 2026-03-29T12:17:45.673Z
This commit is contained in:
206
content.js
Normal file
206
content.js
Normal file
@@ -0,0 +1,206 @@
|
||||
const BLOCKED_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"]);
|
||||
const SCROLL_DEBOUNCE_MS = 250;
|
||||
const MAX_NODES_PER_PASS = 30;
|
||||
|
||||
const originalTextMap = new Map();
|
||||
let autoTranslateEnabled = false;
|
||||
let scrollTimer = null;
|
||||
let translating = false;
|
||||
let needRerun = false;
|
||||
|
||||
function isVisible(element) {
|
||||
let el = element;
|
||||
while (el) {
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.display === "none" || style.visibility === "hidden") {
|
||||
return false;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isInViewport(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
|
||||
}
|
||||
|
||||
function collectTextNodes(onlyViewport) {
|
||||
const root = document.body || document.documentElement;
|
||||
if (!root) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode(node) {
|
||||
const text = node.nodeValue?.trim();
|
||||
const parent = node.parentElement;
|
||||
if (!text || !parent) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
if (BLOCKED_TAGS.has(parent.tagName) || parent.isContentEditable) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
if (originalTextMap.has(node)) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
if (!isVisible(parent)) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
if (onlyViewport && !isInViewport(parent)) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) {
|
||||
nodes.push(walker.currentNode);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function requestTranslation(text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ action: "translate", text }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (!response?.ok) {
|
||||
reject(new Error(response?.error || "翻译失败"));
|
||||
return;
|
||||
}
|
||||
resolve(response.text);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stopAutoTranslate() {
|
||||
autoTranslateEnabled = false;
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = null;
|
||||
}
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
window.removeEventListener("resize", handleScroll);
|
||||
}
|
||||
|
||||
async function translateVisibleOnce() {
|
||||
const nodes = collectTextNodes(true);
|
||||
if (!nodes.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const batch = nodes.slice(0, MAX_NODES_PER_PASS);
|
||||
for (const node of batch) {
|
||||
if (!autoTranslateEnabled) {
|
||||
break;
|
||||
}
|
||||
const original = node.nodeValue;
|
||||
const translated = await requestTranslation(original);
|
||||
originalTextMap.set(node, original);
|
||||
node.nodeValue = translated;
|
||||
}
|
||||
|
||||
if (nodes.length > batch.length) {
|
||||
needRerun = true;
|
||||
}
|
||||
return batch.length;
|
||||
}
|
||||
|
||||
async function runTranslateCycle() {
|
||||
if (!autoTranslateEnabled) {
|
||||
return;
|
||||
}
|
||||
if (translating) {
|
||||
needRerun = true;
|
||||
return;
|
||||
}
|
||||
|
||||
translating = true;
|
||||
try {
|
||||
await translateVisibleOnce();
|
||||
} catch (err) {
|
||||
stopAutoTranslate();
|
||||
alert(`Transly: ${err.message || "翻译失败"}`);
|
||||
} finally {
|
||||
translating = false;
|
||||
if (autoTranslateEnabled && needRerun) {
|
||||
needRerun = false;
|
||||
setTimeout(() => {
|
||||
runTranslateCycle().catch(() => {});
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!autoTranslateEnabled) {
|
||||
return;
|
||||
}
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer);
|
||||
}
|
||||
scrollTimer = setTimeout(() => {
|
||||
runTranslateCycle().catch(() => {});
|
||||
}, SCROLL_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
async function translatePage() {
|
||||
const hasCurrentNodes = collectTextNodes(true).length > 0;
|
||||
|
||||
if (!autoTranslateEnabled) {
|
||||
autoTranslateEnabled = true;
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
window.addEventListener("resize", handleScroll);
|
||||
}
|
||||
|
||||
await runTranslateCycle();
|
||||
if (!hasCurrentNodes && !originalTextMap.size) {
|
||||
alert("Transly: 当前可视区域没有可翻译文本");
|
||||
}
|
||||
}
|
||||
|
||||
function restorePage() {
|
||||
stopAutoTranslate();
|
||||
|
||||
if (!originalTextMap.size) {
|
||||
alert("Transly: 没有可恢复内容");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [node, text] of originalTextMap.entries()) {
|
||||
if (node?.isConnected) {
|
||||
node.nodeValue = text;
|
||||
}
|
||||
}
|
||||
originalTextMap.clear();
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message?.action === "translatePage") {
|
||||
translatePage()
|
||||
.then(() => sendResponse({ ok: true }))
|
||||
.catch((err) => sendResponse({ ok: false, error: err.message || "翻译失败" }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message?.action === "restorePage") {
|
||||
restorePage();
|
||||
sendResponse({ ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (message?.action === "showTranslation") {
|
||||
alert(message.text || "");
|
||||
sendResponse({ ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (message?.action === "showError") {
|
||||
alert(`Transly: ${message.error || "翻译失败"}`);
|
||||
sendResponse({ ok: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user