PushGo Auto Commit 2026-03-29T12:17:45.673Z

This commit is contained in:
PushGo User
2026-03-29 20:17:45 +08:00
commit 8398984cc5
12 changed files with 1276 additions and 0 deletions

206
content.js Normal file
View 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 });
}
});