`;
}
}
startWebImageOCR() {
console.log("Starting web image OCR");
const style = document.createElement("style");
style.textContent = `
.translator-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background-color: rgba(0,0,0,0.3) !important;
z-index: 2147483647 !important;
pointer-events: none !important;
}
.translator-guide {
position: fixed !important;
top: 20px !important;
left: 50% !important;
transform: translateX(-50%) !important;
background-color: rgba(0,0,0,0.8) !important;
color: white !important;
padding: 10px 20px !important;
border-radius: 8px !important;
font-size: 14px !important;
z-index: 2147483647 !important;
pointer-events: none !important;
}
.translator-cancel {
position: fixed !important;
top: 20px !important;
right: 20px !important;
background-color: #ff4444 !important;
color: white !important;
border: none !important;
border-radius: 50% !important;
width: 30px !important;
height: 30px !important;
font-size: 16px !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
z-index: 2147483647 !important;
pointer-events: auto !important;
}
img {
pointer-events: auto !important;
}
img.translator-image-highlight {
outline: 3px solid #4a90e2 !important;
cursor: pointer !important;
position: relative !important;
z-index: 2147483647 !important;
}
`;
document.head.appendChild(style);
const overlay = document.createElement("div");
overlay.className = "translator-overlay";
const guide = document.createElement("div");
guide.className = "translator-guide";
guide.textContent = "Click vào ảnh để OCR";
const cancelBtn = document.createElement("button");
cancelBtn.className = "translator-cancel";
cancelBtn.textContent = "✕";
document.body.appendChild(overlay);
document.body.appendChild(guide);
document.body.appendChild(cancelBtn);
const handleHover = (e) => {
if (e.target.tagName === "IMG") {
e.target.classList.add("translator-image-highlight");
}
};
const handleLeave = (e) => {
if (e.target.tagName === "IMG") {
e.target.classList.remove("translator-image-highlight");
}
};
const handleClick = async (e) => {
if (e.target.tagName === "IMG") {
e.preventDefault();
e.stopPropagation();
try {
this.showTranslatingStatus();
const canvas = document.createElement("canvas");
canvas.width = e.target.naturalWidth;
canvas.height = e.target.naturalHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(e.target, 0, 0);
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, "image/png")
);
const file = new File([blob], "web-image.png", {
type: "image/png",
});
const showSource = this.translator.userSettings.settings.displayOptions.languageLearning.showSource;
const result = await this.ocr.processImage(file);
const translations = result.split("\n");
let fullTranslation = "";
let pinyin = "";
let text = "";
for (const trans of translations) {
const parts = trans.split("<|>");
if (showSource) {
text += (parts[0]?.trim() || "") + "\n";
}
pinyin += (parts[1]?.trim() || "") + "\n";
fullTranslation += (parts[2]?.trim() || trans) + "\n";
}
this.displayPopup(
fullTranslation.trim(),
text.trim(),
"OCR Web Image",
pinyin.trim()
);
this.removeWebImageListeners();
} catch (error) {
console.error("OCR error:", error);
this.showNotification(error.message, "error");
} finally {
this.removeTranslatingStatus();
}
}
};
document.addEventListener("mouseover", handleHover, true);
document.addEventListener("mouseout", handleLeave, true);
document.addEventListener("click", handleClick, true);
cancelBtn.addEventListener("click", () => {
this.removeWebImageListeners();
});
this.webImageListeners = {
hover: handleHover,
leave: handleLeave,
click: handleClick,
overlay,
guide,
cancelBtn,
style,
};
}
startMangaTranslation() {
const style = document.createElement("style");
style.textContent = `
.translator-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.3);
z-index: 2147483647 !important;
pointer-events: none;
transition: background 0.3s ease;
}
.translator-overlay.translating-done {
background-color: transparent;
}
.translator-guide {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0,0,0,0.8);
color: white;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 2147483647 !important;
}
.translator-cancel {
position: fixed;
top: 20px;
right: 20px;
background-color: #ff4444;
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
font-size: 16px;
cursor: pointer;
z-index: 2147483647 !important;
}
.manga-translation-container {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 2147483647 !important;
}
.manga-translation-overlay {
position: absolute;
background-color: rgba(255, 255, 255, 0.95);
padding: 4px 8px;
border-radius: 8px;
pointer-events: none;
text-align: center;
word-break: break-word;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
border: 2px solid rgba(74, 144, 226, 0.7);
}
img.translator-image-highlight {
outline: 3px solid #4a90e2;
cursor: pointer;
}
`;
document.head.appendChild(style);
const overlayContainer = document.createElement("div");
overlayContainer.className = "manga-translation-container";
document.body.appendChild(overlayContainer);
const overlay = document.createElement("div");
overlay.className = "translator-overlay";
const guide = document.createElement("div");
guide.className = "translator-guide";
guide.textContent = "Click vào ảnh để dịch";
const cancelBtn = document.createElement("button");
cancelBtn.className = "translator-cancel";
cancelBtn.textContent = "✕";
document.body.appendChild(overlay);
document.body.appendChild(guide);
document.body.appendChild(cancelBtn);
let existingOverlays = [];
const handleHover = (e) => {
if (e.target.tagName === "IMG") {
e.target.classList.add("translator-image-highlight");
}
};
const handleLeave = (e) => {
if (e.target.tagName === "IMG") {
e.target.classList.remove("translator-image-highlight");
}
};
const handleClick = async (e) => {
if (e.target.tagName === "IMG") {
e.preventDefault();
e.stopPropagation();
try {
this.showTranslatingStatus();
const image = e.target;
const imageUrl = new URL(image.src);
const referer = window.location.href;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const loadImage = async (url) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
Accept: "image/webp,image/apng,image/*,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Cache-Control": "no-cache",
Pragma: "no-cache",
Referer: referer,
Origin: imageUrl.origin,
"Sec-Fetch-Dest": "image",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "cross-site",
"User-Agent": navigator.userAgent,
},
responseType: "blob",
anonymous: true,
onload: function(response) {
if (response.status === 200) {
const blob = response.response;
const img = new Image();
img.onload = () => {
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
resolve();
};
img.onerror = () =>
reject(new Error("Không thể load ảnh"));
img.src = URL.createObjectURL(blob);
} else {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
resolve();
};
img.onerror = () =>
reject(new Error("Không thể load ảnh"));
img.src = url;
}
},
onerror: function() {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
resolve();
};
img.onerror = () => reject(new Error("Không thể load ảnh"));
img.src = url;
},
});
});
};
await loadImage(image.src);
const blob = await new Promise((resolve, reject) => {
try {
canvas.toBlob((b) => {
if (b) resolve(b);
else reject(new Error("Không thể tạo blob"));
}, "image/png");
} catch (err) {
reject(new Error("Lỗi khi tạo blob"));
}
});
const file = new File([blob], "manga.png", { type: "image/png" });
const result = await this.detectTextPositions(file);
overlayContainer.innerHTML = "";
existingOverlays = [];
if (result?.regions) {
overlayContainer.innerHTML = "";
existingOverlays = [];
overlay.classList.add("translating-done");
const sortedRegions = result.regions.sort((a, b) => {
if (Math.abs(a.position.y - b.position.y) < 20) {
return b.position.x - a.position.x;
}
return a.position.y - b.position.y;
});
sortedRegions.forEach((region) => {
const overlay = document.createElement("div");
overlay.className = "manga-translation-overlay";
const calculatePosition = () => {
const imageRect = image.getBoundingClientRect();
const x =
(imageRect.width * region.position.x) / 100 +
imageRect.left;
const y =
(imageRect.height * region.position.y) / 100 +
imageRect.top;
const width = (imageRect.width * region.position.width) / 100;
const height =
(imageRect.height * region.position.height) / 100;
return { x, y, width, height };
};
const pos = calculatePosition();
const padding = 2;
const themeMode = this.translator.userSettings.settings.theme;
const theme = CONFIG.THEME[themeMode];
Object.assign(overlay.style, {
position: "fixed",
left: `${pos.x}px`,
top: `${pos.y}px`,
minWidth: `${pos.width - padding * 2}px`,
width: "auto",
maxWidth: `${pos.width * 1.4 - padding * 2}px`,
height: "auto",
// maxHeight: `${pos.height * 2}px`,
backgroundColor: `${theme.background}`,
color: `${theme.text}`,
padding: `${padding * 2}px ${padding * 4}px`,
borderRadius: "8px",
display: "flex",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
wordBreak: "keep-all",
wordWrap: "break-word",
// overflowWrap: "normal",
lineHeight: "1.2",
pointerEvents: "none",
zIndex: "2147483647 !important",
fontSize:
this.translator.userSettings.settings.displayOptions
.webImageTranslation.fontSize || "9px",
fontWeight: "600",
margin: "0",
flexWrap: "wrap",
whiteSpace: "pre-wrap",
overflow: "visible",
boxSizing: "border-box",
transform: "none",
transformOrigin: "center center",
});
overlay.textContent = region.translation;
overlayContainer.appendChild(overlay);
const updatePosition = debounce(() => {
const newPos = calculatePosition();
overlay.style.left = `${newPos.x}px`;
overlay.style.top = `${newPos.y}px`;
overlay.style.minWidth = `${newPos.width - padding * 2}px`;
overlay.style.maxWidth = `${newPos.width * 1.4 - padding * 2
}px`;
// overlay.style.maxHeight = `${newPos.height * 2}px`;
}, 16);
window.addEventListener("scroll", updatePosition, {
passive: true,
});
window.addEventListener("resize", updatePosition, {
passive: true,
});
});
overlayContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 2147483647 !important;
`;
document.head.appendChild(style);
}
} catch (error) {
console.error("Translation error:", error);
this.showNotification(error.message, "error");
} finally {
this.removeTranslatingStatus();
}
}
};
document.addEventListener("mouseover", handleHover, true);
document.addEventListener("mouseout", handleLeave, true);
document.addEventListener("click", handleClick, true);
cancelBtn.addEventListener("click", () => {
if (this.updatePosition) {
window.removeEventListener("scroll", this.updatePosition);
window.removeEventListener("resize", this.updatePosition);
this.updatePosition = null;
}
overlayContainer.innerHTML = "";
overlayContainer.remove();
document.removeEventListener("mouseover", handleHover, true);
document.removeEventListener("mouseout", handleLeave, true);
document.removeEventListener("click", handleClick, true);
overlay.remove();
guide.remove();
cancelBtn.remove();
style.remove();
document
.querySelectorAll(".translator-image-highlight")
.forEach((el) => {
el.classList.remove("translator-image-highlight");
});
document
.querySelectorAll(".manga-translation-overlay")
.forEach((el) => el.remove());
overlay.classList.remove("translating-done");
this.removeWebImageListeners();
});
this.webImageListeners = {
hover: handleHover,
leave: handleLeave,
click: handleClick,
overlay,
guide,
cancelBtn,
style,
container: overlayContainer,
};
}
async detectTextPositions(file) {
try {
const base64Image = await this.ocr.fileToBase64(file);
const settings = this.translator.userSettings.settings;
const selectedModel = this.translator.api.getGeminiModel();
const targetLanguage = settings.displayOptions.targetLanguage;
const requestBody = {
contents: [
{
parts: [
{
text: `Analyze this image and extract all text regions. For each text region:
1. Extract the original text
2. Dịch sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' với yêu cầu sau:
Bạn là một người dịch truyện chuyên nghiệp, chuyên tạo bản dịch chính xác và tự nhiên. Bạn cần dịch một đoạn truyện sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'. Hãy đảm bảo rằng bản dịch của bạn giữ nguyên ý nghĩa của câu gốc và phù hợp với văn phong của ngôn ngữ đích. Khi dịch, hãy chú ý đến ngữ cảnh văn hóa và bối cảnh của câu chuyện để người đọc có thể hiểu chính xác nội dung. Các quy tắc quan trọng bạn cần tuân thủ bao gồm:
- Đảm bảo nghĩa của các câu không bị thay đổi khi dịch.
- Sử dụng các từ lóng hoặc cụm từ thông dụng khi cần thiết để bản dịch gần gũi với người đọc.
- Kiểm tra chính tả và ngữ pháp trong bản dịch.
- Thêm các trợ từ ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' khi cần để câu văn hoàn chỉnh hơn.
- Sử dụng đại từ nhân xưng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' tự nhiên và hợp ngữ cảnh.
- Tham khảo các trang web sau để lấy văn phong phù hợp cho các đoạn truyện liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
Lưu ý:
- Những từ tên riêng, địa điểm thì hãy dịch theo nghĩa Hán Việt ví dụ như: Diệp Trần, Lục Thiếu Du, Long kiếm, Long Sĩ Đầu, Thiên kiếp, ngõ Nê Bình, ... thì giữ theo nghĩa Hán Việt sẽ hay hơn là dịch hẳn sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'.
- Chỉ trả về bản dịch ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', không giải thích thêm.
3. Determine PRECISE position and size:
- x, y: exact percentage position relative to image (0-100)
- width, height: exact percentage size relative to image (0-100)
- text_length: character count of original text
- text_lines: number of text lines
- bubble_type: speech/thought/narration/sfx
Return ONLY a JSON object like:
{
"regions": [{
"text": "original text",
"translation": "translated text",
"position": {
"x": 20.5,
"y": 30.2,
"width": 15.3,
"height": 10.1,
"text_length": 25,
"text_lines": 2,
"bubble_type": "speech"
}
}]
}`,
},
{
inline_data: {
mime_type: file.type,
data: base64Image,
},
},
],
},
],
generationConfig: {
temperature: settings.ocrOptions.temperature,
topP: settings.ocrOptions.topP,
topK: settings.ocrOptions.topK,
},
};
console.log("Sending API request...");
const responses =
await this.translator.api.keyManager.executeWithMultipleKeys(
async (key) => {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify(requestBody),
onload: (response) => {
console.log("API Response:", response);
if (response.status === 200) {
try {
const result = JSON.parse(response.responseText);
const text =
result?.candidates?.[0]?.content?.parts?.[0]?.text;
if (text) {
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsedJson = JSON.parse(jsonMatch[0]);
resolve(parsedJson);
} else {
reject(new Error("No JSON found in response"));
}
} else {
reject(new Error("Invalid response format"));
}
} catch (error) {
console.error("Parse error:", error);
reject(error);
}
} else {
reject(new Error(`API Error: ${response.status}`));
}
},
onerror: (error) => reject(error),
});
});
return response;
},
settings.apiProvider
);
console.log("API responses:", responses);
const response = responses.find((r) => r && r.regions);
if (!response) {
throw new Error("No valid response found");
}
return response;
} catch (error) {
console.error("Text detection error:", error);
throw error;
}
}
getBrowserContextMenuSize() {
const browser = navigator.userAgent;
const sizes = {
firefox: {
width: 275,
height: 340,
itemHeight: 34,
},
chrome: {
width: 250,
height: 320,
itemHeight: 32,
},
safari: {
width: 240,
height: 300,
itemHeight: 30,
},
edge: {
width: 260,
height: 330,
itemHeight: 33,
},
};
let size;
if (browser.includes("Firefox")) {
size = sizes.firefox;
} else if (browser.includes("Safari") && !browser.includes("Chrome")) {
size = sizes.safari;
} else if (browser.includes("Edge")) {
size = sizes.edge;
} else {
size = sizes.chrome;
}
const dpi = window.devicePixelRatio || 1;
return {
width: Math.round(size.width * dpi),
height: Math.round(size.height * dpi),
itemHeight: Math.round(size.itemHeight * dpi),
};
}
setupContextMenu() {
if (!this.translator.userSettings.settings.contextMenu?.enabled) return;
document.addEventListener("contextmenu", (e) => {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText) {
const oldMenus = document.querySelectorAll(
".translator-context-menu"
);
oldMenus.forEach((menu) => menu.remove());
const contextMenu = document.createElement("div");
contextMenu.className = "translator-context-menu";
const menuItems = [
{ text: "Dịch nhanh", action: "quick" },
{ text: "Dịch popup", action: "popup" },
{ text: "Dịch nâng cao", action: "advanced" },
];
const range = selection.getRangeAt(0).cloneRange();
menuItems.forEach((item) => {
const menuItem = document.createElement("div");
menuItem.className = "translator-context-menu-item";
menuItem.textContent = item.text;
menuItem.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const newSelection = window.getSelection();
newSelection.removeAllRanges();
newSelection.addRange(range);
this.handleTranslateButtonClick(newSelection, item.action);
contextMenu.remove();
};
contextMenu.appendChild(menuItem);
});
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const menuWidth = 150;
const menuHeight = (menuItems.length * 40);
const browserMenu = this.getBrowserContextMenuSize();
const browserMenuWidth = browserMenu.width;
const browserMenuHeight = browserMenu.height;
const spaceWidth = browserMenuWidth + menuWidth;
const remainingWidth = viewportWidth - e.clientX;
const rightEdge = viewportWidth - menuWidth;
const bottomEdge = viewportHeight - menuHeight;
const browserMenuWidthEdge = viewportWidth - browserMenuWidth;
const browserMenuHeightEdge = viewportHeight - browserMenuHeight;
let left, top;
if (e.clientX < menuWidth && e.clientY < menuHeight) {
left = e.clientX + browserMenuWidth + 10;
top = e.clientY;
} else if (
e.clientX > browserMenuWidthEdge &&
e.clientY < browserMenuHeight
) {
left = e.clientX - spaceWidth + remainingWidth;
top = e.clientY;
} else if (
e.clientX > browserMenuWidthEdge &&
e.clientY > viewportHeight - browserMenuHeight
) {
left = e.clientX - spaceWidth + remainingWidth;
top = e.clientY - menuHeight;
} else if (
e.clientX < menuWidth &&
e.clientY > viewportHeight - browserMenuHeight
) {
left = e.clientX + browserMenuWidth + 10;
top = e.clientY - menuHeight;
} else if (e.clientY < menuHeight) {
left = e.clientX - menuWidth;
top = e.clientY;
} else if (e.clientX > browserMenuWidthEdge) {
left = e.clientX - spaceWidth + remainingWidth;
top = e.clientY;
} else if (e.clientY > browserMenuHeightEdge - menuHeight / 2) {
left = e.clientX - menuWidth;
top = e.clientY - menuHeight;
} else {
left = e.clientX;
top = e.clientY - menuHeight;
}
left = Math.max(5, Math.min(left, rightEdge - 5));
top = Math.max(5, Math.min(top, bottomEdge - 5));
contextMenu.style.left = `${left}px`;
contextMenu.style.top = `${top}px`;
document.body.appendChild(contextMenu);
const closeMenu = (e) => {
if (!contextMenu.contains(e.target)) {
contextMenu.remove();
document.removeEventListener("click", closeMenu);
}
};
document.addEventListener("click", closeMenu);
const handleScroll = () => {
contextMenu.remove();
window.removeEventListener("scroll", handleScroll);
};
window.addEventListener("scroll", handleScroll);
}
});
const themeMode = this.translator.userSettings.settings.theme;
const theme = CONFIG.THEME[themeMode];
GM_addStyle(`
.translator-context-menu {
position: fixed;
color: ${theme.text};
background-color: ${theme.background};
border-radius: 8px;
padding: 8px 8px 5px 8px;
min-width: 150px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 2147483647 !important;
font-family: Arial, sans-serif;
font-size: 14px;
opacity: 0;
transform: scale(0.95);
transition: all 0.1s ease-out;
animation: menuAppear 0.15s ease-out forwards;
}
@keyframes menuAppear {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.translator-context-menu-item {
padding: 5px;
margin-bottom: 3px;
cursor: pointer;
color: ${theme.text};
background-color: ${theme.backgroundShadow};
border: 1px solid ${theme.border};
border-radius: 7px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
z-index: 2147483647 !important;
}
.translator-context-menu-item:hover {
background-color: ${theme.button.translate.background};
color: ${theme.button.translate.text};
}
.translator-context-menu-item:active {
transform: scale(0.98);
}
`);
}
removeWebImageListeners() {
if (this.webImageListeners) {
document.removeEventListener(
"mouseover",
this.webImageListeners.hover,
true
);
document.removeEventListener(
"mouseout",
this.webImageListeners.leave,
true
);
document.removeEventListener(
"click",
this.webImageListeners.click,
true
);
this.webImageListeners.overlay?.remove();
this.webImageListeners.guide?.remove();
this.webImageListeners.cancelBtn?.remove();
this.webImageListeners.style?.remove();
document
.querySelectorAll(".translator-image-highlight")
.forEach((el) => {
el.classList.remove("translator-image-highlight");
});
this.webImageListeners = null;
}
}
handleSettingsShortcut(e) {
if (!this.translator.userSettings.settings.shortcuts?.settingsEnabled)
return;
if ((e.altKey || e.metaKey) && e.key === "s") {
e.preventDefault();
const settingsUI = this.translator.userSettings.createSettingsUI();
document.body.appendChild(settingsUI);
}
}
async handleTranslationShortcuts(e) {
if (!this.translator.userSettings.settings.shortcuts?.enabled) return;
const shortcuts = this.translator.userSettings.settings.shortcuts;
if (e.altKey || e.metaKey) {
let translateType = null;
if (e.key === shortcuts.pageTranslate.key) {
e.preventDefault();
await this.handlePageTranslation();
return;
} else if (e.key === shortcuts.inputTranslate.key) {
e.preventDefault();
const activeElement = document.activeElement;
if (this.input.isValidEditor(activeElement)) {
const text = this.input.getEditorContent(activeElement);
if (text) {
await this.input.translateEditor(activeElement, true);
}
}
return;
}
const selection = window.getSelection();
const selectedText = selection?.toString().trim();
if (!selectedText || this.isTranslating) return;
const targetElement = selection.anchorNode?.parentElement;
if (!targetElement) return;
if (e.key === shortcuts.quickTranslate.key) {
e.preventDefault();
translateType = "quick";
} else if (e.key === shortcuts.popupTranslate.key) {
e.preventDefault();
translateType = "popup";
} else if (e.key === shortcuts.advancedTranslate.key) {
e.preventDefault();
translateType = "advanced";
}
if (translateType) {
await this.handleTranslateButtonClick(selection, translateType);
}
}
}
updateSettingsListener(enabled) {
if (enabled) {
document.addEventListener("keydown", this.settingsShortcutListener);
} else {
document.removeEventListener("keydown", this.settingsShortcutListener);
}
}
updateSettingsTranslationListeners(enabled) {
if (enabled) {
document.addEventListener("keydown", this.translationShortcutListener);
} else {
document.removeEventListener(
"keydown",
this.translationShortcutListener
);
}
}
updateSelectionListeners(enabled) {
if (enabled) this.setupSelectionHandlers();
}
updateTapListeners(enabled) {
if (enabled) this.setupDocumentTapHandler();
}
setupEventListeners() {
const shortcuts = this.translator.userSettings.settings.shortcuts;
const clickOptions = this.translator.userSettings.settings.clickOptions;
const touchOptions = this.translator.userSettings.settings.touchOptions;
if (this.translator.userSettings.settings.contextMenu?.enabled) {
this.setupContextMenu();
}
if (shortcuts?.settingsEnabled) {
this.updateSettingsListener(true);
}
if (shortcuts?.enabled) {
this.updateSettingsTranslationListeners(true);
}
if (clickOptions?.enabled) {
this.updateSelectionListeners(true);
this.translationButtonEnabled = true;
}
if (touchOptions?.enabled) {
this.updateTapListeners(true);
this.translationTapEnabled = true;
}
const isEnabled =
localStorage.getItem("translatorToolsEnabled") === "true";
if (isEnabled) {
this.setupTranslatorTools();
}
document.addEventListener("settingsChanged", (e) => {
this.removeToolsContainer();
const newSettings = e.detail;
if (newSettings.theme !== this.translator.userSettings.settings.theme) {
this.updateAllButtonStyles();
}
this.updateSettingsListener(newSettings.shortcuts?.settingsEnabled);
this.updateSettingsTranslationListeners(newSettings.shortcuts?.enabled);
if (newSettings.clickOptions?.enabled !== undefined) {
this.translationButtonEnabled = newSettings.clickOptions.enabled;
this.updateSelectionListeners(newSettings.clickOptions.enabled);
if (!newSettings.clickOptions.enabled) {
this.removeTranslateButton();
}
}
if (newSettings.touchOptions?.enabled !== undefined) {
this.translationTapEnabled = newSettings.touchOptions.enabled;
this.updateTapListeners(newSettings.touchOptions.enabled);
if (!newSettings.touchOptions.enabled) {
this.removeTranslateButton();
}
}
this.cache = new TranslationCache(
newSettings.cacheOptions.text.maxSize,
newSettings.cacheOptions.text.expirationTime
);
this.cache.clear();
if (this.ocr?.imageCache) {
this.ocr.imageCache.clear();
}
const apiConfig = {
providers: CONFIG.API.providers,
currentProvider: newSettings.apiProvider,
apiKey: newSettings.apiKey,
maxRetries: CONFIG.API.maxRetries,
retryDelay: CONFIG.API.retryDelay,
};
this.api = new APIManager(
apiConfig,
() => this.translator.userSettings.settings
);
const isEnabled =
localStorage.getItem("translatorToolsEnabled") === "true";
if (isEnabled) {
this.setupTranslatorTools();
}
});
}
showNotification(message, type = "info") {
const notification = document.createElement("div");
notification.className = "translator-notification";
const colors = {
info: "#4a90e2",
success: "#28a745",
warning: "#ffc107",
error: "#dc3545",
};
const backgroundColor = colors[type] || colors.info;
const textColor = type === "warning" ? "#000" : "#fff";
Object.assign(notification.style, {
position: "fixed",
top: "20px",
left: "50%",
transform: "translateX(-50%)",
backgroundColor,
color: textColor,
padding: "10px 20px",
borderRadius: "8px",
zIndex: "2147483647 !important",
animation: "fadeInOut 2s ease",
fontFamily: "Arial, sans-serif",
fontSize: "14px",
boxShadow: "0 2px 10px rgba(0,0,0,0.2)",
});
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 2000);
}
resetState() {
if (this.pressTimer) clearTimeout(this.pressTimer);
if (this.timer) clearTimeout(this.timer);
this.isLongPress = false;
this.lastTime = 0;
this.count = 0;
this.isDown = false;
this.isTranslating = false;
this.ignoreNextSelectionChange = false;
this.removeTranslateButton();
this.removeTranslatingStatus();
}
removeTranslateButton() {
if (this.currentTranslateButton) {
this.currentTranslateButton.remove();
this.currentTranslateButton = null;
}
}
removeTranslatingStatus() {
if (this.translatingStatus) {
this.translatingStatus.remove();
this.translatingStatus = null;
}
}
}
class Translator {
constructor() {
window.translator = this;
this.userSettings = new UserSettings(this);
const apiConfig = {
...CONFIG.API,
currentProvider: this.userSettings.getSetting("apiProvider"),
apiKey: this.userSettings.getSetting("apiKey"),
};
this.api = new APIManager(apiConfig, () => this.userSettings.settings);
this.ocr = new OCRManager(this);
this.media = new MediaManager(this);
this.ui = new UIManager(this);
this.cache = new TranslationCache(
this.userSettings.settings.cacheOptions.text.maxSize,
this.userSettings.settings.cacheOptions.text.expirationTime
);
this.page = new PageTranslator(this);
this.inputTranslator = new InputTranslator(this);
this.ui.setupEventListeners();
this.cache.optimizeStorage();
this.autoCorrectEnabled = true;
}
async translate(
text,
targetElement,
isAdvanced = false,
popup = false,
targetLang = ""
) {
try {
if (!text) return null;
const settings = this.userSettings.settings.displayOptions;
const targetLanguage = targetLang || settings.targetLanguage;
const promptType = isAdvanced ? "advanced" : "normal";
const prompt = this.createPrompt(text, promptType, targetLanguage);
let translatedText;
const cacheEnabled =
this.userSettings.settings.cacheOptions.text.enabled;
if (cacheEnabled) {
translatedText = this.cache.get(text, isAdvanced, targetLanguage);
}
if (!translatedText) {
translatedText = await this.api.request(prompt);
if (cacheEnabled && translatedText) {
this.cache.set(text, translatedText, isAdvanced, targetLanguage);
}
}
if (
translatedText &&
targetElement &&
!targetElement.isPDFTranslation
) {
if (isAdvanced || popup) {
const translations = translatedText.split("\n");
let fullTranslation = "";
let pinyin = "";
for (const trans of translations) {
const parts = trans.split("<|>");
pinyin += (parts[1]?.trim() || "") + "\n";
fullTranslation += (parts[2]?.trim() || trans) + "\n";
}
this.ui.displayPopup(
fullTranslation.trim(),
text,
"King1x32 <3",
pinyin.trim()
);
} else {
this.ui.showTranslationBelow(translatedText, targetElement, text);
}
}
return translatedText;
} catch (error) {
console.error("Lỗi dịch:", error);
this.ui.showNotification(error.message, "error");
}
}
async translateLongText(text, maxChunkSize = 1000) {
const chunks = this.splitIntoChunks(text, maxChunkSize);
const translations = await Promise.all(
chunks.map((chunk) => this.translate(chunk))
);
return this.smartMerge(translations);
}
splitIntoChunks(text, maxChunkSize) {
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
const chunks = [];
let currentChunk = "";
for (const sentence of sentences) {
if ((currentChunk + sentence).length > maxChunkSize && currentChunk) {
chunks.push(currentChunk.trim());
currentChunk = "";
}
currentChunk += sentence + " ";
}
if (currentChunk) {
chunks.push(currentChunk.trim());
}
return chunks;
}
smartMerge(translations) {
return translations.reduce((merged, current, index) => {
if (index === 0) return current;
const lastChar = merged.slice(-1);
if (".!?".includes(lastChar)) {
return `${merged} ${current}`;
}
return merged + current;
}, "");
}
async autoCorrect(translation) {
const targetLanguage =
this.userSettings.settings.displayOptions.targetLanguage;
const prompt = `Vui lòng kiểm tra và sửa chữa bất kỳ lỗi ngữ pháp hoặc vấn đề về ngữ cảnh trong bản dịch sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' này: "${translation}". Không thêm hay bớt ý của bản gốc cũng như không thêm tiêu đề, không giải thích về các thay đổi đã thực hiện.`;
try {
const corrected = await this.api.request(prompt);
return corrected.trim();
} catch (error) {
console.error("Auto-correction failed:", error);
return translation;
}
}
createPrompt(text, type = "normal", targetLang = "") {
const settings = this.userSettings.settings;
const targetLanguage =
targetLang || settings.displayOptions.targetLanguage;
const sourceLanguage = settings.displayOptions.sourceLanguage;
const isPinyinMode =
settings.displayOptions.translationMode !== "translation_only"
if (
settings.promptSettings?.enabled &&
settings.promptSettings?.useCustom
) {
const prompts = settings.promptSettings.customPrompts;
const promptKey = isPinyinMode ? `${type}_chinese` : type;
let promptTemplate = prompts[promptKey];
if (promptTemplate) {
return promptTemplate
.replace(/\{text\}/g, text)
.replace(/\{targetLang\}/g, targetLanguage)
.replace(
/\{sourceLang\}/g,
sourceLanguage || this.page.languageCode
);
}
}
return this.createDefaultPrompt(text, type, isPinyinMode, targetLanguage);
}
createDefaultPrompt(
text,
type = "normal",
isPinyinMode = false,
targetLang = ""
) {
const settings = this.userSettings.settings;
const targetLanguage =
targetLang || settings.displayOptions.targetLanguage;
const share_media = `Bạn là một người dịch phụ đề phim chuyên nghiệp, chuyên tạo file SRT. Bạn cần dịch một đoạn hội thoại phim sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'. Hãy đảm bảo rằng bản dịch của bạn chính xác và tự nhiên, giữ nguyên ý nghĩa của câu gốc. Khi dịch, hãy chú ý đến ngữ cảnh văn hóa và bối cảnh của bộ phim để người xem có thể hiểu chính xác nội dung. Các quy tắc quan trọng bạn cần tuân thủ bao gồm:
- Đảm bảo nghĩa của các câu không bị thay đổi khi dịch.
- Sử dụng các từ lóng hoặc cụm từ thông dụng khi cần thiết để bản dịch gần gũi với người đọc.
- Kiểm tra chính tả và ngữ pháp trong bản dịch.
- Thêm các trợ từ ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' khi cần để hội thoại hoàn chỉnh hơn.
- Sử dụng đại từ nhân xưng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' tự nhiên và hợp ngữ cảnh.
- Tham khảo các trang web sau để lấy văn phong phù hợp cho các đoạn hội thoại liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
Lưu ý:
- Bản dịch phải hoàn toàn bằng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', nhưng ví dụ khi dịch sang tiếng Việt nếu gặp những danh từ riêng chỉ địa điểm hoặc tên riêng, có phạm trù trong ngôn ngữ là từ ghép của 2 ngôn ngữ gọi là từ Hán Việt, hãy dịch sang nghĩa từ Hán Việt như Diệp Trần, Lục Thiếu Du, Long kiếm, Thiên kiếp, núi Long Sĩ Đầu, ngõ Nê Bình, Thiên Kiếm môn,... thì sẽ hay hơn là dịch hẳn sang nghĩa tiếng Việt là Lá Trần, Rồng kiếm, Trời kiếp, núi Rồng Ngẩng Đầu,...`;
const base_normal = `Cho bạn văn bản cần xử lý: "${text}"
Hãy dịch văn bản cần xử lý trên sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' với các yêu cầu sau:
- Dịch phải tuân thủ chặt chẽ bối cảnh và sắc thái ban đầu của văn bản.
- Đảm bảo sự lưu loát và tự nhiên như người bản xứ.
- Không thêm bất kỳ giải thích hay diễn giải nào ngoài bản dịch.
- Bảo toàn các thuật ngữ và danh từ riêng với tỷ lệ 1:1.
Nếu bạn nhận thấy văn bản là truyện thì hãy dịch truyện theo yêu cầu sau:
Bạn là một người dịch truyện chuyên nghiệp, chuyên tạo bản dịch chính xác và tự nhiên. Bạn cần dịch một đoạn truyện sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'. Hãy đảm bảo rằng bản dịch của bạn giữ nguyên ý nghĩa của câu gốc và phù hợp với văn phong của ngôn ngữ đích. Khi dịch, hãy chú ý đến ngữ cảnh văn hóa và bối cảnh của câu chuyện để người đọc có thể hiểu chính xác nội dung. Các quy tắc quan trọng bạn cần tuân thủ bao gồm:
- Đảm bảo nghĩa của các câu không bị thay đổi khi dịch.
- Sử dụng các từ lóng hoặc cụm từ thông dụng khi cần thiết để bản dịch gần gũi với người đọc.
- Kiểm tra chính tả và ngữ pháp trong bản dịch.
- Thêm các trợ từ ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' khi cần để câu văn hoàn chỉnh hơn.
- Sử dụng đại từ nhân xưng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' tự nhiên và hợp ngữ cảnh.
- Tham khảo các trang web sau để lấy văn phong phù hợp cho các đoạn truyện liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
Lưu ý:
- Bản dịch phải hoàn toàn bằng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', nhưng ví dụ khi dịch sang tiếng Việt nếu gặp những danh từ riêng chỉ địa điểm hoặc tên riêng, có phạm trù trong ngôn ngữ là từ ghép của 2 ngôn ngữ gọi là từ Hán Việt, hãy dịch sang nghĩa từ Hán Việt như Diệp Trần, Lục Thiếu Du, Long kiếm, Thiên kiếp, núi Long Sĩ Đầu, ngõ Nê Bình, Thiên Kiếm môn,... thì sẽ hay hơn là dịch hẳn sang nghĩa tiếng Việt là Lá Trần, Rồng kiếm, Trời kiếp, núi Rồng Ngẩng Đầu,...
- Hãy in ra bản dịch mà không có dấu ngoặc kép, giữ nguyên định dạng phông chữ ban đầu và không giải thích gì thêm.`;
const basePrompts = {
normal: `${base_normal}`,
advanced: `Dịch và phân tích từ khóa: "${text}"`,
ocr: `Bạn là một người dịch truyện chuyên nghiệp, chuyên tạo bản dịch chính xác và tự nhiên. Bạn cần dịch một đoạn truyện sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'. Hãy đảm bảo rằng bản dịch của bạn giữ nguyên ý nghĩa của câu gốc và phù hợp với văn phong của ngôn ngữ đích. Khi dịch, hãy chú ý đến ngữ cảnh văn hóa và bối cảnh của câu chuyện để người đọc có thể hiểu chính xác nội dung. Các quy tắc quan trọng bạn cần tuân thủ bao gồm:
- Đảm bảo nghĩa của các câu không bị thay đổi khi dịch.
- Sử dụng các từ lóng hoặc cụm từ thông dụng khi cần thiết để bản dịch gần gũi với người đọc.
- Kiểm tra chính tả và ngữ pháp trong bản dịch.
- Thêm các trợ từ ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' khi cần để câu văn hoàn chỉnh hơn.
- Sử dụng đại từ nhân xưng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}' tự nhiên và hợp ngữ cảnh.
- Tham khảo các trang web sau để lấy văn phong phù hợp cho các đoạn truyện liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
Lưu ý:
- Bản dịch phải hoàn toàn bằng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', nhưng ví dụ khi dịch sang tiếng Việt nếu gặp những danh từ riêng chỉ địa điểm hoặc tên riêng, có phạm trù trong ngôn ngữ là từ ghép của 2 ngôn ngữ gọi là từ Hán Việt, hãy dịch sang nghĩa từ Hán Việt như Diệp Trần, Lục Thiếu Du, Long kiếm, Thiên kiếp, núi Long Sĩ Đầu, ngõ Nê Bình, Thiên Kiếm môn,... thì sẽ hay hơn là dịch hẳn sang nghĩa tiếng Việt là Lá Trần, Rồng kiếm, Trời kiếp, núi Rồng Ngẩng Đầu,...
- Chỉ trả về bản dịch ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', không giải thích thêm.`,
media: `${share_media}
- Định dạng bản dịch của bạn theo định dạng SRT và đảm bảo rằng mỗi đoạn hội thoại được đánh số thứ tự, có thời gian bắt đầu và kết thúc rõ ràng. Không cần giải thích thêm.`,
page: `${base_normal}`,
};
const pinyinPrompts = {
normal: `Hãy trả về theo format sau, mỗi phần cách nhau bằng dấu <|> và không có giải thích thêm:
Văn bản gốc <|> pinyin có số tone (1-4) <|> bản dịch sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'
Văn bản cần xử lý: "${text}"
Lưu ý:
- Nếu có từ không phải là tiếng Trung, hãy trả về giá trị pinyin của từ đó là phiên âm của từ đó và theo ngôn ngữ đó (Nếu là tiếng Anh thì hay theo phiên âm của US). Ví dụ: Hello <|> /heˈloʊ/ <|> Xin chào
- Bản dịch phải hoàn toàn bằng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', nhưng ví dụ khi dịch sang tiếng Việt nếu gặp những danh từ riêng chỉ địa điểm hoặc tên riêng, có phạm trù trong ngôn ngữ là từ ghép của 2 ngôn ngữ gọi là từ Hán Việt, hãy dịch sang nghĩa từ Hán Việt như Diệp Trần, Lục Thiếu Du, Long kiếm, Thiên kiếp, núi Long Sĩ Đầu, ngõ Nê Bình, Thiên Kiếm môn,... thì sẽ hay hơn là dịch hẳn sang nghĩa tiếng Việt là Lá Trần, Rồng kiếm, Trời kiếp, núi Rồng Ngẩng Đầu,...
- Chỉ trả về bản dịch theo format trên và không giải thích thêm.`,
advanced: `Dịch và phân tích từ khóa: "${text}"`,
ocr: `Hãy trả về theo format sau, mỗi phần cách nhau bằng dấu <|> và không có giải thích thêm:
Văn bản gốc <|> pinyin có số tone (1-4) <|> bản dịch sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'
Đọc hiểu thật kĩ và xử lý toàn bộ văn bản trong hình ảnh.
Lưu ý:
- Nếu có từ không phải là tiếng Trung, hãy trả về giá trị pinyin của từ đó là phiên âm của từ đó và theo ngôn ngữ đó (Nếu là tiếng Anh thì hay theo phiên âm của US). Ví dụ: Hello <|> /heˈloʊ/ <|> Xin chào
- Bản dịch phải hoàn toàn bằng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', nhưng ví dụ khi dịch sang tiếng Việt nếu gặp những danh từ riêng chỉ địa điểm hoặc tên riêng, có phạm trù trong ngôn ngữ là từ ghép của 2 ngôn ngữ gọi là từ Hán Việt, hãy dịch sang nghĩa từ Hán Việt như Diệp Trần, Lục Thiếu Du, Long kiếm, Thiên kiếp, núi Long Sĩ Đầu, ngõ Nê Bình, Thiên Kiếm môn,... thì sẽ hay hơn là dịch hẳn sang nghĩa tiếng Việt là Lá Trần, Rồng kiếm, Trời kiếp, núi Rồng Ngẩng Đầu,...
- Chỉ trả về bản dịch theo format trên, mỗi 1 cụm theo format sẽ ở 1 dòng và không giải thích thêm.`,
media: `${share_media}
- Định dạng bản dịch của bạn theo định dạng SRT phải đảm bảo rằng mỗi đoạn hội thoại được đánh số thứ tự, có thời gian bắt đầu và kết thúc, có dòng văn bản gốc và dòng văn bản dịch.
- Không cần giải thích thêm.`,
page: `Hãy trả về theo format sau, mỗi phần cách nhau bằng dấu <|> và không có giải thích thêm:
Văn bản gốc <|> pinyin có số tone (1-4) <|> bản dịch sang ngôn ngữ có mã ngôn ngữ là '${targetLanguage}'
Dịch thật tự nhiên, đúng ngữ cảnh, giữ nguyên định dạng phông chữ ban đầu. Nếu có từ không phải là tiếng Trung, hãy trả về phiên âm của từ đó theo ngôn ngữ của từ đó.
Văn bản cần xử lý: "${text}"
Lưu ý:
- Nếu có từ không phải là tiếng Trung, hãy trả về giá trị pinyin của từ đó là phiên âm của từ đó và theo ngôn ngữ đó (Nếu là tiếng Anh thì hay theo phiên âm của US). Ví dụ: Hello <|> /heˈloʊ/ <|> Xin chào
- Bản dịch phải hoàn toàn bằng ngôn ngữ có mã ngôn ngữ là '${targetLanguage}', nhưng ví dụ khi dịch sang tiếng Việt nếu gặp những danh từ riêng chỉ địa điểm hoặc tên riêng, có phạm trù trong ngôn ngữ là từ ghép của 2 ngôn ngữ gọi là từ Hán Việt, hãy dịch sang nghĩa từ Hán Việt như Diệp Trần, Lục Thiếu Du, Long kiếm, Thiên kiếp, núi Long Sĩ Đầu, ngõ Nê Bình, Thiên Kiếm môn,... thì sẽ hay hơn là dịch hẳn sang nghĩa tiếng Việt là Lá Trần, Rồng kiếm, Trời kiếp, núi Rồng Ngẩng Đầu,...
- Chỉ trả về bản dịch theo format trên và không giải thích thêm.`,
};
return isPinyinMode ? pinyinPrompts[type] : basePrompts[type];
}
showSettingsUI() {
const settingsUI = this.userSettings.createSettingsUI();
document.body.appendChild(settingsUI);
}
handleError(error, targetElement) {
console.error("Translation failed:", error);
const message = error.message.includes("Rate limit")
? "Vui lòng chờ giữa các lần dịch"
: error.message.includes("Gemini API")
? "Lỗi Gemini: " + error.message
: error.message.includes("API Key")
? "Lỗi xác thực API"
: "Lỗi dịch thuật: " + error.message;
this.ui.showTranslationBelow(targetElement, message);
}
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const createFileInput = (accept, onFileSelected) => {
return new Promise((resolve) => {
const translator = window.translator;
const themeMode = translator.userSettings.settings.theme;
const theme = CONFIG.THEME[themeMode];
const div = document.createElement('div');
div.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 2147483647 !important;
display: flex;
justify-content: center;
align-items: center;
font-family: Arial, sans-serif;
`;
const container = document.createElement('div');
container.style.cssText = `
background: ${theme.background};
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 15px;
min-width: 300px;
border: 1px solid ${theme.border};
`;
const title = document.createElement('div');
title.style.cssText = `
color: ${theme.title};
font-size: 16px;
font-weight: bold;
text-align: center;
margin-bottom: 5px;
`;
title.textContent = 'Chọn file để dịch';
const inputContainer = document.createElement('div');
inputContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
`;
const input = document.createElement('input');
input.type = 'file';
input.accept = accept;
input.style.cssText = `
padding: 8px;
border-radius: 8px;
border: 1px solid ${theme.border};
background: ${themeMode === 'dark' ? '#444' : '#fff'};
color: ${theme.text};
width: 100%;
cursor: pointer;
`;
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 10px;
justify-content: center;
margin-top: 10px;
`;
const cancelButton = document.createElement('button');
cancelButton.style.cssText = `
padding: 8px 16px;
border-radius: 8px;
border: none;
background: ${theme.button.close.background};
color: ${theme.button.close.text};
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
`;
cancelButton.textContent = 'Hủy';
cancelButton.onmouseover = () => {
cancelButton.style.transform = 'translateY(-2px)';
cancelButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
};
cancelButton.onmouseout = () => {
cancelButton.style.transform = 'none';
cancelButton.style.boxShadow = 'none';
};
const translateButton = document.createElement('button');
translateButton.style.cssText = `
padding: 8px 16px;
border-radius: 8px;
border: none;
background: ${theme.button.translate.background};
color: ${theme.button.translate.text};
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
opacity: 0.5;
`;
translateButton.textContent = 'Dịch';
translateButton.disabled = true;
translateButton.onmouseover = () => {
if (!translateButton.disabled) {
translateButton.style.transform = 'translateY(-2px)';
translateButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
}
};
translateButton.onmouseout = () => {
translateButton.style.transform = 'none';
translateButton.style.boxShadow = 'none';
};
const cleanup = () => {
div.remove();
resolve();
};
input.addEventListener('change', (e) => {
const file = e.target.files?.[0];
if (file) {
translateButton.disabled = false;
translateButton.style.opacity = '1';
} else {
translateButton.disabled = true;
translateButton.style.opacity = '0.5';
}
});
cancelButton.addEventListener('click', cleanup);
translateButton.addEventListener('click', async () => {
const file = input.files?.[0];
if (file) {
try {
translateButton.disabled = true;
translateButton.style.opacity = '0.5';
translateButton.textContent = 'Đang xử lý...';
await onFileSelected(file);
} catch (error) {
console.error('Error processing file:', error);
}
cleanup();
}
});
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(translateButton);
inputContainer.appendChild(input);
container.appendChild(title);
container.appendChild(inputContainer);
container.appendChild(buttonContainer);
div.appendChild(container);
document.body.appendChild(div);
div.addEventListener('click', (e) => {
if (e.target === div) cleanup();
});
});
};
GM_registerMenuCommand("📄 Dịch trang", async () => {
const translator = window.translator;
if (translator) {
try {
translator.ui.showTranslatingStatus();
const result = await translator.page.translatePage();
if (result.success) {
translator.ui.showNotification(result.message, "success");
} else {
translator.ui.showNotification(result.message, "warning");
}
} catch (error) {
console.error("Page translation error:", error);
translator.ui.showNotification(error.message, "error");
} finally {
translator.ui.removeTranslatingStatus();
}
}
});
GM_registerMenuCommand("📷 Dịch Ảnh", async () => {
const translator = window.translator;
if (!translator) return;
await createFileInput("image/*", async (file) => {
try {
translator.ui.showTranslatingStatus();
const result = await translator.ocr.processImage(file);
translator.ui.displayPopup(result, null, "OCR Result");
} catch (error) {
translator.ui.showNotification(error.message);
} finally {
translator.ui.removeTranslatingStatus();
}
});
});
GM_registerMenuCommand("📸 Dịch Màn hình", async () => {
const translator = window.translator;
if (translator) {
try {
translator.ui.showTranslatingStatus();
const screenshot = await translator.ocr.captureScreen();
if (!screenshot) {
throw new Error("Không thể tạo ảnh chụp màn hình");
}
const result = await translator.ocr.processImage(screenshot);
if (!result) {
throw new Error("Không thể xử lý ảnh chụp màn hình");
}
translator.ui.displayPopup(result, null, "OCR Màn hình");
} catch (error) {
console.error("Screen translation error:", error);
translator.ui.showNotification(error.message, "error");
} finally {
translator.ui.removeTranslatingStatus();
}
}
});
GM_registerMenuCommand("🖼️ Dịch Ảnh Web", () => {
const translator = window.translator;
if (translator) {
translator.ui.startWebImageOCR();
}
});
GM_registerMenuCommand("📚 Dịch Manga", () => {
const translator = window.translator;
if (translator) {
translator.ui.startMangaTranslation();
}
});
GM_registerMenuCommand("🎵 Dịch Media", async () => {
const translator = window.translator;
if (!translator) return;
await createFileInput("audio/*, video/*", async (file) => {
try {
translator.ui.showTranslatingStatus();
await translator.media.processMediaFile(file);
} catch (error) {
translator.ui.showNotification(error.message);
} finally {
translator.ui.removeTranslatingStatus();
}
});
});
GM_registerMenuCommand("📄 Dịch File HTML", async () => {
const translator = window.translator;
if (!translator) return;
await createFileInput(".html,.htm", async (file) => {
try {
translator.ui.showTranslatingStatus();
const content = await translator.ui.readFileContent(file);
const translatedHTML = await translator.page.translateHTML(content);
const blob = new Blob([translatedHTML], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `king1x32_translated_${file.name}`;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
a.remove();
translator.ui.showNotification("Dịch file HTML thành công", "success");
} catch (error) {
console.error("Lỗi dịch file HTML:", error);
translator.ui.showNotification(error.message, "error");
} finally {
translator.ui.removeTranslatingStatus();
}
});
});
GM_registerMenuCommand("📑 Dịch File PDF", async () => {
const translator = window.translator;
if (!translator) return;
await createFileInput(".pdf", async (file) => {
try {
translator.ui.showLoadingStatus("Đang xử lý PDF...");
const translatedBlob = await translator.page.translatePDF(file);
const url = URL.createObjectURL(translatedBlob);
const a = document.createElement("a");
a.href = url;
a.download = `king1x32_translated_${file.name.replace(".pdf", ".html")}`;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
a.remove();
translator.ui.showNotification("Dịch PDF thành công", "success");
} catch (error) {
console.error("Lỗi dịch PDF:", error);
translator.ui.showNotification(error.message, "error");
} finally {
translator.ui.removeLoadingStatus();
}
});
});
GM_registerMenuCommand("⚙️ Cài đặt King Translator AI", () => {
const translator = window.translator;
if (translator) {
const settingsUI = translator.userSettings.createSettingsUI();
document.body.appendChild(settingsUI);
}
});
new Translator();
})();