Gemini AI Translator (Inline & Popup)

Dịch văn bản (bôi đen văn bản, khi nhập văn bản), hình ảnh, audio, video bằng Google Gemini API. Hỗ trợ popup phân tích từ vựng, popup dịch và dịch nhanh.

当前为 2025-03-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Gemini AI Translator (Inline & Popup)
  3. // @namespace Gemini AI Translator (Inline & Popup)
  4. // @version 4.2.2
  5. // @author King1x32
  6. // @icon https://raw.githubusercontent.com/king1x32/UserScripts/refs/heads/main/kings.jpg
  7. // @description Dịch văn bản (bôi đen văn bản, khi nhập văn bản), hình ảnh, audio, video bằng Google Gemini API. Hỗ trợ popup phân tích từ vựng, popup dịch và dịch nhanh.
  8. // @match *://*/*
  9. // @match file:///*
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_registerMenuCommand
  15. // @grant unsafeWindow
  16. // @inject-into auto
  17. // @connect generativelanguage.googleapis.com
  18. // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
  19. // @require https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js
  21. // @homepageURL https://github.com/king1x32/UserScripts
  22. // ==/UserScript==
  23. (function() {
  24. "use strict";
  25. const CONFIG = {
  26. API: {
  27. providers: {
  28. gemini: {
  29. baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
  30. models: {
  31. fast: [
  32. "gemini-2.0-flash-lite",
  33. "gemini-2.0-flash",
  34. "gemini-2.0-flash-exp",
  35. ],
  36. pro: ["gemini-2.0-pro-exp-02-05", "gemini-2.0-pro-exp"],
  37. vision: [
  38. "gemini-2.0-flash-thinking-exp-01-21",
  39. "gemini-2.0-flash-thinking-exp",
  40. ],
  41. },
  42. headers: { "Content-Type": "application/json" },
  43. body: (prompt) => ({
  44. contents: [
  45. {
  46. parts: [{ text: prompt }],
  47. },
  48. ],
  49. generationConfig: { temperature: 0.7 },
  50. }),
  51. responseParser: (response) => {
  52. console.log("Parsing response:", response);
  53. if (typeof response === "string") {
  54. return response;
  55. }
  56. if (response?.candidates?.[0]?.content?.parts?.[0]?.text) {
  57. return response.candidates[0].content.parts[0].text;
  58. }
  59. throw new Error("Không thể đọc kết quả từ API");
  60. },
  61. },
  62. openai: {
  63. url: () => "https://api.groq.com/openai/v1/chat/completions",
  64. headers: (apiKey) => ({
  65. "Content-Type": "application/json",
  66. Authorization: `Bearer ${apiKey}`,
  67. }),
  68. body: (prompt) => ({
  69. model: "llama-3.3-70b-versatile",
  70. messages: [{ role: "user", content: prompt }],
  71. temperature: 0.7,
  72. }),
  73. responseParser: (response) => response.choices?.[0]?.message?.content,
  74. },
  75. },
  76. currentProvider: "gemini",
  77. apiKey: {
  78. gemini: [""],
  79. openai: [""],
  80. },
  81. currentKeyIndex: {
  82. gemini: 0,
  83. openai: 0,
  84. },
  85. maxRetries: 3,
  86. retryDelay: 1000,
  87. },
  88. OCR: {
  89. generation: {
  90. temperature: 0.2,
  91. topP: 0.7,
  92. topK: 20,
  93. },
  94. maxFileSize: 15 * 1024 * 1024, // 15MB
  95. supportedFormats: [
  96. "image/jpeg",
  97. "image/png",
  98. "image/webp",
  99. "image/heic",
  100. "image/heif",
  101. ],
  102. },
  103. MEDIA: {
  104. generation: {
  105. temperature: 0.2,
  106. topP: 0.7,
  107. topK: 20,
  108. },
  109. audio: {
  110. maxSize: 100 * 1024 * 1024, // 100MB
  111. supportedFormats: [
  112. "audio/wav",
  113. "audio/mp3",
  114. "audio/ogg",
  115. "audio/m4a",
  116. "audio/aac",
  117. "audio/flac",
  118. "audio/wma",
  119. "audio/opus",
  120. "audio/amr",
  121. "audio/midi",
  122. "audio/mpa",
  123. ],
  124. },
  125. video: {
  126. maxSize: 200 * 1024 * 1024, // 200MB
  127. supportedFormats: [
  128. "video/mp4",
  129. "video/webm",
  130. "video/ogg",
  131. "video/x-msvideo",
  132. "video/quicktime",
  133. "video/x-ms-wmv",
  134. "video/x-flv",
  135. "video/3gpp",
  136. "video/3gpp2",
  137. "video/x-matroska",
  138. ],
  139. },
  140. },
  141. contextMenu: {
  142. enabled: true,
  143. },
  144. pageTranslation: {
  145. enabled: true, // Bật/tắt tính năng
  146. autoTranslate: true,
  147. showInitialButton: true, // Hiện nút dịch ban đầu
  148. buttonTimeout: 10000, // Thời gian hiển thị nút (10 giây)
  149. useCustomSelectors: false,
  150. customSelectors: [],
  151. defaultSelectors: [
  152. "script",
  153. "code",
  154. "style",
  155. "input",
  156. "button",
  157. "textarea",
  158. ".notranslate",
  159. ".translator-settings-container",
  160. ".translator-tools-container",
  161. ".translation-div",
  162. ".draggable",
  163. ".page-translate-button",
  164. ".translator-tools-dropdown",
  165. ".translator-notification",
  166. ".translator-content",
  167. ".translator-context-menu",
  168. ".translator-overlay",
  169. ".translator-guide",
  170. ".center-translate-status",
  171. ".no-translate",
  172. "[data-notranslate]",
  173. "[translate='no']",
  174. ".html5-player-chrome",
  175. ".html5-video-player",
  176. ],
  177. },
  178. promptSettings: {
  179. enabled: true,
  180. customPrompts: {
  181. normal: "",
  182. advanced: "",
  183. chinese: "",
  184. ocr: "",
  185. media: "",
  186. page: "",
  187. },
  188. useCustom: false,
  189. },
  190. CACHE: {
  191. text: {
  192. maxSize: 100, // Tối đa 100 entries cho text
  193. expirationTime: 300000, // 5 phút
  194. },
  195. image: {
  196. maxSize: 25, // Tối đa 25 entries cho ảnh
  197. expirationTime: 1800000, // 30 phút
  198. },
  199. media: {
  200. maxSize: 25, // Số lượng media được cache tối đa
  201. expirationTime: 1800000, // 30 phút
  202. },
  203. },
  204. RATE_LIMIT: {
  205. maxRequests: 5,
  206. perMilliseconds: 10000,
  207. },
  208. THEME: {
  209. mode: "dark",
  210. light: {
  211. background: "#cccccc",
  212. backgroundShadow: "rgba(255, 255, 255, 0.05)",
  213. text: "#333333",
  214. border: "#bbb",
  215. title: "#202020",
  216. content: "#555",
  217. button: {
  218. close: { background: "#ff4444", text: "#ddd" },
  219. translate: { background: "#007BFF", text: "#ddd" },
  220. },
  221. },
  222. dark: {
  223. background: "#333333",
  224. backgroundShadow: "rgba(0, 0, 0, 0.05)",
  225. text: "#cccccc",
  226. border: "#555",
  227. title: "#eeeeee",
  228. content: "#bbb",
  229. button: {
  230. close: { background: "#aa2222", text: "#ddd" },
  231. translate: { background: "#004a99", text: "#ddd" },
  232. },
  233. },
  234. },
  235. STYLES: {
  236. translation: {
  237. marginTop: "10px",
  238. padding: "10px",
  239. backgroundColor: "#f0f0f0",
  240. borderLeft: "3px solid #4CAF50",
  241. borderRadius: "8px",
  242. color: "#333",
  243. position: "relative",
  244. fontFamily: "SF Pro Rounded, sans-serif",
  245. fontSize: "16px",
  246. zIndex: "2147483647",
  247. },
  248. popup: {
  249. position: "fixed",
  250. border: "1px solid",
  251. padding: "20px",
  252. zIndex: "2147483647",
  253. maxWidth: "90vw",
  254. minWidth: "300px",
  255. maxHeight: "80vh",
  256. boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
  257. borderRadius: "15px",
  258. fontFamily: "SF Pro Rounded, Arial, sans-serif",
  259. fontSize: "16px",
  260. top: "50%",
  261. left: "50%",
  262. transform: "translate(-50%, -50%)",
  263. display: "flex",
  264. flexDirection: "column",
  265. overflowY: "auto",
  266. },
  267. button: {
  268. position: "fixed",
  269. border: "none",
  270. borderRadius: "8px",
  271. padding: "5px 10px",
  272. cursor: "pointer",
  273. zIndex: "2147483647",
  274. fontSize: "14px",
  275. },
  276. dragHandle: {
  277. padding: "10px",
  278. borderBottom: "1px solid",
  279. cursor: "move",
  280. userSelect: "none",
  281. display: "flex",
  282. justifyContent: "space-between",
  283. alignItems: "center",
  284. borderTopLeftRadius: "15px",
  285. borderTopRightRadius: "15px",
  286. zIndex: "2147483647",
  287. },
  288. },
  289. };
  290. const DEFAULT_SETTINGS = {
  291. theme: CONFIG.THEME.mode,
  292. apiProvider: CONFIG.API.currentProvider,
  293. apiKey: {
  294. gemini: [""],
  295. openai: [""],
  296. },
  297. currentKeyIndex: {
  298. gemini: 0,
  299. openai: 0,
  300. },
  301. geminiOptions: {
  302. modelType: "fast", // 'fast', 'pro', 'vision', 'custom'
  303. fastModel: "gemini-2.0-flash-lite",
  304. proModel: "gemini-2.0-pro-exp-02-05",
  305. visionModel: "gemini-2.0-flash-thinking-exp-01-21",
  306. customModel: "",
  307. },
  308. contextMenu: {
  309. enabled: true,
  310. },
  311. promptSettings: {
  312. enabled: true,
  313. customPrompts: {
  314. normal: "",
  315. advanced: "",
  316. chinese: "",
  317. ocr: "",
  318. media: "",
  319. page: "",
  320. },
  321. useCustom: false,
  322. },
  323. inputTranslation: {
  324. enabled: true,
  325. excludeSelectors: [], // Selectors để loại trừ
  326. },
  327. pageTranslation: {
  328. enabled: true,
  329. autoTranslate: true,
  330. showInitialButton: true, // Hiện nút dịch ban đầu
  331. buttonTimeout: 10000, // Thời gian hiển thị nút (10 giây)
  332. useCustomSelectors: false,
  333. customSelectors: [],
  334. defaultSelectors: [
  335. "script",
  336. "code",
  337. "style",
  338. "input",
  339. "button",
  340. "textarea",
  341. ".notranslate",
  342. ".translator-settings-container",
  343. ".translator-tools-container",
  344. ".translation-div",
  345. ".draggable",
  346. ".page-translate-button",
  347. ".translator-tools-dropdown",
  348. ".translator-notification",
  349. ".translator-content",
  350. ".translator-context-menu",
  351. ".translator-overlay",
  352. ".translator-guide",
  353. ".center-translate-status",
  354. ".no-translate",
  355. "[data-notranslate]",
  356. "[translate='no']",
  357. ".html5-player-chrome",
  358. ".html5-video-player",
  359. ],
  360. },
  361. ocrOptions: {
  362. enabled: true,
  363. preferredProvider: CONFIG.API.currentProvider,
  364. displayType: "popup",
  365. maxFileSize: CONFIG.OCR.maxFileSize,
  366. temperature: CONFIG.OCR.generation.temperature,
  367. topP: CONFIG.OCR.generation.topP,
  368. topK: CONFIG.OCR.generation.topK,
  369. },
  370. mediaOptions: {
  371. enabled: true,
  372. temperature: CONFIG.MEDIA.generation.temperature,
  373. topP: CONFIG.MEDIA.generation.topP,
  374. topK: CONFIG.MEDIA.generation.topK,
  375. audio: {
  376. processingInterval: 2000, // 2 seconds
  377. bufferSize: 16384,
  378. format: {
  379. sampleRate: 44100,
  380. numChannels: 1,
  381. bitsPerSample: 16,
  382. },
  383. },
  384. },
  385. displayOptions: {
  386. fontSize: "16px",
  387. minPopupWidth: "300px",
  388. maxPopupWidth: "90vw",
  389. webImageTranslation: {
  390. fontSize: "9px", // Font size mặc định
  391. minFontSize: "8px",
  392. maxFontSize: "16px",
  393. },
  394. translationMode: "parallel", // 'translation_only', 'parallel' hoặc 'language_learning'
  395. sourceLanguage: "auto", // 'auto' hoặc 'zh','en','vi',...
  396. targetLanguage: "vi", // 'vi', 'en', 'zh', 'ko', 'ja',...
  397. languageLearning: {
  398. showSource: true,
  399. },
  400. },
  401. shortcuts: {
  402. settingsEnabled: true,
  403. enabled: true,
  404. pageTranslate: { key: "f", altKey: true },
  405. inputTranslate: { key: "t", altKey: true },
  406. quickTranslate: { key: "q", altKey: true },
  407. popupTranslate: { key: "e", altKey: true },
  408. advancedTranslate: { key: "a", altKey: true },
  409. },
  410. clickOptions: {
  411. enabled: true,
  412. singleClick: { translateType: "popup" },
  413. doubleClick: { translateType: "quick" },
  414. hold: { translateType: "advanced" },
  415. },
  416. touchOptions: {
  417. enabled: true,
  418. sensitivity: 100,
  419. twoFingers: { translateType: "popup" },
  420. threeFingers: { translateType: "advanced" },
  421. fourFingers: { translateType: "quick" },
  422. },
  423. cacheOptions: {
  424. text: {
  425. enabled: true,
  426. maxSize: CONFIG.CACHE.text.maxSize,
  427. expirationTime: CONFIG.CACHE.text.expirationTime,
  428. },
  429. image: {
  430. enabled: true,
  431. maxSize: CONFIG.CACHE.image.maxSize,
  432. expirationTime: CONFIG.CACHE.image.expirationTime,
  433. },
  434. media: {
  435. enabled: true,
  436. maxSize: CONFIG.CACHE.media.maxSize,
  437. expirationTime: CONFIG.CACHE.media.expirationTime,
  438. },
  439. },
  440. rateLimit: {
  441. maxRequests: CONFIG.RATE_LIMIT.maxRequests,
  442. perMilliseconds: CONFIG.RATE_LIMIT.perMilliseconds,
  443. },
  444. };
  445. class NetworkOptimizer {
  446. constructor() {
  447. this.queue = [];
  448. this.processing = false;
  449. this.retryDelays = [1000, 2000, 4000];
  450. this.batchSize = 6; // Số api đa luồng
  451. this.batchDelay = 100;
  452. }
  453. async optimizeRequest(request) {
  454. const priority = this.calculatePriority(request);
  455. const optimizedRequest = {
  456. ...request,
  457. priority,
  458. retries: 0,
  459. timestamp: Date.now(),
  460. };
  461. this.queue.push(optimizedRequest);
  462. if (!this.processing) {
  463. this.processQueue();
  464. }
  465. }
  466. async batchRequests(requests) {
  467. const batches = [];
  468. for (let i = 0; i < requests.length; i += this.batchSize) {
  469. batches.push(requests.slice(i, i + this.batchSize));
  470. }
  471. const results = [];
  472. for (const batch of batches) {
  473. const batchResults = await Promise.all(
  474. batch.map((req) => this.executeRequest(req))
  475. );
  476. results.push(...batchResults);
  477. if (batches.indexOf(batch) < batches.length - 1) {
  478. await new Promise((resolve) => setTimeout(resolve, this.batchDelay));
  479. }
  480. }
  481. return results;
  482. }
  483. calculatePriority(request) {
  484. if (request.urgent) return 3;
  485. if (request.type === "translation") return 2;
  486. return 1;
  487. }
  488. async processQueue() {
  489. this.processing = true;
  490. while (this.queue.length > 0) {
  491. const batch = this.createBatch();
  492. await this.processBatch(batch);
  493. }
  494. this.processing = false;
  495. }
  496. createBatch() {
  497. return this.queue
  498. .sort((a, b) => b.priority - a.priority)
  499. .slice(0, this.batchSize);
  500. }
  501. async processBatch(batch) {
  502. try {
  503. const results = await this.batchRequests(batch);
  504. this.queue = this.queue.filter((req) => !batch.includes(req));
  505. return results;
  506. } catch (error) {
  507. console.error("Batch processing error:", error);
  508. throw error;
  509. }
  510. }
  511. async executeRequest(request) {
  512. try {
  513. const response = await fetch(request.url, request.options);
  514. if (!response.ok) {
  515. throw new Error(`HTTP error! status: ${response.status}`);
  516. }
  517. return await response.json();
  518. } catch (error) {
  519. if (request.retries < this.retryDelays.length) {
  520. await new Promise((resolve) =>
  521. setTimeout(resolve, this.retryDelays[request.retries])
  522. );
  523. request.retries++;
  524. return this.executeRequest(request);
  525. }
  526. throw error;
  527. }
  528. }
  529. }
  530. class MobileOptimizer {
  531. constructor(ui) {
  532. this.ui = ui;
  533. this.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  534. if (this.isMobile) {
  535. this.optimizeForMobile();
  536. }
  537. }
  538. optimizeForMobile() {
  539. this.reduceDOMOperations();
  540. this.optimizeTouchHandling();
  541. this.adjustUIForMobile();
  542. }
  543. reduceDOMOperations() {
  544. const observer = new MutationObserver((mutations) => {
  545. requestAnimationFrame(() => {
  546. mutations.forEach((mutation) => {
  547. if (mutation.type === "childList") {
  548. this.optimizeAddedNodes(mutation.addedNodes);
  549. }
  550. });
  551. });
  552. });
  553. observer.observe(document.body, {
  554. childList: true,
  555. subtree: true,
  556. });
  557. }
  558. optimizeTouchHandling() {
  559. let touchStartY = 0;
  560. let touchStartX = 0;
  561. document.addEventListener(
  562. "touchstart",
  563. (e) => {
  564. touchStartY = e.touches[0].clientY;
  565. touchStartX = e.touches[0].clientX;
  566. },
  567. { passive: true }
  568. );
  569. document.addEventListener(
  570. "touchmove",
  571. (e) => {
  572. const touchY = e.touches[0].clientY;
  573. const touchX = e.touches[0].clientX;
  574. if (
  575. Math.abs(touchY - touchStartY) > 10 ||
  576. Math.abs(touchX - touchStartX) > 10
  577. ) {
  578. this.ui.removeTranslateButton();
  579. }
  580. },
  581. { passive: true }
  582. );
  583. }
  584. adjustUIForMobile() {
  585. const style = document.createElement("style");
  586. style.textContent = `
  587. .translator-tools-container {
  588. bottom: 25px !important;
  589. right: 5px !important;
  590. }
  591. .translator-tools-button {
  592. padding: 8px 15px !important;
  593. font-size: 14px !important;
  594. }
  595. .translator-tools-dropdown {
  596. min-width: 195px !important;
  597. max-height: 60vh !important;
  598. overflow-y: auto !important;
  599. }
  600. .translator-tools-item {
  601. padding: 10px !important;
  602. }
  603. .draggable {
  604. max-width: 95vw !important;
  605. max-height: 80vh !important;
  606. }
  607. `;
  608. document.head.appendChild(style);
  609. }
  610. optimizeAddedNodes(nodes) {
  611. nodes.forEach((node) => {
  612. if (node.nodeType === Node.ELEMENT_NODE) {
  613. const images = node.getElementsByTagName("img");
  614. Array.from(images).forEach((img) => {
  615. if (!img.loading) img.loading = "lazy";
  616. });
  617. }
  618. });
  619. }
  620. }
  621. // const bypassCSP = () => {
  622. // const style = document.createElement("style");
  623. // style.textContent = `
  624. // .translator-tools-container {
  625. // position: fixed !important;
  626. // bottom: 40px !important;
  627. // right: 25px !important;
  628. // z-index: 2147483647 !important;
  629. // font-family: Arial, sans-serif !important;
  630. // display: block !important;
  631. // visibility: visible !important;
  632. // opacity: 1 !important;
  633. // }
  634. // `;
  635. // document.head.appendChild(style);
  636. // };
  637. class UserSettings {
  638. constructor(translator) {
  639. this.translator = translator;
  640. this.settings = this.loadSettings();
  641. this.isSettingsUIOpen = false;
  642. }
  643. createSettingsUI() {
  644. if (this.isSettingsUIOpen) {
  645. return;
  646. }
  647. this.isSettingsUIOpen = true;
  648. const container = document.createElement("div");
  649. const themeMode = this.settings.theme ? this.settings.theme : CONFIG.THEME.mode;
  650. const theme = CONFIG.THEME[themeMode];
  651. const isDark = themeMode === "dark";
  652. const geminiModels = {
  653. fast: CONFIG.API.providers.gemini.models.fast || [],
  654. pro: CONFIG.API.providers.gemini.models.pro || [],
  655. vision: CONFIG.API.providers.gemini.models.vision || [],
  656. };
  657. const resetStyle = `
  658. * {
  659. all: revert;
  660. box-sizing: border-box !important;
  661. font-family: Arial, sans-serif !important;
  662. margin: 0;
  663. padding: 0;
  664. }
  665. .settings-grid {
  666. display: grid !important;
  667. grid-template-columns: 47% 53% !important;
  668. align-items: center !important;
  669. gap: 10px !important;
  670. margin-bottom: 8px !important;
  671. }
  672. .settings-label {
  673. min-width: 100px !important;
  674. text-align: left !important;
  675. padding-right: 10px !important;
  676. }
  677. .settings-input {
  678. min-width: 100px !important;
  679. margin-left: 5px !important;
  680. }
  681. h2 {
  682. flex: 1 !important;
  683. display: flex !important;
  684. font-family: Arial, sans-serif !important;
  685. align-items: center !important;
  686. justify-content: center !important;
  687. margin-bottom: 15px;
  688. font-weight: bold;
  689. color: ${theme.title} !important;
  690. grid-column: 1 / -1 !important;
  691. }
  692. h3 {
  693. font-family: Arial, sans-serif !important;
  694. margin-bottom: 15px;
  695. font-weight: bold;
  696. color: ${theme.title} !important;
  697. grid-column: 1 / -1 !important;
  698. }
  699. h4 {
  700. color: ${theme.title} !important;
  701. }
  702. input[type="radio"],
  703. input[type="checkbox"] {
  704. align-items: center !important;
  705. justify-content: center !important;
  706. }
  707. button {
  708. font-family: Arial, sans-serif !important;
  709. font-size: 14px !important;
  710. background-color: ${isDark ? "#444" : "#ddd"};
  711. color: ${isDark ? "#ddd" : "#000"} !important;
  712. padding: 5px 15px !important;
  713. border-radius: 8px !important;
  714. cursor: pointer !important;
  715. border: none !important;
  716. margin: 5px !important;
  717. }
  718. #cancelSettings {
  719. background-color: ${isDark ? "#666" : "#ddd"} !important;
  720. color: ${isDark ? "#ddd" : "#000"} !important;
  721. padding: 5px 15px !important;
  722. border-radius: 8px !important;
  723. cursor: pointer !important;
  724. border: none !important;
  725. margin: 5px !important;
  726. }
  727. #cancelSettings:hover {
  728. background-color: ${isDark ? "#888" : "#aaa"} !important;
  729. }
  730. #saveSettings {
  731. background-color: #007BFF !important;
  732. padding: 5px 15px !important;
  733. border-radius: 8px !important;
  734. cursor: pointer !important;
  735. border: none !important;
  736. margin: 5px !important;
  737. }
  738. #saveSettings:hover {
  739. background-color: #009ddd !important;
  740. }
  741. button {
  742. font-family: Arial, sans-serif !important;
  743. font-size: 14px !important;
  744. border: none !important;
  745. border-radius: 8px !important;
  746. cursor: pointer !important;
  747. transition: all 0.2s ease !important;
  748. font-weight: 500 !important;
  749. letter-spacing: 0.3px !important;
  750. }
  751. button:hover {
  752. transform: translateY(-2px) !important;
  753. box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
  754. }
  755. button:active {
  756. transform: translateY(0) !important;
  757. }
  758. #exportSettings:hover {
  759. background-color: #218838 !important;
  760. }
  761. #importSettings:hover {
  762. background-color: #138496 !important;
  763. }
  764. #cancelSettings:hover {
  765. background-color: ${isDark ? "#777" : "#dae0e5"} !important;
  766. }
  767. #saveSettings:hover {
  768. background-color: #0056b3 !important;
  769. }
  770. @keyframes buttonPop {
  771. 0% { transform: scale(1); }
  772. 50% { transform: scale(0.98); }
  773. 100% { transform: scale(1); }
  774. }
  775. button:active {
  776. animation: buttonPop 0.2s ease;
  777. }
  778. .radio-group {
  779. display: flex !important;
  780. gap: 15px !important;
  781. }
  782. .radio-group label {
  783. flex: 1 !important;
  784. display: flex !important;
  785. color: ${isDark ? "#ddd" : "#000"} !important;
  786. align-items: center !important;
  787. justify-content: center !important;
  788. padding: 5px !important;
  789. }
  790. .radio-group input[type="radio"] {
  791. margin-right: 5px !important;
  792. }
  793. .shortcut-container {
  794. display: flex !important;
  795. align-items: center !important;
  796. gap: 8px !important;
  797. }
  798. .shortcut-prefix {
  799. white-space: nowrap !important;
  800. color: ${isDark ? "#aaa" : "#555"};
  801. font-size: 14px !important;
  802. min-width: 45px !important;
  803. }
  804. .shortcut-input {
  805. flex: 1 !important;
  806. min-width: 60px !important;
  807. max-width: 100px !important;
  808. }
  809. .prompt-textarea {
  810. width: 100%;
  811. min-height: 100px;
  812. margin: 5px 0;
  813. padding: 8px;
  814. background-color: ${isDark ? "#444" : "#fff"};
  815. color: ${isDark ? "#fff" : "#000"};
  816. border: 1px solid ${isDark ? "#666" : "#ccc"};
  817. border-radius: 8px;
  818. font-family: monospace;
  819. font-size: 13px;
  820. resize: vertical;
  821. }
  822. `;
  823. const styleElement = document.createElement("style");
  824. styleElement.textContent = resetStyle;
  825. container.appendChild(styleElement);
  826. container.innerHTML += `
  827. <h2>Cài đặt Translator AI</h2>
  828. <div style="margin-bottom: 15px;">
  829. <h3>GIAO DIN</h3>
  830. <div class="radio-group">
  831. <label>
  832. <input type="radio" name="theme" value="light" ${!isDark ? "checked" : ""
  833. }>
  834. <span class="settings-label">Sáng</span>
  835. </label>
  836. <label>
  837. <input type="radio" name="theme" value="dark" ${isDark ? "checked" : ""}>
  838. <span class="settings-label">Ti</span>
  839. </label>
  840. </div>
  841. </div>
  842. <div style="margin-bottom: 15px;">
  843. <h3>API PROVIDER</h3>
  844. <div class="radio-group">
  845. <label>
  846. <input type="radio" name="apiProvider" value="gemini" ${this.settings.apiProvider === "gemini" ? "checked" : ""
  847. }>
  848. <span class="settings-label">Gemini</span>
  849. </label>
  850. <label>
  851. <input type="radio" name="apiProvider" value="openai" disabled>
  852. <span class="settings-label">OpenAI</span>
  853. </label>
  854. </div>
  855. </div>
  856. <div style="margin-bottom: 15px;">
  857. <h3>API KEYS</h3>
  858. <div id="geminiKeys" style="margin-bottom: 10px;">
  859. <h4 class="settings-label" style="margin-bottom: 5px;">Gemini API Keys</h4>
  860. <div class="api-keys-container">
  861. ${this.settings.apiKey.gemini
  862. .map(
  863. (key) => `
  864. <div class="api-key-entry" style="display: flex; gap: 10px; margin-bottom: 5px;">
  865. <input type="text" class="gemini-key" value="${key}" style="flex: 1; width: 100%; border-radius: 6px !important; margin-left: 5px;">
  866. <button class="remove-key" data-provider="gemini" data-index="${this.settings.apiKey.gemini.indexOf(
  867. key
  868. )}" style="background-color: #ff4444;">×</button>
  869. </div>
  870. `
  871. )
  872. .join("")}
  873. </div>
  874. <button id="addGeminiKey" class="settings-label" style="background-color: #28a745; margin-top: 5px;">+ Add Gemini Key</button>
  875. </div>
  876. <div id="openaiKeys" style="margin-bottom: 10px;">
  877. <h4 class="settings-label" style="margin-bottom: 5px;">OpenAI API Keys</h4>
  878. <div class="api-keys-container">
  879. ${this.settings.apiKey.openai
  880. .map(
  881. (key) => `
  882. <div class="api-key-entry" style="display: flex; gap: 10px; margin-bottom: 5px;">
  883. <input type="text" class="openai-key" value="${key}" style="flex: 1; width: 100%; border-radius: 6px !important; margin-left: 5px;">
  884. <button class="remove-key" data-provider="openai" data-index="${this.settings.apiKey.openai.indexOf(
  885. key
  886. )}" style="background-color: #ff4444;">×</button>
  887. </div>
  888. `
  889. )
  890. .join("")}
  891. </div>
  892. <button id="addOpenaiKey" class="settings-label" style="background-color: #28a745; margin-top: 5px;">+ Add OpenAI Key</button>
  893. </div>
  894. </div>
  895. <div style="margin-bottom: 15px;">
  896. <h3>MODEL GEMINI</h3>
  897. <div class="settings-grid">
  898. <span class="settings-label">S dng loi model:</span>
  899. <select id="geminiModelType" class="settings-input">
  900. <option value="fast" ${this.settings.geminiOptions?.modelType === "fast" ? "selected" : ""
  901. }>Nhanh</option>
  902. <option value="pro" ${this.settings.geminiOptions?.modelType === "pro" ? "selected" : ""
  903. }>Pro</option>
  904. <option value="vision" ${this.settings.geminiOptions?.modelType === "vision" ? "selected" : ""
  905. }>Suy lun</option>
  906. <option value="custom" ${this.settings.geminiOptions?.modelType === "custom" ? "selected" : ""
  907. }>Tùy chnh</option>
  908. </select>
  909. </div>
  910. <div id="fastModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "fast"
  911. ? 'style="display: none;"'
  912. : ""
  913. }>
  914. <span class="settings-label">Model Nhanh:</span>
  915. <select id="fastModel" class="settings-input">
  916. ${geminiModels.fast
  917. .map(
  918. (model) => `
  919. <option value="${model}" ${this.settings.geminiOptions?.fastModel === model ? "selected" : ""
  920. }>${model}</option>
  921. `
  922. )
  923. .join("")}
  924. </select>
  925. </div>
  926. <div id="proModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "pro"
  927. ? 'style="display: none;"'
  928. : ""
  929. }>
  930. <span class="settings-label">Model Chuyên nghip:</span>
  931. <select id="proModel" class="settings-input">
  932. ${geminiModels.pro
  933. .map(
  934. (model) => `
  935. <option value="${model}" ${this.settings.geminiOptions?.proModel === model ? "selected" : ""
  936. }>${model}</option>
  937. `
  938. )
  939. .join("")}
  940. </select>
  941. </div>
  942. <div id="visionModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "vision"
  943. ? 'style="display: none;"'
  944. : ""
  945. }>
  946. <span class="settings-label">Model Suy lun:</span>
  947. <select id="visionModel" class="settings-input">
  948. ${geminiModels.vision
  949. .map(
  950. (model) => `
  951. <option value="${model}" ${this.settings.geminiOptions?.visionModel === model ? "selected" : ""
  952. }>${model}</option>
  953. `
  954. )
  955. .join("")}
  956. </select>
  957. </div>
  958. <div id="customModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "custom"
  959. ? 'style="display: none;"'
  960. : ""
  961. }>
  962. <span class="settings-label">Model tùy chnh:</span>
  963. <input type="text" id="customModel" class="settings-input" value="${this.settings.geminiOptions?.customModel || ""
  964. }"
  965. placeholder="Nhập tên model">
  966. </div>
  967. </div>
  968. <div style="margin-bottom: 15px;">
  969. <h3>DCH KHI VIT</h3>
  970. <div class="settings-grid">
  971. <span class="settings-label">Bt tính năng:</span>
  972. <input type="checkbox" id="inputTranslationEnabled"
  973. ${this.settings.inputTranslation?.enabled ? "checked" : ""}>
  974. </div>
  975. </div>
  976. <div style="margin-bottom: 15px;">
  977. <h3>TOOLS DCH</h3>
  978. <div class="settings-grid">
  979. <span class="settings-label">Hin th Tools ⚙️</span>
  980. <input type="checkbox" id="showTranslatorTools"
  981. ${localStorage.getItem("translatorToolsEnabled") === "true"
  982. ? "checked"
  983. : ""
  984. }>
  985. </div>
  986. </div>
  987. <div style="margin-bottom: 15px;">
  988. <h3>DCH TOÀN TRANG</h3>
  989. <div class="settings-grid">
  990. <span class="settings-label">Bt tính năng dch trang:</span>
  991. <input type="checkbox" id="pageTranslationEnabled" ${this.settings.pageTranslation?.enabled ? "checked" : ""
  992. }>
  993. </div>
  994. <div class="settings-grid">
  995. <span class="settings-label">Hin nút dch 10s đầu:</span>
  996. <input type="checkbox" id="showInitialButton" ${this.settings.pageTranslation?.showInitialButton ? "checked" : ""
  997. }>
  998. </div>
  999. <div class="settings-grid">
  1000. <span class="settings-label">T động dch trang:</span>
  1001. <input type="checkbox" id="autoTranslatePage" ${this.settings.pageTranslation?.autoTranslate ? "checked" : ""
  1002. }>
  1003. </div>
  1004. <div class="settings-grid">
  1005. <span class="settings-label">Tùy chnh Selectors loi trừ:</span>
  1006. <input type="checkbox" id="useCustomSelectors" ${this.settings.pageTranslation?.useCustomSelectors ? "checked" : ""
  1007. }>
  1008. </div>
  1009. <div id="selectorsSettings" style="display: ${this.settings.pageTranslation?.useCustomSelectors ? "block" : "none"
  1010. }">
  1011. <div class="settings-grid" style="align-items: start !important;">
  1012. <span class="settings-label">Selectors loi trừ:</span>
  1013. <div style="flex: 1;">
  1014. <textarea id="customSelectors"
  1015. style="width: 100%; min-height: 100px; margin: 5px 0; padding: 8px;
  1016. background-color: ${isDark ? "#444" : "#fff"};
  1017. color: ${isDark ? "#fff" : "#000"};
  1018. border: 1px solid ${isDark ? "#666" : "#ccc"};
  1019. border-radius: 8px;
  1020. font-family: monospace;
  1021. font-size: 13px;"
  1022. >${this.settings.pageTranslation?.customSelectors?.join("\n") || ""
  1023. }</textarea>
  1024. <div style="font-size: 12px; color: ${isDark ? "#999" : "#666"
  1025. }; margin-top: 4px;">
  1026. Hãy nhp mi selector mt dòng!
  1027. </div>
  1028. </div>
  1029. </div>
  1030. <div class="settings-grid" style="align-items: start !important;">
  1031. <span class="settings-label">Selectors mc định:</span>
  1032. <div style="flex: 1;">
  1033. <textarea id="defaultSelectors" readonly
  1034. style="width: 100%; min-height: 100px; margin: 5px 0; padding: 8px;
  1035. background-color: ${isDark ? "#333" : "#f5f5f5"};
  1036. color: ${isDark ? "#999" : "#666"};
  1037. border: 1px solid ${isDark ? "#555" : "#ddd"};
  1038. border-radius: 8px;
  1039. font-family: monospace;
  1040. font-size: 13px;"
  1041. >${this.settings.pageTranslation?.defaultSelectors?.join("\n") || ""
  1042. }</textarea>
  1043. <div style="font-size: 12px; color: ${isDark ? "#999" : "#666"
  1044. }; margin-top: 4px;">
  1045. Đây là danh sách selectors mc định s được s dng khi tt tùy chnh.
  1046. </div>
  1047. </div>
  1048. </div>
  1049. <div class="settings-grid">
  1050. <span class="settings-label">Kết hp vi mc định:</span>
  1051. <input type="checkbox" id="combineWithDefault" ${this.settings.pageTranslation?.combineWithDefault ? "checked" : ""
  1052. }>
  1053. <div style="font-size: 12px; color: ${isDark ? "#999" : "#666"
  1054. }; margin-top: 4px; grid-column: 2;">
  1055. Nếu bt, selectors tùy chnh s được thêm vào danh sách mc định thay vì thay thế hoàn toàn.
  1056. </div>
  1057. </div>
  1058. </div>
  1059. </div>
  1060. <div style="margin-bottom: 15px;">
  1061. <h3>TÙY CHNH PROMPT</h3>
  1062. <div class="settings-grid">
  1063. <span class="settings-label">S dng prompt tùy chnh:</span>
  1064. <input type="checkbox" id="useCustomPrompt" ${this.settings.promptSettings?.useCustom ? "checked" : ""
  1065. }>
  1066. </div>
  1067. <div id="promptSettings" style="display: ${this.settings.promptSettings?.useCustom ? "block" : "none"
  1068. }">
  1069. <!-- Normal prompts -->
  1070. <div class="settings-grid" style="align-items: start !important;">
  1071. <span class="settings-label">Prompt dch thường (nhanh + popup):</span>
  1072. <textarea id="normalPrompt" class="prompt-textarea"
  1073. placeholder="Nhập prompt cho dịch thường..."
  1074. >${this.settings.promptSettings?.customPrompts?.normal || ""}</textarea>
  1075. </div>
  1076. <div class="settings-grid" style="align-items: start !important;">
  1077. <span class="settings-label">Prompt dch thường (nhanh + popup)(Chinese):</span>
  1078. <textarea id="normalPrompt_chinese" class="prompt-textarea"
  1079. placeholder="Nhập prompt cho dịch thường với pinyin..."
  1080. >${this.settings.promptSettings?.customPrompts?.normal_chinese || ""
  1081. }</textarea>
  1082. </div>
  1083. <!-- Advanced prompts -->
  1084. <div class="settings-grid" style="align-items: start !important;">
  1085. <span class="settings-label">Prompt dch nâng cao:</span>
  1086. <textarea id="advancedPrompt" class="prompt-textarea"
  1087. placeholder="Nhập prompt cho dịch nâng cao..."
  1088. >${this.settings.promptSettings?.customPrompts?.advanced || ""}</textarea>
  1089. </div>
  1090. <div class="settings-grid" style="align-items: start !important;">
  1091. <span class="settings-label">Prompt dch nâng cao (Chinese):</span>
  1092. <textarea id="advancedPrompt_chinese" class="prompt-textarea"
  1093. placeholder="Nhập prompt cho dịch nâng cao với pinyin..."
  1094. >${this.settings.promptSettings?.customPrompts?.advanced_chinese || ""
  1095. }</textarea>
  1096. </div>
  1097. <!-- OCR prompts -->
  1098. <div class="settings-grid" style="align-items: start !important;">
  1099. <span class="settings-label">Prompt OCR:</span>
  1100. <textarea id="ocrPrompt" class="prompt-textarea"
  1101. placeholder="Nhập prompt cho OCR..."
  1102. >${this.settings.promptSettings?.customPrompts?.ocr || ""}</textarea>
  1103. </div>
  1104. <div class="settings-grid" style="align-items: start !important;">
  1105. <span class="settings-label">Prompt OCR (Chinese):</span>
  1106. <textarea id="ocrPrompt_chinese" class="prompt-textarea"
  1107. placeholder="Nhập prompt cho OCR với pinyin..."
  1108. >${this.settings.promptSettings?.customPrompts?.ocr_chinese || ""
  1109. }</textarea>
  1110. </div>
  1111. <!-- Media prompts -->
  1112. <div class="settings-grid" style="align-items: start !important;">
  1113. <span class="settings-label">Prompt Media:</span>
  1114. <textarea id="mediaPrompt" class="prompt-textarea"
  1115. placeholder="Nhập prompt cho media..."
  1116. >${this.settings.promptSettings?.customPrompts?.media || ""}</textarea>
  1117. </div>
  1118. <div class="settings-grid" style="align-items: start !important;">
  1119. <span class="settings-label">Prompt Media (Chinese):</span>
  1120. <textarea id="mediaPrompt_chinese" class="prompt-textarea"
  1121. placeholder="Nhập prompt cho media với pinyin..."
  1122. >${this.settings.promptSettings?.customPrompts?.media_chinese || ""
  1123. }</textarea>
  1124. </div>
  1125. <!-- Page prompts -->
  1126. <div class="settings-grid" style="align-items: start !important;">
  1127. <span class="settings-label">Prompt dch trang:</span>
  1128. <textarea id="pagePrompt" class="prompt-textarea"
  1129. placeholder="Nhập prompt cho dịch trang..."
  1130. >${this.settings.promptSettings?.customPrompts?.page || ""}</textarea>
  1131. </div>
  1132. <div class="settings-grid" style="align-items: start !important;">
  1133. <span class="settings-label">Prompt dch trang (Chinese):</span>
  1134. <textarea id="pagePrompt_chinese" class="prompt-textarea"
  1135. placeholder="Nhập prompt cho dịch trang với pinyin..."
  1136. >${this.settings.promptSettings?.customPrompts?.page_chinese || ""
  1137. }</textarea>
  1138. </div>
  1139. <div style="margin-top: 10px; font-size: 12px; color: ${isDark ? "#999" : "#666"
  1140. };">
  1141. Các biến có th s dng trong prompt:
  1142. <ul style="margin-left: 20px;">
  1143. <li>{text} - Văn bn cn dch</li>
  1144. <li>{targetLang} - Ngôn ng đích</li>
  1145. <li>{sourceLang} - Ngôn ng ngun (nếu có)</li>
  1146. </ul>
  1147. </div>
  1148. </div>
  1149. </div>
  1150. <div style="margin-bottom: 15px;">
  1151. <h3>DCH VĂN BN TRONG NH</h3>
  1152. <div class="settings-grid">
  1153. <span class="settings-label">Bt OCR dch:</span>
  1154. <input type="checkbox" id="ocrEnabled" ${this.settings.ocrOptions?.enabled ? "checked" : ""
  1155. }>
  1156. </div>
  1157. <div class="settings-grid">
  1158. <span class="settings-label">Temperature:</span>
  1159. <input type="number" id="ocrTemperature" class="settings-input" value="${this.settings.ocrOptions.temperature
  1160. }"
  1161. min="0" max="1" step="0.1">
  1162. </div>
  1163. <div class="settings-grid">
  1164. <span class="settings-label">Top P:</span>
  1165. <input type="number" id="ocrTopP" class="settings-input" value="${this.settings.ocrOptions.topP
  1166. }" min="0" max="1"
  1167. step="0.1">
  1168. </div>
  1169. <div class="settings-grid">
  1170. <span class="settings-label">Top K:</span>
  1171. <input type="number" id="ocrTopK" class="settings-input" value="${this.settings.ocrOptions.topK
  1172. }" min="1"
  1173. max="100" step="1">
  1174. </div>
  1175. </div>
  1176. <div style="margin-bottom: 15px;">
  1177. <h3>DCH MEDIA</h3>
  1178. <div class="settings-grid">
  1179. <span class="settings-label">Bt dch Media:</span>
  1180. <input type="checkbox" id="mediaEnabled" ${this.settings.mediaOptions.enabled ? "checked" : ""
  1181. }>
  1182. </div>
  1183. <div class="settings-grid">
  1184. <span class="settings-label">Temperature:</span>
  1185. <input type="number" id="mediaTemperature" class="settings-input"
  1186. value="${this.settings.mediaOptions.temperature
  1187. }" min="0" max="1" step="0.1">
  1188. </div>
  1189. <div class="settings-grid">
  1190. <span class="settings-label">Top P:</span>
  1191. <input type="number" id="mediaTopP" class="settings-input" value="${this.settings.mediaOptions.topP
  1192. }" min="0"
  1193. max="1" step="0.1">
  1194. </div>
  1195. <div class="settings-grid">
  1196. <span class="settings-label">Top K:</span>
  1197. <input type="number" id="mediaTopK" class="settings-input" value="${this.settings.mediaOptions.topK
  1198. }" min="1"
  1199. max="100" step="1">
  1200. </div>
  1201. </div>
  1202. <div style="margin-bottom: 15px;">
  1203. <h3>HIN THỊ</h3>
  1204. <div class="settings-grid">
  1205. <span class="settings-label">Chế độ hin thị:</span>
  1206. <select id="displayMode" class="settings-input">
  1207. <option value="translation_only" ${this.settings.displayOptions.translationMode === "translation_only"
  1208. ? "selected"
  1209. : ""
  1210. }>Ch hin bn dch</option>
  1211. <option value="parallel" ${this.settings.displayOptions.translationMode === "parallel"
  1212. ? "selected"
  1213. : ""
  1214. }>Song song văn bn gc và bn dch</option>
  1215. <option value="language_learning" ${this.settings.displayOptions.translationMode === "language_learning"
  1216. ? "selected"
  1217. : ""
  1218. }>Chế độ hc ngôn ngữ</option>
  1219. </select>
  1220. </div>
  1221. <div class="settings-grid">
  1222. <span class="settings-label">Ngôn ng ngun:</span>
  1223. <select id="sourceLanguage" class="settings-input">
  1224. <option value="auto" ${this.settings.displayOptions.sourceLanguage === "auto" ? "selected" : ""
  1225. }>T động nhn din</option>
  1226. <option value="en" ${this.settings.displayOptions.sourceLanguage === "en" ? "selected" : ""
  1227. }>Tiếng Anh</option>
  1228. <option value="zh" ${this.settings.displayOptions.sourceLanguage === "zh" ? "selected" : ""
  1229. }>Tiếng Trung</option>
  1230. <option value="ko" ${this.settings.displayOptions.sourceLanguage === "ko" ? "selected" : ""
  1231. }>Tiếng Hàn</option>
  1232. <option value="ja" ${this.settings.displayOptions.sourceLanguage === "ja" ? "selected" : ""
  1233. }>Tiếng Nht</option>
  1234. <option value="fr" ${this.settings.displayOptions.sourceLanguage === "fr" ? "selected" : ""
  1235. }>Tiếng Pháp</option>
  1236. <option value="de" ${this.settings.displayOptions.sourceLanguage === "de" ? "selected" : ""
  1237. }>Tiếng Đức</option>
  1238. <option value="es" ${this.settings.displayOptions.sourceLanguage === "es" ? "selected" : ""
  1239. }>Tiếng Tây Ban Nha</option>
  1240. <option value="it" ${this.settings.displayOptions.sourceLanguage === "it" ? "selected" : ""
  1241. }>Tiếng Ý</option>
  1242. <option value="pt" ${this.settings.displayOptions.sourceLanguage === "pt" ? "selected" : ""
  1243. }>Tiếng B Đào Nha</option>
  1244. <option value="ru" ${this.settings.displayOptions.sourceLanguage === "ru" ? "selected" : ""
  1245. }>Tiếng Nga</option>
  1246. <option value="ar" ${this.settings.displayOptions.sourceLanguage === "ar" ? "selected" : ""
  1247. }>Tiếng Rp</option>
  1248. <option value="hi" ${this.settings.displayOptions.sourceLanguage === "hi" ? "selected" : ""
  1249. }>Tiếng Hindi</option>
  1250. <option value="bn" ${this.settings.displayOptions.sourceLanguage === "bn" ? "selected" : ""
  1251. }>Tiếng Bengal</option>
  1252. <option value="id" ${this.settings.displayOptions.sourceLanguage === "id" ? "selected" : ""
  1253. }>Tiếng Indonesia</option>
  1254. <option value="ms" ${this.settings.displayOptions.sourceLanguage === "ms" ? "selected" : ""
  1255. }>Tiếng Malaysia</option>
  1256. <option value="th" ${this.settings.displayOptions.sourceLanguage === "th" ? "selected" : ""
  1257. }>Tiếng Thái</option>
  1258. <option value="tr" ${this.settings.displayOptions.sourceLanguage === "tr" ? "selected" : ""
  1259. }>Tiếng Th Nhĩ Kỳ</option>
  1260. <option value="nl" ${this.settings.displayOptions.sourceLanguage === "nl" ? "selected" : ""
  1261. }>Tiếng Hà Lan</option>
  1262. <option value="pl" ${this.settings.displayOptions.sourceLanguage === "pl" ? "selected" : ""
  1263. }>Tiếng Ba Lan</option>
  1264. <option value="uk" ${this.settings.displayOptions.sourceLanguage === "uk" ? "selected" : ""
  1265. }>Tiếng Ukraine</option>
  1266. <option value="el" ${this.settings.displayOptions.sourceLanguage === "el" ? "selected" : ""
  1267. }>Tiếng Hy Lp</option>
  1268. <option value="cs" ${this.settings.displayOptions.sourceLanguage === "cs" ? "selected" : ""
  1269. }>Tiếng Séc</option>
  1270. <option value="da" ${this.settings.displayOptions.sourceLanguage === "da" ? "selected" : ""
  1271. }>Tiếng Đan Mch</option>
  1272. <option value="fi" ${this.settings.displayOptions.sourceLanguage === "fi" ? "selected" : ""
  1273. }>Tiếng Phn Lan</option>
  1274. <option value="he" ${this.settings.displayOptions.sourceLanguage === "he" ? "selected" : ""
  1275. }>Tiếng Do Thái</option>
  1276. <option value="hu" ${this.settings.displayOptions.sourceLanguage === "hu" ? "selected" : ""
  1277. }>Tiếng Hungary</option>
  1278. <option value="no" ${this.settings.displayOptions.sourceLanguage === "no" ? "selected" : ""
  1279. }>Tiếng Na Uy</option>
  1280. <option value="ro" ${this.settings.displayOptions.sourceLanguage === "ro" ? "selected" : ""
  1281. }>Tiếng Romania</option>
  1282. <option value="sv" ${this.settings.displayOptions.sourceLanguage === "sv" ? "selected" : ""
  1283. }>Tiếng Thy Đin</option>
  1284. <option value="ur" ${this.settings.displayOptions.sourceLanguage === "ur" ? "selected" : ""
  1285. }>Tiếng Urdu</option>
  1286. <option value="vi" ${this.settings.displayOptions.sourceLanguage === "vi" ? "selected" : ""
  1287. }>Tiếng Vit</option>
  1288. </select>
  1289. </div>
  1290. <div class="settings-grid">
  1291. <span class="settings-label">Ngôn ng đích:</span>
  1292. <select id="targetLanguage" class="settings-input">
  1293. <option value="vi" ${this.settings.displayOptions.targetLanguage === "vi" ? "selected" : ""
  1294. }>Tiếng Vit</option>
  1295. <option value="en" ${this.settings.displayOptions.targetLanguage === "en" ? "selected" : ""
  1296. }>Tiếng Anh</option>
  1297. <option value="zh" ${this.settings.displayOptions.targetLanguage === "zh" ? "selected" : ""
  1298. }>Tiếng Trung</option>
  1299. <option value="ko" ${this.settings.displayOptions.targetLanguage === "ko" ? "selected" : ""
  1300. }>Tiếng Hàn</option>
  1301. <option value="ja" ${this.settings.displayOptions.targetLanguage === "ja" ? "selected" : ""
  1302. }>Tiếng Nht</option>
  1303. <option value="fr" ${this.settings.displayOptions.targetLanguage === "fr" ? "selected" : ""
  1304. }>Tiếng Pháp</option>
  1305. <option value="de" ${this.settings.displayOptions.targetLanguage === "de" ? "selected" : ""
  1306. }>Tiếng Đức</option>
  1307. <option value="es" ${this.settings.displayOptions.targetLanguage === "es" ? "selected" : ""
  1308. }>Tiếng Tây Ban Nha</option>
  1309. <option value="it" ${this.settings.displayOptions.targetLanguage === "it" ? "selected" : ""
  1310. }>Tiếng Ý</option>
  1311. <option value="pt" ${this.settings.displayOptions.targetLanguage === "pt" ? "selected" : ""
  1312. }>Tiếng B Đào Nha</option>
  1313. <option value="ru" ${this.settings.displayOptions.targetLanguage === "ru" ? "selected" : ""
  1314. }>Tiếng Nga</option>
  1315. <option value="ar" ${this.settings.displayOptions.targetLanguage === "ar" ? "selected" : ""
  1316. }>Tiếng Rp</option>
  1317. <option value="hi" ${this.settings.displayOptions.targetLanguage === "hi" ? "selected" : ""
  1318. }>Tiếng Hindi</option>
  1319. <option value="bn" ${this.settings.displayOptions.targetLanguage === "bn" ? "selected" : ""
  1320. }>Tiếng Bengal</option>
  1321. <option value="id" ${this.settings.displayOptions.targetLanguage === "id" ? "selected" : ""
  1322. }>Tiếng Indonesia</option>
  1323. <option value="ms" ${this.settings.displayOptions.targetLanguage === "ms" ? "selected" : ""
  1324. }>Tiếng Malaysia</option>
  1325. <option value="th" ${this.settings.displayOptions.targetLanguage === "th" ? "selected" : ""
  1326. }>Tiếng Thái</option>
  1327. <option value="tr" ${this.settings.displayOptions.targetLanguage === "tr" ? "selected" : ""
  1328. }>Tiếng Th Nhĩ Kỳ</option>
  1329. <option value="nl" ${this.settings.displayOptions.targetLanguage === "nl" ? "selected" : ""
  1330. }>Tiếng Hà Lan</option>
  1331. <option value="pl" ${this.settings.displayOptions.targetLanguage === "pl" ? "selected" : ""
  1332. }>Tiếng Ba Lan</option>
  1333. <option value="uk" ${this.settings.displayOptions.targetLanguage === "uk" ? "selected" : ""
  1334. }>Tiếng Ukraine</option>
  1335. <option value="el" ${this.settings.displayOptions.targetLanguage === "el" ? "selected" : ""
  1336. }>Tiếng Hy Lp</option>
  1337. <option value="cs" ${this.settings.displayOptions.targetLanguage === "cs" ? "selected" : ""
  1338. }>Tiếng Séc</option>
  1339. <option value="da" ${this.settings.displayOptions.targetLanguage === "da" ? "selected" : ""
  1340. }>Tiếng Đan Mch</option>
  1341. <option value="fi" ${this.settings.displayOptions.targetLanguage === "fi" ? "selected" : ""
  1342. }>Tiếng Phn Lan</option>
  1343. <option value="he" ${this.settings.displayOptions.targetLanguage === "he" ? "selected" : ""
  1344. }>Tiếng Do Thái</option>
  1345. <option value="hu" ${this.settings.displayOptions.targetLanguage === "hu" ? "selected" : ""
  1346. }>Tiếng Hungary</option>
  1347. <option value="no" ${this.settings.displayOptions.targetLanguage === "no" ? "selected" : ""
  1348. }>Tiếng Na Uy</option>
  1349. <option value="ro" ${this.settings.displayOptions.targetLanguage === "ro" ? "selected" : ""
  1350. }>Tiếng Romania</option>
  1351. <option value="sv" ${this.settings.displayOptions.targetLanguage === "sv" ? "selected" : ""
  1352. }>Tiếng Thy Đin</option>
  1353. <option value="ur" ${this.settings.displayOptions.targetLanguage === "ur" ? "selected" : ""
  1354. }>Tiếng Urdu</option>
  1355. </select>
  1356. </div>
  1357. <div id="languageLearningOptions" style="display: ${this.settings.displayOptions.translationMode === "language_learning"
  1358. ? "block"
  1359. : "none"
  1360. }">
  1361. <div id="sourceOption" class="settings-grid">
  1362. <span class="settings-label">Hin bn gc:</span>
  1363. <input type="checkbox" id="showSource" ${this.settings.displayOptions.languageLearning.showSource
  1364. ? "checked"
  1365. : ""
  1366. }>
  1367. </div>
  1368. </div>
  1369. <div class="settings-grid">
  1370. <span class="settings-label">C ch dch nh web:</span>
  1371. <select id="webImageFontSize" class="settings-input">
  1372. <option value="8px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "8px"
  1373. ? "selected"
  1374. : ""
  1375. }>Rt nh (8px)</option>
  1376. <option value="9px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "9px"
  1377. ? "selected"
  1378. : ""
  1379. }>Nh (9px)</option>
  1380. <option value="10px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "10px"
  1381. ? "selected"
  1382. : ""
  1383. }>Va (10px)</option>
  1384. <option value="12px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "12px"
  1385. ? "selected"
  1386. : ""
  1387. }>Ln (12px)</option>
  1388. <option value="14px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "14px"
  1389. ? "selected"
  1390. : ""
  1391. }>Rt ln (14px)</option>
  1392. <option value="16px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "16px"
  1393. ? "selected"
  1394. : ""
  1395. }>Siêu ln (16px)</option>
  1396. </select>
  1397. </div>
  1398. <div class="settings-grid">
  1399. <span class="settings-label">C ch dch popup:</span>
  1400. <select id="fontSize" class="settings-input">
  1401. <option value="12px" ${this.settings.displayOptions?.fontSize === "12px" ? "selected" : ""
  1402. }>Nh (12px)</option>
  1403. <option value="14px" ${this.settings.displayOptions?.fontSize === "14px" ? "selected" : ""
  1404. }>Hơi nh (14px)
  1405. </option>
  1406. <option value="16px" ${this.settings.displayOptions?.fontSize === "16px" ? "selected" : ""
  1407. }>Va (16px)</option>
  1408. <option value="18px" ${this.settings.displayOptions?.fontSize === "18px" ? "selected" : ""
  1409. }>Hơi ln (18px)
  1410. </option>
  1411. <option value="20px" ${this.settings.displayOptions?.fontSize === "20px" ? "selected" : ""
  1412. }>Ln (20px)</option>
  1413. <option value="22px" ${this.settings.displayOptions?.fontSize === "22px" ? "selected" : ""
  1414. }>Cc ln (22px)
  1415. </option>
  1416. <option value="24px" ${this.settings.displayOptions?.fontSize === "24px" ? "selected" : ""
  1417. }>Siêu ln (24px)
  1418. </option>
  1419. </select>
  1420. </div>
  1421. <div class="settings-grid">
  1422. <span class="settings-label">Độ rng ti thiu (popup):</span>
  1423. <select id="minPopupWidth" class="settings-input">
  1424. <option value="100px" ${this.settings.displayOptions?.minPopupWidth === "100px"
  1425. ? "selected"
  1426. : ""
  1427. }>Rt nh
  1428. (100px)</option>
  1429. <option value="200px" ${this.settings.displayOptions?.minPopupWidth === "200px"
  1430. ? "selected"
  1431. : ""
  1432. }>Hơi nh
  1433. (200px)</option>
  1434. <option value="300px" ${this.settings.displayOptions?.minPopupWidth === "300px"
  1435. ? "selected"
  1436. : ""
  1437. }>Nh (300px)
  1438. </option>
  1439. <option value="400px" ${this.settings.displayOptions?.minPopupWidth === "400px"
  1440. ? "selected"
  1441. : ""
  1442. }>Va (400px)
  1443. </option>
  1444. <option value="500px" ${this.settings.displayOptions?.minPopupWidth === "500px"
  1445. ? "selected"
  1446. : ""
  1447. }>Hơi ln
  1448. (500px)</option>
  1449. <option value="600px" ${this.settings.displayOptions?.minPopupWidth === "600px"
  1450. ? "selected"
  1451. : ""
  1452. }>Ln (600px)
  1453. </option>
  1454. <option value="700px" ${this.settings.displayOptions?.minPopupWidth === "700px"
  1455. ? "selected"
  1456. : ""
  1457. }>Cc ln
  1458. (700px)</option>
  1459. <option value="800px" ${this.settings.displayOptions?.minPopupWidth === "800px"
  1460. ? "selected"
  1461. : ""
  1462. }>Siêu ln
  1463. (800px)</option>
  1464. </select>
  1465. </div>
  1466. <div class="settings-grid">
  1467. <span class="settings-label">Độ rng ti đa (popup):</span>
  1468. <select id="maxPopupWidth" class="settings-input">
  1469. <option value="30vw" ${this.settings.displayOptions?.maxPopupWidth === "30vw" ? "selected" : ""
  1470. }>30% màn hình
  1471. </option>
  1472. <option value="40vw" ${this.settings.displayOptions?.maxPopupWidth === "40vw" ? "selected" : ""
  1473. }>40% màn hình
  1474. </option>
  1475. <option value="50vw" ${this.settings.displayOptions?.maxPopupWidth === "50vw" ? "selected" : ""
  1476. }>50% màn hình
  1477. </option>
  1478. <option value="60vw" ${this.settings.displayOptions?.maxPopupWidth === "60vw" ? "selected" : ""
  1479. }>60% màn hình
  1480. </option>
  1481. <option value="70vw" ${this.settings.displayOptions?.maxPopupWidth === "70vw" ? "selected" : ""
  1482. }>70% màn hình
  1483. </option>
  1484. <option value="80vw" ${this.settings.displayOptions?.maxPopupWidth === "80vw" ? "selected" : ""
  1485. }>80% màn hình
  1486. </option>
  1487. <option value="90vw" ${this.settings.displayOptions?.maxPopupWidth === "90vw" ? "selected" : ""
  1488. }>90% màn hình
  1489. </option>
  1490. </select>
  1491. </div>
  1492. </div>
  1493. <div style="margin-bottom: 15px;">
  1494. <h3>CONTEXT MENU</h3>
  1495. <div class="settings-grid">
  1496. <span class="settings-label">Bt Context Menu:</span>
  1497. <input type="checkbox" id="contextMenuEnabled" ${this.settings.contextMenu?.enabled ? "checked" : ""
  1498. }>
  1499. </div>
  1500. </div>
  1501. <div style="margin-bottom: 15px;">
  1502. <h3>PHÍM TT</h3>
  1503. <div class="settings-grid">
  1504. <span class="settings-label">Bt phím tt m cài đặt:</span>
  1505. <input type="checkbox" id="settingsShortcutEnabled" ${this.settings.shortcuts?.settingsEnabled ? "checked" : ""
  1506. }>
  1507. </div>
  1508. <div class="settings-grid">
  1509. <span class="settings-label">Bt phím tt dch:</span>
  1510. <input type="checkbox" id="shortcutsEnabled" ${this.settings.shortcuts?.enabled ? "checked" : ""
  1511. }>
  1512. </div>
  1513. <div class="settings-grid">
  1514. <span class="settings-label">Dch trang:</span>
  1515. <div class="shortcut-container">
  1516. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1517. <input type="text" id="pageTranslateKey" class="shortcut-input settings-input"
  1518. value="${this.settings.shortcuts.pageTranslate.key}">
  1519. </div>
  1520. </div>
  1521. <div class="settings-grid">
  1522. <span class="settings-label">Dch text trong hp nhp:</span>
  1523. <div class="shortcut-container">
  1524. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1525. <input type="text" id="inputTranslationKey" class="shortcut-input settings-input"
  1526. value="${this.settings.shortcuts.inputTranslate.key}">
  1527. </div>
  1528. </div>
  1529. <div class="settings-grid">
  1530. <span class="settings-label">Dch nhanh:</span>
  1531. <div class="shortcut-container">
  1532. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1533. <input type="text" id="quickKey" class="shortcut-input settings-input"
  1534. value="${this.settings.shortcuts.quickTranslate.key}">
  1535. </div>
  1536. </div>
  1537. <div class="settings-grid">
  1538. <span class="settings-label">Dch popup:</span>
  1539. <div class="shortcut-container">
  1540. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1541. <input type="text" id="popupKey" class="shortcut-input settings-input"
  1542. value="${this.settings.shortcuts.popupTranslate.key}">
  1543. </div>
  1544. </div>
  1545. <div class="settings-grid">
  1546. <span class="settings-label">Dch nâng cao:</span>
  1547. <div class="shortcut-container">
  1548. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1549. <input type="text" id="advancedKey" class="shortcut-input settings-input" value="${this.settings.shortcuts.advancedTranslate.key
  1550. }">
  1551. </div>
  1552. </div>
  1553. </div>
  1554. <div style="margin-bottom: 15px;">
  1555. <h3>NÚT DCH</h3>
  1556. <div class="settings-grid">
  1557. <span class="settings-label">Bt nút dch:</span>
  1558. <input type="checkbox" id="translationButtonEnabled" ${this.settings.clickOptions?.enabled ? "checked" : ""
  1559. }>
  1560. </div>
  1561. <div class="settings-grid">
  1562. <span class="settings-label">Nhp đơn:</span>
  1563. <select id="singleClickSelect" class="settings-input">
  1564. <option value="quick" ${this.settings.clickOptions.singleClick.translateType === "quick"
  1565. ? "selected"
  1566. : ""
  1567. }>Dch
  1568. nhanh</option>
  1569. <option value="popup" ${this.settings.clickOptions.singleClick.translateType === "popup"
  1570. ? "selected"
  1571. : ""
  1572. }>Dch
  1573. popup</option>
  1574. <option value="advanced" ${this.settings.clickOptions.singleClick.translateType === "advanced"
  1575. ? "selected"
  1576. : ""
  1577. }>Dch nâng cao</option>
  1578. </select>
  1579. </div>
  1580. <div class="settings-grid">
  1581. <span class="settings-label">Nhp đúp:</span>
  1582. <select id="doubleClickSelect" class="settings-input">
  1583. <option value="quick" ${this.settings.clickOptions.doubleClick.translateType === "quick"
  1584. ? "selected"
  1585. : ""
  1586. }>Dch
  1587. nhanh</option>
  1588. <option value="popup" ${this.settings.clickOptions.doubleClick.translateType === "popup"
  1589. ? "selected"
  1590. : ""
  1591. }>Dch
  1592. popup</option>
  1593. <option value="advanced" ${this.settings.clickOptions.doubleClick.translateType === "advanced"
  1594. ? "selected"
  1595. : ""
  1596. }>Dch nâng cao</option>
  1597. </select>
  1598. </div>
  1599. <div class="settings-grid">
  1600. <span class="settings-label">Gi nút:</span>
  1601. <select id="holdSelect" class="settings-input">
  1602. <option value="quick" ${this.settings.clickOptions.hold.translateType === "quick"
  1603. ? "selected"
  1604. : ""
  1605. }>Dch nhanh
  1606. </option>
  1607. <option value="popup" ${this.settings.clickOptions.hold.translateType === "popup"
  1608. ? "selected"
  1609. : ""
  1610. }>Dch popup
  1611. </option>
  1612. <option value="advanced" ${this.settings.clickOptions.hold.translateType === "advanced"
  1613. ? "selected"
  1614. : ""
  1615. }>Dch
  1616. nâng cao</option>
  1617. </select>
  1618. </div>
  1619. </div>
  1620. <div style="margin-bottom: 15px;">
  1621. <h3>CM NG ĐA ĐIM</h3>
  1622. <div class="settings-grid">
  1623. <span class="settings-label">Bt cm ng:</span>
  1624. <input type="checkbox" id="touchEnabled" ${this.settings.touchOptions?.enabled ? "checked" : ""
  1625. }>
  1626. </div>
  1627. <div class="settings-grid">
  1628. <span class="settings-label">Hai ngón tay:</span>
  1629. <select id="twoFingersSelect" class="settings-input">
  1630. <option value="quick" ${this.settings.touchOptions?.twoFingers?.translateType === "quick"
  1631. ? "selected"
  1632. : ""
  1633. }>
  1634. Dch nhanh</option>
  1635. <option value="popup" ${this.settings.touchOptions?.twoFingers?.translateType === "popup"
  1636. ? "selected"
  1637. : ""
  1638. }>
  1639. Dch popup</option>
  1640. <option value="advanced" ${this.settings.touchOptions?.twoFingers?.translateType === "advanced"
  1641. ? "selected"
  1642. : ""
  1643. }>Dch nâng cao</option>
  1644. </select>
  1645. </div>
  1646. <div class="settings-grid">
  1647. <span class="settings-label">Ba ngón tay:</span>
  1648. <select id="threeFingersSelect" class="settings-input">
  1649. <option value="quick" ${this.settings.touchOptions?.threeFingers?.translateType === "quick"
  1650. ? "selected"
  1651. : ""
  1652. }>
  1653. Dch nhanh</option>
  1654. <option value="popup" ${this.settings.touchOptions?.threeFingers?.translateType === "popup"
  1655. ? "selected"
  1656. : ""
  1657. }>
  1658. Dch popup</option>
  1659. <option value="advanced" ${this.settings.touchOptions?.threeFingers?.translateType === "advanced"
  1660. ? "selected"
  1661. : ""
  1662. }>Dch nâng cao</option>
  1663. </select>
  1664. </div>
  1665. <div class="settings-grid">
  1666. <span class="settings-label">Độ nhy (ms):</span>
  1667. <input type="number" id="touchSensitivity" class="settings-input"
  1668. value="${this.settings.touchOptions?.sensitivity || 100
  1669. }" min="50" max="350" step="50">
  1670. </div>
  1671. </div>
  1672. <div style="margin-bottom: 15px;">
  1673. <h3>RATE LIMIT</h3>
  1674. <div class="settings-grid">
  1675. <span class="settings-label">S yêu cu ti đa:</span>
  1676. <input type="number" id="maxRequests" class="settings-input" value="${this.settings.rateLimit?.maxRequests || CONFIG.RATE_LIMIT.maxRequests
  1677. }" min="1" max="50" step="1">
  1678. </div>
  1679. <div class="settings-grid">
  1680. <span class="settings-label">Thi gian ch (ms):</span>
  1681. <input type="number" id="perMilliseconds" class="settings-input" value="${this.settings.rateLimit?.perMilliseconds ||
  1682. CONFIG.RATE_LIMIT.perMilliseconds
  1683. }" min="1000" step="1000">
  1684. </div>
  1685. </div>
  1686. <div style="margin-bottom: 15px;">
  1687. <h3>CACHE</h3>
  1688. <div style="margin-bottom: 10px;">
  1689. <h4 style="color: ${isDark ? "#678" : "#333"
  1690. }; margin-bottom: 8px;">Text Cache</h4>
  1691. <div class="settings-grid">
  1692. <span class="settings-label">Bt cache text:</span>
  1693. <input type="checkbox" id="textCacheEnabled" ${this.settings.cacheOptions?.text?.enabled ? "checked" : ""
  1694. }>
  1695. </div>
  1696. <div class="settings-grid">
  1697. <span class="settings-label">Kích thước cache text:</span>
  1698. <input type="number" id="textCacheMaxSize" class="settings-input" value="${this.settings.cacheOptions?.text?.maxSize || CONFIG.CACHE.text.maxSize
  1699. }" min="10" max="1000">
  1700. </div>
  1701. <div class="settings-grid">
  1702. <span class="settings-label">Thi gian cache text (ms):</span>
  1703. <input type="number" id="textCacheExpiration" class="settings-input" value="${this.settings.cacheOptions?.text?.expirationTime ||
  1704. CONFIG.CACHE.text.expirationTime
  1705. }" min="60000" step="60000">
  1706. </div>
  1707. <div style="margin-bottom: 10px;">
  1708. <h4 style="color: ${isDark ? "#678" : "#333"
  1709. }; margin-bottom: 8px;">Image Cache</h4>
  1710. <div class="settings-grid">
  1711. <span class="settings-label">Bt cache nh:</span>
  1712. <input type="checkbox" id="imageCacheEnabled" ${this.settings.cacheOptions?.image?.enabled ? "checked" : ""
  1713. }>
  1714. </div>
  1715. <div class="settings-grid">
  1716. <span class="settings-label">Kích thước cache nh:</span>
  1717. <input type="number" id="imageCacheMaxSize" class="settings-input" value="${this.settings.cacheOptions?.image?.maxSize ||
  1718. CONFIG.CACHE.image.maxSize
  1719. }" min="10" max="100">
  1720. </div>
  1721. <div class="settings-grid">
  1722. <span class="settings-label">Thi gian cache nh (ms):</span>
  1723. <input type="number" id="imageCacheExpiration" class="settings-input" value="${this.settings.cacheOptions?.image?.expirationTime ||
  1724. CONFIG.CACHE.image.expirationTime
  1725. }" min="60000" step="60000">
  1726. </div>
  1727. </div>
  1728. <div style="margin-bottom: 10px;">
  1729. <h4 style="color: ${isDark ? "#678" : "#333"
  1730. }; margin-bottom: 8px;">Media Cache</h4>
  1731. <div class="settings-grid">
  1732. <span class="settings-label">Bt cache media:</span>
  1733. <input type="checkbox" id="mediaCacheEnabled" ${this.settings.cacheOptions.media?.enabled ? "checked" : ""
  1734. }>
  1735. </div>
  1736. <div class="settings-grid">
  1737. <span class="settings-label">Media cache entries:</span>
  1738. <input type="number" id="mediaCacheMaxSize" class="settings-input" value="${this.settings.cacheOptions.media?.maxSize ||
  1739. CONFIG.CACHE.media.maxSize
  1740. }" min="5" max="100">
  1741. </div>
  1742. <div class="settings-grid">
  1743. <span class="settings-label">Thi gian expire (giây):</span>
  1744. <input type="number" id="mediaCacheExpirationTime" class="settings-input" value="${this.settings.cacheOptions.media?.expirationTime / 1000 ||
  1745. CONFIG.CACHE.media.expirationTime / 1000
  1746. }" min="60000" step="60000">
  1747. </div>
  1748. </div>
  1749. </div>
  1750. </div>
  1751. <div style="border-top: 1px solid ${isDark ? "#444" : "#ddd"
  1752. }; margin-top: 20px; padding-top: 20px;">
  1753. <h3>SAO LƯU CÀI ĐẶT</h3>
  1754. <div style="display: flex; gap: 10px; margin-bottom: 15px;">
  1755. <button id="exportSettings" style="flex: 1; background-color: #28a745 !important; min-width: 140px; height: 36px; display: flex; align-items: center; justify-content: center; gap: 8px;">
  1756. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1757. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  1758. <polyline points="7 10 12 15 17 10"/>
  1759. <line x1="12" y1="15" x2="12" y2="3"/>
  1760. </svg>
  1761. Xut cài đặt
  1762. </button>
  1763. <input type="file" id="importInput" accept=".json" style="display: none;">
  1764. <button id="importSettings" style="flex: 1; background-color: #17a2b8 !important; min-width: 140px; height: 36px; display: flex; align-items: center; justify-content: center; gap: 8px;">
  1765. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1766. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  1767. <polyline points="17 8 12 3 7 8"/>
  1768. <line x1="12" y1="3" x2="12" y2="15"/>
  1769. </svg>
  1770. Nhp cài đặt
  1771. </button>
  1772. </div>
  1773. </div>
  1774. <div style="border-top: 1px solid ${isDark ? "#444" : "#ddd"
  1775. }; margin-top: 20px; padding-top: 20px;">
  1776. <div style="display: flex; gap: 10px; justify-content: flex-end;">
  1777. <button id="cancelSettings" style="min-width: 100px; height: 36px; background-color: ${isDark ? "#666" : "#e9ecef"
  1778. } !important; color: ${isDark ? "#fff" : "#333"} !important;">
  1779. Hy
  1780. </button>
  1781. <button id="saveSettings" style="min-width: 100px; height: 36px; background-color: #007bff !important; color: white !important;">
  1782. Lưu
  1783. </button>
  1784. </div>
  1785. </div>
  1786. `;
  1787. container.className = "translator-settings-container";
  1788. const addGeminiKey = container.querySelector("#addGeminiKey");
  1789. const addOpenaiKey = container.querySelector("#addOpenaiKey");
  1790. const geminiContainer = container.querySelector(
  1791. "#geminiKeys .api-keys-container"
  1792. );
  1793. const openaiContainer = container.querySelector(
  1794. "#openaiKeys .api-keys-container"
  1795. );
  1796. addGeminiKey.addEventListener("click", () => {
  1797. const newEntry = document.createElement("div");
  1798. newEntry.className = "api-key-entry";
  1799. newEntry.style.cssText =
  1800. "display: flex; gap: 10px; margin-bottom: 5px;";
  1801. const currentKeysCount = geminiContainer.children.length;
  1802. newEntry.innerHTML = `
  1803. <input type="text" class="gemini-key" value="" style="flex: 1; width: 100%; border-radius: 6px !important; margin-left: 5px;">
  1804. <button class="remove-key" data-provider="gemini" data-index="${currentKeysCount}" style="background-color: #ff4444;">×</button>
  1805. `;
  1806. geminiContainer.appendChild(newEntry);
  1807. });
  1808. addOpenaiKey.addEventListener("click", () => {
  1809. const newEntry = document.createElement("div");
  1810. newEntry.className = "api-key-entry";
  1811. newEntry.style.cssText =
  1812. "display: flex; gap: 10px; margin-bottom: 5px;";
  1813. const currentKeysCount = openaiContainer.children.length;
  1814. newEntry.innerHTML = `
  1815. <input type="text" class="openai-key" value="" style="flex: 1; width: 100%; border-radius: 6px !important; margin-left: 5px;">
  1816. <button class="remove-key" data-provider="openai" data-index="${currentKeysCount}" style="background-color: #ff4444;">×</button>
  1817. `;
  1818. openaiContainer.appendChild(newEntry);
  1819. });
  1820. container.addEventListener("click", (e) => {
  1821. if (e.target.classList.contains("remove-key")) {
  1822. const provider = e.target.dataset.provider;
  1823. e.target.parentElement.remove();
  1824. const container = document.querySelector(
  1825. `#${provider}Keys .api-keys-container`
  1826. );
  1827. Array.from(container.querySelectorAll(".remove-key")).forEach(
  1828. (btn, i) => {
  1829. btn.dataset.index = i;
  1830. }
  1831. );
  1832. }
  1833. });
  1834. const modelTypeSelect = container.querySelector("#geminiModelType");
  1835. const fastContainer = container.querySelector("#fastModelContainer");
  1836. const proContainer = container.querySelector("#proModelContainer");
  1837. const visionContainer = container.querySelector("#visionModelContainer");
  1838. const customContainer = container.querySelector("#customModelContainer");
  1839. modelTypeSelect.addEventListener("change", (e) => {
  1840. const selectedType = e.target.value;
  1841. fastContainer.style.display = selectedType === "fast" ? "" : "none";
  1842. proContainer.style.display = selectedType === "pro" ? "" : "none";
  1843. visionContainer.style.display = selectedType === "vision" ? "" : "none";
  1844. customContainer.style.display = selectedType === "custom" ? "" : "none";
  1845. });
  1846. const useCustomSelectors = container.querySelector("#useCustomSelectors");
  1847. const selectorsSettings = container.querySelector("#selectorsSettings");
  1848. useCustomSelectors.addEventListener("change", (e) => {
  1849. selectorsSettings.style.display = e.target.checked ? "block" : "none";
  1850. });
  1851. const useCustomPrompt = container.querySelector("#useCustomPrompt");
  1852. const promptSettings = container.querySelector("#promptSettings");
  1853. useCustomPrompt.addEventListener("change", (e) => {
  1854. promptSettings.style.display = e.target.checked ? "block" : "none";
  1855. });
  1856. const displayModeSelect = container.querySelector("#displayMode");
  1857. displayModeSelect.addEventListener("change", (e) => {
  1858. const languageLearningOptions = container.querySelector(
  1859. "#languageLearningOptions"
  1860. );
  1861. languageLearningOptions.style.display =
  1862. e.target.value === "language_learning" ? "block" : "none";
  1863. });
  1864. const handleEscape = (e) => {
  1865. if (e.key === "Escape") {
  1866. document.removeEventListener("keydown", handleEscape);
  1867. if (container && container.parentNode) {
  1868. container.parentNode.removeChild(container);
  1869. }
  1870. }
  1871. };
  1872. document.addEventListener("keydown", handleEscape);
  1873. container.addEventListener("remove", () => {
  1874. document.removeEventListener("keydown", handleEscape);
  1875. });
  1876. const exportBtn = container.querySelector("#exportSettings");
  1877. const importBtn = container.querySelector("#importSettings");
  1878. const importInput = container.querySelector("#importInput");
  1879. exportBtn.addEventListener("click", () => {
  1880. try {
  1881. this.exportSettings();
  1882. this.showNotification("Export settings thành công");
  1883. } catch (error) {
  1884. this.showNotification("Lỗi export settings", "error");
  1885. }
  1886. });
  1887. importBtn.addEventListener("click", () => {
  1888. importInput.click();
  1889. });
  1890. importInput.addEventListener("change", async (e) => {
  1891. const file = e.target.files[0];
  1892. if (!file) return;
  1893. try {
  1894. await this.importSettings(file);
  1895. this.showNotification("Import settings thành công");
  1896. setTimeout(() => location.reload(), 1500);
  1897. } catch (error) {
  1898. this.showNotification(error.message, "error");
  1899. }
  1900. });
  1901. const cancelButton = container.querySelector("#cancelSettings");
  1902. cancelButton.addEventListener("click", () => {
  1903. if (container && container.parentNode) {
  1904. container.parentNode.removeChild(container);
  1905. }
  1906. });
  1907. const saveButton = container.querySelector("#saveSettings");
  1908. saveButton.addEventListener("click", () => {
  1909. this.saveSettings(container);
  1910. container.remove();
  1911. location.reload();
  1912. });
  1913. return container;
  1914. }
  1915. exportSettings() {
  1916. const settings = this.settings;
  1917. const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  1918. const filename = `king1x32-translator-settings-${timestamp}.json`;
  1919. const blob = new Blob([JSON.stringify(settings, null, 2)], {
  1920. type: "application/json",
  1921. });
  1922. const url = URL.createObjectURL(blob);
  1923. const link = document.createElement("a");
  1924. link.href = url;
  1925. link.download = filename;
  1926. document.body.appendChild(link);
  1927. link.click();
  1928. document.body.removeChild(link);
  1929. URL.revokeObjectURL(url);
  1930. }
  1931. async importSettings(file) {
  1932. try {
  1933. const content = await new Promise((resolve, reject) => {
  1934. const reader = new FileReader();
  1935. reader.onload = () => resolve(reader.result);
  1936. reader.onerror = () => reject(new Error("Không thể đọc file"));
  1937. reader.readAsText(file);
  1938. });
  1939. const importedSettings = JSON.parse(content);
  1940. if (!this.validateImportedSettings(importedSettings)) {
  1941. throw new Error("File settings không hợp lệ");
  1942. }
  1943. const mergedSettings = this.mergeWithDefaults(importedSettings);
  1944. GM_setValue("translatorSettings", JSON.stringify(mergedSettings));
  1945. return true;
  1946. } catch (error) {
  1947. console.error("Import error:", error);
  1948. throw new Error(`Li import: ${error.message}`);
  1949. }
  1950. }
  1951. validateImportedSettings(settings) {
  1952. const requiredFields = [
  1953. "theme",
  1954. "apiProvider",
  1955. "apiKey",
  1956. "geminiOptions",
  1957. "ocrOptions",
  1958. "mediaOptions",
  1959. "displayOptions",
  1960. "shortcuts",
  1961. "clickOptions",
  1962. "touchOptions",
  1963. "cacheOptions",
  1964. "rateLimit",
  1965. ];
  1966. return requiredFields.every((field) => settings.hasOwnProperty(field));
  1967. }
  1968. showNotification(message, type = "info") {
  1969. const notification = document.createElement("div");
  1970. notification.className = "translator-notification";
  1971. const colors = {
  1972. info: "#4a90e2",
  1973. success: "#28a745",
  1974. warning: "#ffc107",
  1975. error: "#dc3545",
  1976. };
  1977. const backgroundColor = colors[type] || colors.info;
  1978. const textColor = type === "warning" ? "#000" : "#fff";
  1979. Object.assign(notification.style, {
  1980. position: "fixed",
  1981. top: "20px",
  1982. left: "50%",
  1983. transform: "translateX(-50%)",
  1984. backgroundColor,
  1985. color: textColor,
  1986. padding: "10px 20px",
  1987. borderRadius: "8px",
  1988. zIndex: "2147483647 !important",
  1989. animation: "fadeInOut 2s ease",
  1990. fontFamily: "Arial, sans-serif",
  1991. fontSize: "14px",
  1992. boxShadow: "0 2px 10px rgba(0,0,0,0.2)",
  1993. });
  1994. notification.textContent = message;
  1995. document.body.appendChild(notification);
  1996. setTimeout(() => notification.remove(), 2000);
  1997. }
  1998. loadSettings() {
  1999. const savedSettings = GM_getValue("translatorSettings");
  2000. return savedSettings
  2001. ? this.mergeWithDefaults(JSON.parse(savedSettings))
  2002. : DEFAULT_SETTINGS;
  2003. }
  2004. mergeWithDefaults(savedSettings) {
  2005. return {
  2006. ...DEFAULT_SETTINGS,
  2007. ...savedSettings,
  2008. geminiOptions: {
  2009. ...DEFAULT_SETTINGS.geminiOptions,
  2010. ...(savedSettings?.geminiOptions || {}),
  2011. },
  2012. apiKey: {
  2013. gemini: [
  2014. ...(savedSettings?.apiKey?.gemini ||
  2015. DEFAULT_SETTINGS.apiKey.gemini),
  2016. ],
  2017. openai: [
  2018. ...(savedSettings?.apiKey?.openai ||
  2019. DEFAULT_SETTINGS.apiKey.openai),
  2020. ],
  2021. },
  2022. currentKeyIndex: {
  2023. ...DEFAULT_SETTINGS.currentKeyIndex,
  2024. ...(savedSettings?.currentKeyIndex || {}),
  2025. },
  2026. contextMenu: {
  2027. ...DEFAULT_SETTINGS.contextMenu,
  2028. ...(savedSettings?.contextMenu || {}),
  2029. },
  2030. promptSettings: {
  2031. ...DEFAULT_SETTINGS.promptSettings,
  2032. ...(savedSettings?.promptSettings || {}),
  2033. },
  2034. inputTranslation: {
  2035. ...DEFAULT_SETTINGS.inputTranslation,
  2036. ...(savedSettings?.inputTranslation || {}),
  2037. },
  2038. pageTranslation: {
  2039. ...DEFAULT_SETTINGS.pageTranslation,
  2040. ...(savedSettings?.pageTranslation || {}),
  2041. },
  2042. ocrOptions: {
  2043. ...DEFAULT_SETTINGS.ocrOptions,
  2044. ...(savedSettings?.ocrOptions || {}),
  2045. },
  2046. displayOptions: {
  2047. ...DEFAULT_SETTINGS.displayOptions,
  2048. ...(savedSettings?.displayOptions || {}),
  2049. },
  2050. shortcuts: {
  2051. ...DEFAULT_SETTINGS.shortcuts,
  2052. ...(savedSettings?.shortcuts || {}),
  2053. },
  2054. clickOptions: {
  2055. ...DEFAULT_SETTINGS.clickOptions,
  2056. ...(savedSettings?.clickOptions || {}),
  2057. },
  2058. touchOptions: {
  2059. ...DEFAULT_SETTINGS.touchOptions,
  2060. ...(savedSettings?.touchOptions || {}),
  2061. },
  2062. cacheOptions: {
  2063. text: {
  2064. ...DEFAULT_SETTINGS.cacheOptions.text,
  2065. ...(savedSettings?.cacheOptions?.text || {}),
  2066. },
  2067. image: {
  2068. ...DEFAULT_SETTINGS.cacheOptions.image,
  2069. ...(savedSettings?.cacheOptions?.image || {}),
  2070. },
  2071. media: {
  2072. ...DEFAULT_SETTINGS.cacheOptions.media,
  2073. ...(savedSettings?.cacheOptions?.media || {}),
  2074. },
  2075. page: {
  2076. ...DEFAULT_SETTINGS.cacheOptions.page,
  2077. ...(savedSettings?.cacheOptions?.page || {}),
  2078. },
  2079. },
  2080. rateLimit: {
  2081. ...DEFAULT_SETTINGS.rateLimit,
  2082. ...(savedSettings?.rateLimit || {}),
  2083. },
  2084. };
  2085. }
  2086. saveSettings(settingsUI) {
  2087. const geminiKeys = Array.from(settingsUI.querySelectorAll(".gemini-key"))
  2088. .map((input) => input.value.trim())
  2089. .filter((key) => key !== "");
  2090. const openaiKeys = Array.from(settingsUI.querySelectorAll(".openai-key"))
  2091. .map((input) => input.value.trim())
  2092. .filter((key) => key !== "");
  2093. const useCustomSelectors = settingsUI.querySelector(
  2094. "#useCustomSelectors"
  2095. ).checked;
  2096. const customSelectors = settingsUI
  2097. .querySelector("#customSelectors")
  2098. .value.split("\n")
  2099. .map((s) => s.trim())
  2100. .filter((s) => s && s.length > 0);
  2101. const combineWithDefault = settingsUI.querySelector(
  2102. "#combineWithDefault"
  2103. ).checked;
  2104. const maxWidthVw = settingsUI.querySelector("#maxPopupWidth").value;
  2105. const maxWidthPx = (window.innerWidth * parseInt(maxWidthVw)) / 100;
  2106. const minWidthPx = parseInt(
  2107. settingsUI.querySelector("#minPopupWidth").value
  2108. );
  2109. const finalMinWidth =
  2110. minWidthPx > maxWidthPx
  2111. ? maxWidthVw
  2112. : settingsUI.querySelector("#minPopupWidth").value;
  2113. const newSettings = {
  2114. theme: settingsUI.querySelector('input[name="theme"]:checked').value,
  2115. apiProvider: settingsUI.querySelector(
  2116. 'input[name="apiProvider"]:checked'
  2117. ).value,
  2118. apiKey: {
  2119. gemini:
  2120. geminiKeys.length > 0
  2121. ? geminiKeys
  2122. : [DEFAULT_SETTINGS.apiKey.gemini[0]],
  2123. openai:
  2124. openaiKeys.length > 0
  2125. ? openaiKeys
  2126. : [DEFAULT_SETTINGS.apiKey.openai[0]],
  2127. },
  2128. currentKeyIndex: {
  2129. gemini: 0,
  2130. openai: 0,
  2131. },
  2132. geminiOptions: {
  2133. modelType: settingsUI.querySelector("#geminiModelType").value,
  2134. fastModel: settingsUI.querySelector("#fastModel").value,
  2135. proModel: settingsUI.querySelector("#proModel").value,
  2136. visionModel: settingsUI.querySelector("#visionModel").value,
  2137. customModel: settingsUI.querySelector("#customModel").value,
  2138. },
  2139. contextMenu: {
  2140. enabled: settingsUI.querySelector("#contextMenuEnabled").checked,
  2141. },
  2142. inputTranslation: {
  2143. enabled: settingsUI.querySelector("#inputTranslationEnabled").checked,
  2144. },
  2145. promptSettings: {
  2146. enabled: true,
  2147. useCustom: settingsUI.querySelector("#useCustomPrompt").checked,
  2148. customPrompts: {
  2149. normal: settingsUI.querySelector("#normalPrompt").value.trim(),
  2150. normal_chinese: settingsUI
  2151. .querySelector("#normalPrompt_chinese")
  2152. .value.trim(),
  2153. advanced: settingsUI.querySelector("#advancedPrompt").value.trim(),
  2154. advanced_chinese: settingsUI
  2155. .querySelector("#advancedPrompt_chinese")
  2156. .value.trim(),
  2157. ocr: settingsUI.querySelector("#ocrPrompt").value.trim(),
  2158. ocr_chinese: settingsUI
  2159. .querySelector("#ocrPrompt_chinese")
  2160. .value.trim(),
  2161. media: settingsUI.querySelector("#mediaPrompt").value.trim(),
  2162. media_chinese: settingsUI
  2163. .querySelector("#mediaPrompt_chinese")
  2164. .value.trim(),
  2165. page: settingsUI.querySelector("#pagePrompt").value.trim(),
  2166. page_chinese: settingsUI
  2167. .querySelector("#pagePrompt_chinese")
  2168. .value.trim(),
  2169. },
  2170. },
  2171. pageTranslation: {
  2172. enabled: settingsUI.querySelector("#pageTranslationEnabled").checked,
  2173. autoTranslate: settingsUI.querySelector("#autoTranslatePage").checked,
  2174. showInitialButton:
  2175. settingsUI.querySelector("#showInitialButton").checked,
  2176. buttonTimeout: DEFAULT_SETTINGS.pageTranslation.buttonTimeout,
  2177. useCustomSelectors,
  2178. customSelectors,
  2179. combineWithDefault,
  2180. defaultSelectors: DEFAULT_SETTINGS.pageTranslation.defaultSelectors,
  2181. excludeSelectors: useCustomSelectors
  2182. ? combineWithDefault
  2183. ? [
  2184. ...new Set([
  2185. ...DEFAULT_SETTINGS.pageTranslation.defaultSelectors,
  2186. ...customSelectors,
  2187. ]),
  2188. ]
  2189. : customSelectors
  2190. : DEFAULT_SETTINGS.pageTranslation.defaultSelectors,
  2191. },
  2192. ocrOptions: {
  2193. enabled: settingsUI.querySelector("#ocrEnabled").checked,
  2194. preferredProvider: settingsUI.querySelector(
  2195. 'input[name="apiProvider"]:checked'
  2196. ).value,
  2197. displayType: "popup",
  2198. maxFileSize: CONFIG.OCR.maxFileSize,
  2199. temperature: parseFloat(
  2200. settingsUI.querySelector("#ocrTemperature").value
  2201. ),
  2202. topP: parseFloat(settingsUI.querySelector("#ocrTopP").value),
  2203. topK: parseInt(settingsUI.querySelector("#ocrTopK").value),
  2204. },
  2205. mediaOptions: {
  2206. enabled: settingsUI.querySelector("#mediaEnabled").checked,
  2207. temperature: parseFloat(
  2208. settingsUI.querySelector("#mediaTemperature").value
  2209. ),
  2210. topP: parseFloat(settingsUI.querySelector("#mediaTopP").value),
  2211. topK: parseInt(settingsUI.querySelector("#mediaTopK").value),
  2212. },
  2213. displayOptions: {
  2214. fontSize: settingsUI.querySelector("#fontSize").value,
  2215. minPopupWidth: finalMinWidth,
  2216. maxPopupWidth: maxWidthVw,
  2217. webImageTranslation: {
  2218. fontSize: settingsUI.querySelector("#webImageFontSize").value,
  2219. },
  2220. translationMode: settingsUI.querySelector("#displayMode").value,
  2221. targetLanguage: settingsUI.querySelector("#targetLanguage").value,
  2222. sourceLanguage: settingsUI.querySelector("#sourceLanguage").value,
  2223. languageLearning: {
  2224. enabled:
  2225. settingsUI.querySelector("#displayMode").value ===
  2226. "language_learning",
  2227. showSource: settingsUI.querySelector("#showSource").checked,
  2228. },
  2229. },
  2230. shortcuts: {
  2231. settingsEnabled: settingsUI.querySelector("#settingsShortcutEnabled")
  2232. .checked,
  2233. enabled: settingsUI.querySelector("#shortcutsEnabled").checked,
  2234. pageTranslate: {
  2235. key: settingsUI.querySelector("#pageTranslateKey").value,
  2236. altKey: true,
  2237. },
  2238. inputTranslate: {
  2239. key: settingsUI.querySelector("#inputTranslationKey").value,
  2240. altKey: true,
  2241. },
  2242. quickTranslate: {
  2243. key: settingsUI.querySelector("#quickKey").value,
  2244. altKey: true,
  2245. },
  2246. popupTranslate: {
  2247. key: settingsUI.querySelector("#popupKey").value,
  2248. altKey: true,
  2249. },
  2250. advancedTranslate: {
  2251. key: settingsUI.querySelector("#advancedKey").value,
  2252. altKey: true,
  2253. },
  2254. },
  2255. clickOptions: {
  2256. enabled: settingsUI.querySelector("#translationButtonEnabled")
  2257. .checked,
  2258. singleClick: {
  2259. translateType: settingsUI.querySelector("#singleClickSelect").value,
  2260. },
  2261. doubleClick: {
  2262. translateType: settingsUI.querySelector("#doubleClickSelect").value,
  2263. },
  2264. hold: {
  2265. translateType: settingsUI.querySelector("#holdSelect").value,
  2266. },
  2267. },
  2268. touchOptions: {
  2269. enabled: settingsUI.querySelector("#touchEnabled").checked,
  2270. sensitivity: parseInt(
  2271. settingsUI.querySelector("#touchSensitivity").value
  2272. ),
  2273. twoFingers: {
  2274. translateType: settingsUI.querySelector("#twoFingersSelect").value,
  2275. },
  2276. threeFingers: {
  2277. translateType: settingsUI.querySelector("#threeFingersSelect")
  2278. .value,
  2279. },
  2280. },
  2281. cacheOptions: {
  2282. text: {
  2283. enabled: settingsUI.querySelector("#textCacheEnabled").checked,
  2284. maxSize: parseInt(
  2285. settingsUI.querySelector("#textCacheMaxSize").value
  2286. ),
  2287. expirationTime: parseInt(
  2288. settingsUI.querySelector("#textCacheExpiration").value
  2289. ),
  2290. },
  2291. image: {
  2292. enabled: settingsUI.querySelector("#imageCacheEnabled").checked,
  2293. maxSize: parseInt(
  2294. settingsUI.querySelector("#imageCacheMaxSize").value
  2295. ),
  2296. expirationTime: parseInt(
  2297. settingsUI.querySelector("#imageCacheExpiration").value
  2298. ),
  2299. },
  2300. media: {
  2301. enabled: document.getElementById("mediaCacheEnabled").checked,
  2302. maxSize: parseInt(
  2303. document.getElementById("mediaCacheMaxSize").value
  2304. ),
  2305. expirationTime:
  2306. parseInt(
  2307. document.getElementById("mediaCacheExpirationTime").value
  2308. ) * 1000,
  2309. },
  2310. },
  2311. rateLimit: {
  2312. maxRequests: parseInt(settingsUI.querySelector("#maxRequests").value),
  2313. perMilliseconds: parseInt(
  2314. settingsUI.querySelector("#perMilliseconds").value
  2315. ),
  2316. },
  2317. };
  2318. const isToolsEnabled = settingsUI.querySelector(
  2319. "#showTranslatorTools"
  2320. ).checked;
  2321. const currentState =
  2322. localStorage.getItem("translatorToolsEnabled") === "true";
  2323. if (isToolsEnabled !== currentState) {
  2324. localStorage.setItem(
  2325. "translatorToolsEnabled",
  2326. isToolsEnabled.toString()
  2327. );
  2328. this.translator.ui.removeToolsContainer();
  2329. this.translator.ui.resetState();
  2330. const overlays = document.querySelectorAll(".translator-overlay");
  2331. overlays.forEach((overlay) => overlay.remove());
  2332. if (isToolsEnabled) {
  2333. this.translator.ui.setupTranslatorTools();
  2334. }
  2335. }
  2336. const mergedSettings = this.mergeWithDefaults(newSettings);
  2337. GM_setValue("translatorSettings", JSON.stringify(mergedSettings));
  2338. this.settings = mergedSettings;
  2339. const event = new CustomEvent("settingsChanged", {
  2340. detail: mergedSettings,
  2341. });
  2342. document.dispatchEvent(event);
  2343. return mergedSettings;
  2344. }
  2345. getSetting(path) {
  2346. return path.split(".").reduce((obj, key) => obj?.[key], this.settings);
  2347. }
  2348. }
  2349. class APIKeyManager {
  2350. constructor(settings) {
  2351. this.settings = settings;
  2352. this.failedKeys = new Map();
  2353. this.activeKeys = new Map();
  2354. this.keyRotationInterval = 10000; // 10s
  2355. this.maxConcurrentRequests = 4; // Số request đồng thời tối đa cho mỗi key
  2356. this.setupKeyRotation();
  2357. }
  2358. markKeyAsFailed(key) {
  2359. if (!key) return;
  2360. this.failedKeys.set(key, {
  2361. timestamp: Date.now(),
  2362. failures: (this.failedKeys.get(key)?.failures || 0) + 1,
  2363. });
  2364. if (this.activeKeys.has(key)) {
  2365. this.activeKeys.delete(key);
  2366. }
  2367. }
  2368. getAvailableKeys(provider) {
  2369. const allKeys = this.settings.apiKey[provider];
  2370. if (!allKeys || allKeys.length === 0) {
  2371. throw new Error("Không có API key nào được cấu hình");
  2372. }
  2373. return allKeys.filter((key) => {
  2374. if (!key) return false;
  2375. const failedInfo = this.failedKeys.get(key);
  2376. const activeInfo = this.activeKeys.get(key);
  2377. const isFailed =
  2378. failedInfo && Date.now() - failedInfo.timestamp < 60000;
  2379. const isBusy =
  2380. activeInfo && activeInfo.requests >= this.maxConcurrentRequests;
  2381. return !isFailed && !isBusy;
  2382. });
  2383. }
  2384. getRandomKey(provider) {
  2385. const availableKeys = this.getAvailableKeys(provider);
  2386. if (availableKeys.length === 0) {
  2387. throw new Error("Không có API key khả dụng");
  2388. }
  2389. return availableKeys[Math.floor(Math.random() * availableKeys.length)];
  2390. }
  2391. async useKey(key, action) {
  2392. let activeInfo = this.activeKeys.get(key) || { requests: 0 };
  2393. activeInfo.requests++;
  2394. this.activeKeys.set(key, activeInfo);
  2395. try {
  2396. const result = await action();
  2397. return result;
  2398. } catch (error) {
  2399. if (
  2400. error.message.includes("API key not valid") ||
  2401. error.message.includes("rate limit") ||
  2402. error.status === 400 ||
  2403. error.status === 429
  2404. ) {
  2405. this.markKeyAsFailed(key);
  2406. }
  2407. throw error;
  2408. } finally {
  2409. activeInfo = this.activeKeys.get(key);
  2410. if (activeInfo) {
  2411. activeInfo.requests--;
  2412. if (activeInfo.requests <= 0) {
  2413. this.activeKeys.delete(key);
  2414. } else {
  2415. this.activeKeys.set(key, activeInfo);
  2416. }
  2417. }
  2418. }
  2419. }
  2420. async executeWithMultipleKeys(
  2421. promiseGenerator,
  2422. provider,
  2423. maxConcurrent = 3
  2424. ) {
  2425. const availableKeys = this.getAvailableKeys(provider);
  2426. if (!availableKeys || availableKeys.length === 0) {
  2427. throw new Error("Không có API key khả dụng");
  2428. }
  2429. const errors = [];
  2430. const promises = [];
  2431. let currentKeyIndex = 0;
  2432. const processNext = async () => {
  2433. if (currentKeyIndex >= availableKeys.length) return null;
  2434. const key = availableKeys[currentKeyIndex++];
  2435. try {
  2436. const result = await this.useKey(key, () => promiseGenerator(key));
  2437. if (result) return result;
  2438. } catch (error) {
  2439. errors.push({ key, error });
  2440. if (
  2441. error.message.includes("API key not valid") ||
  2442. error.message.includes("rate limit")
  2443. ) {
  2444. this.markKeyAsFailed(key);
  2445. }
  2446. return processNext();
  2447. }
  2448. };
  2449. for (let i = 0; i < Math.min(maxConcurrent, availableKeys.length); i++) {
  2450. promises.push(processNext());
  2451. }
  2452. const results = await Promise.allSettled(promises);
  2453. const successResults = results
  2454. .filter((r) => r.status === "fulfilled" && r.value)
  2455. .map((r) => r.value);
  2456. if (successResults.length > 0) {
  2457. return successResults;
  2458. }
  2459. throw new Error(
  2460. `Tt c API key đều tht bi: ${errors
  2461. .map((e) => e.error.message)
  2462. .join(", ")}`
  2463. );
  2464. }
  2465. markKeyAsFailed(key) {
  2466. this.failedKeys.set(key, {
  2467. timestamp: Date.now(),
  2468. failures: (this.failedKeys.get(key)?.failures || 0) + 1,
  2469. });
  2470. }
  2471. setupKeyRotation() {
  2472. setInterval(() => {
  2473. const now = Date.now();
  2474. for (const [key, info] of this.failedKeys.entries()) {
  2475. if (now - info.timestamp >= 60000) {
  2476. this.failedKeys.delete(key);
  2477. }
  2478. }
  2479. }, this.keyRotationInterval);
  2480. }
  2481. }
  2482. class APIManager {
  2483. constructor(config, getSettings) {
  2484. this.config = config;
  2485. this.getSettings = getSettings;
  2486. this.keyManager = new APIKeyManager(getSettings());
  2487. this.currentProvider = getSettings().apiProvider;
  2488. this.networkOptimizer = new NetworkOptimizer();
  2489. }
  2490. async request(prompt) {
  2491. const provider = this.config.providers[this.currentProvider];
  2492. if (!provider) {
  2493. throw new Error(`Provider ${this.currentProvider} not found`);
  2494. }
  2495. let attempts = 0;
  2496. let lastError;
  2497. while (attempts < this.config.maxRetries) {
  2498. try {
  2499. const key = await this.keyManager.getRandomKey(this.currentProvider);
  2500. const response = await this.keyManager.useKey(key, () =>
  2501. this.makeRequest(provider, prompt, key)
  2502. );
  2503. return provider.responseParser(response);
  2504. } catch (error) {
  2505. console.error(`Attempt ${attempts + 1} failed:`, error);
  2506. lastError = error;
  2507. attempts++;
  2508. if (error.message.includes("rate limit")) {
  2509. continue;
  2510. }
  2511. await new Promise((resolve) =>
  2512. setTimeout(resolve, this.config.retryDelay * Math.pow(2, attempts))
  2513. );
  2514. }
  2515. }
  2516. throw (
  2517. lastError || new Error("Failed to get translation after all retries")
  2518. );
  2519. }
  2520. async batchRequest(prompts) {
  2521. return this.keyManager.executeWithMultipleKeys(async (key) => {
  2522. const results = [];
  2523. for (const prompt of prompts) {
  2524. const response = await this.makeRequest(
  2525. this.config.providers[this.currentProvider],
  2526. prompt,
  2527. key
  2528. );
  2529. results.push(response);
  2530. }
  2531. return results;
  2532. }, this.currentProvider);
  2533. }
  2534. async makeRequest(provider, prompt, key) {
  2535. const selectedModel = this.getGeminiModel();
  2536. return new Promise((resolve, reject) => {
  2537. GM_xmlhttpRequest({
  2538. method: "POST",
  2539. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`,
  2540. headers: { "Content-Type": "application/json" },
  2541. data: JSON.stringify({
  2542. contents: [
  2543. {
  2544. parts: [{ text: prompt }],
  2545. },
  2546. ],
  2547. generationConfig: {
  2548. temperature: this.getSettings().mediaOptions.temperature,
  2549. topP: this.getSettings().mediaOptions.topP,
  2550. topK: this.getSettings().mediaOptions.topK,
  2551. },
  2552. }),
  2553. onload: (response) => {
  2554. if (response.status === 200) {
  2555. try {
  2556. const result = JSON.parse(response.responseText);
  2557. if (result?.candidates?.[0]?.content?.parts?.[0]?.text) {
  2558. resolve(result.candidates[0].content.parts[0].text);
  2559. } else {
  2560. reject(new Error("Invalid response format"));
  2561. }
  2562. } catch (error) {
  2563. reject(new Error("Failed to parse response"));
  2564. }
  2565. } else {
  2566. if (response.status === 400) {
  2567. reject(new Error("API key not valid"));
  2568. } else if (response.status === 429) {
  2569. reject(new Error("API key rate limit exceeded"));
  2570. } else {
  2571. reject(new Error(`API Error: ${response.status}`));
  2572. }
  2573. }
  2574. },
  2575. onerror: (error) => reject(error),
  2576. });
  2577. });
  2578. }
  2579. getGeminiModel() {
  2580. const settings = this.getSettings();
  2581. return settings.selectedModel || "gemini-2.0-flash-lite";
  2582. }
  2583. markKeyAsFailed(key) {
  2584. if (this.keyManager) {
  2585. this.keyManager.markKeyAsFailed(key);
  2586. }
  2587. }
  2588. }
  2589. class InputTranslator {
  2590. constructor(translator) {
  2591. this.translator = translator;
  2592. this.isTranslating = false;
  2593. this.activeButtons = new Map();
  2594. this.page = new PageTranslator(translator);
  2595. this.setupObservers();
  2596. this.setupEventListeners();
  2597. this.initializeExistingEditors();
  2598. GM_addStyle(`
  2599. .input-translate-button-container {
  2600. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
  2601. }
  2602. .input-translate-button {
  2603. font-family: inherit;
  2604. }
  2605. `);
  2606. }
  2607. setupObservers() {
  2608. const settings = this.translator.userSettings.settings;
  2609. if (!settings.inputTranslation?.enabled) return;
  2610. this.mutationObserver = new MutationObserver((mutations) => {
  2611. mutations.forEach((mutation) => {
  2612. mutation.addedNodes.forEach((node) => {
  2613. if (node.nodeType === 1) {
  2614. this.handleNewNode(node);
  2615. }
  2616. });
  2617. mutation.removedNodes.forEach((node) => {
  2618. if (node.nodeType === 1) {
  2619. this.handleRemovedNode(node);
  2620. }
  2621. });
  2622. });
  2623. });
  2624. this.resizeObserver = new ResizeObserver(
  2625. debounce((entries) => {
  2626. entries.forEach((entry) => {
  2627. const editor = this.findParentEditor(entry.target);
  2628. if (editor) {
  2629. this.updateButtonPosition(editor);
  2630. }
  2631. });
  2632. }, 100)
  2633. );
  2634. this.mutationObserver.observe(document.body, {
  2635. childList: true,
  2636. subtree: true,
  2637. });
  2638. }
  2639. getEditorSelectors() {
  2640. return [
  2641. ".fr-element.fr-view",
  2642. ".message-editable",
  2643. ".js-editor",
  2644. ".xenForm textarea",
  2645. '[contenteditable="true"]',
  2646. '[role="textbox"]',
  2647. "textarea",
  2648. 'input[type="text"]',
  2649. ].join(",");
  2650. }
  2651. isValidEditor(element) {
  2652. const settings = this.translator.userSettings.settings;
  2653. if (!settings.inputTranslation?.enabled && !settings.shortcuts?.enabled) return;
  2654. if (!element) return false;
  2655. const style = window.getComputedStyle(element);
  2656. if (style.display === "none" || style.visibility === "hidden") {
  2657. return false;
  2658. }
  2659. const rect = element.getBoundingClientRect();
  2660. if (rect.width === 0 || rect.height === 0) {
  2661. return false;
  2662. }
  2663. return element.matches(this.getEditorSelectors());
  2664. }
  2665. findParentEditor(element) {
  2666. while (element && element !== document.body) {
  2667. if (this.isValidEditor(element)) {
  2668. return element;
  2669. }
  2670. if (element.tagName === "IFRAME") {
  2671. try {
  2672. const iframeDoc = element.contentDocument;
  2673. if (iframeDoc && this.isValidEditor(iframeDoc.body)) {
  2674. return iframeDoc.body;
  2675. }
  2676. } catch (e) {
  2677. }
  2678. }
  2679. element = element.parentElement;
  2680. }
  2681. return null;
  2682. }
  2683. setupEventListeners() {
  2684. const settings = this.translator.userSettings.settings;
  2685. if (!settings.inputTranslation?.enabled) return;
  2686. document.addEventListener("focusin", (e) => {
  2687. const editor = this.findParentEditor(e.target);
  2688. if (editor) {
  2689. this.addTranslateButton(editor);
  2690. this.updateButtonVisibility(editor);
  2691. }
  2692. });
  2693. document.addEventListener("focusout", (e) => {
  2694. const editor = this.findParentEditor(e.target);
  2695. if (editor) {
  2696. setTimeout(() => {
  2697. const activeElement = document.activeElement;
  2698. const container = this.activeButtons.get(editor);
  2699. if (container && !container.contains(activeElement)) {
  2700. this.removeTranslateButton(editor);
  2701. }
  2702. }, 100);
  2703. }
  2704. });
  2705. document.addEventListener("input", (e) => {
  2706. const editor = this.findParentEditor(e.target);
  2707. if (editor) {
  2708. if (!this.activeButtons.has(editor)) {
  2709. this.addTranslateButton(editor);
  2710. }
  2711. this.updateButtonVisibility(editor);
  2712. }
  2713. });
  2714. }
  2715. addTranslateButton(editor) {
  2716. if (this.activeButtons.has(editor)) {
  2717. this.updateButtonVisibility(editor);
  2718. return;
  2719. }
  2720. const container = this.createButtonContainer();
  2721. const translateButton = this.createButton(
  2722. "🌐",
  2723. "Dịch sang ngôn ngữ đích"
  2724. );
  2725. const reverseButton = this.createButton("🔄", "Dịch sang ngôn ngữ nguồn");
  2726. translateButton.onclick = async (e) => {
  2727. e.preventDefault();
  2728. e.stopPropagation();
  2729. await this.translateEditor(editor, false);
  2730. };
  2731. reverseButton.onclick = async (e) => {
  2732. e.preventDefault();
  2733. e.stopPropagation();
  2734. await this.translateEditor(editor, true);
  2735. };
  2736. container.appendChild(translateButton);
  2737. container.appendChild(reverseButton);
  2738. this.positionButtonContainer(container, editor);
  2739. document.body.appendChild(container);
  2740. this.activeButtons.set(editor, container);
  2741. this.updateButtonVisibility(editor);
  2742. this.resizeObserver.observe(editor);
  2743. }
  2744. updateButtonVisibility(editor) {
  2745. const container = this.activeButtons.get(editor);
  2746. if (container) {
  2747. const hasContent = this.getEditorContent(editor);
  2748. container.style.display = hasContent ? "" : "none";
  2749. }
  2750. }
  2751. getEditorContent(editor) {
  2752. const settings = this.translator.userSettings.settings;
  2753. if (!settings.inputTranslation?.enabled && !settings.shortcuts?.enabled) return;
  2754. let content = "";
  2755. if (editor.value !== undefined) {
  2756. content = editor.value;
  2757. } else if (editor.textContent !== undefined) {
  2758. content = editor.textContent;
  2759. } else if (editor.innerText !== undefined) {
  2760. content = editor.innerText;
  2761. }
  2762. return content.trim();
  2763. }
  2764. setEditorContent(editor, content) {
  2765. if (editor.matches(".fr-element.fr-view")) {
  2766. editor.innerHTML = content;
  2767. } else if (editor.value !== undefined) {
  2768. editor.value = content;
  2769. } else {
  2770. editor.innerHTML = content;
  2771. }
  2772. editor.dispatchEvent(new Event("input", { bubbles: true }));
  2773. editor.dispatchEvent(new Event("change", { bubbles: true }));
  2774. }
  2775. createButtonContainer() {
  2776. const container = document.createElement("div");
  2777. container.className = "input-translate-button-container";
  2778. const theme = this.getCurrentTheme();
  2779. container.style.cssText = `
  2780. position: absolute;
  2781. display: flex;
  2782. gap: 2px;
  2783. align-items: center;
  2784. z-index: 2147483647 !important;
  2785. pointer-events: auto;
  2786. background-color: rgba(0,74,153,0.1);
  2787. border-radius: 8px;
  2788. padding: 2px;
  2789. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  2790. border: 1px solid ${theme.border};
  2791. `;
  2792. return container;
  2793. }
  2794. createButton(icon, title) {
  2795. const button = document.createElement("button");
  2796. button.className = "input-translate-button";
  2797. button.innerHTML = icon;
  2798. button.title = title;
  2799. const theme = this.getCurrentTheme();
  2800. button.style.cssText = `
  2801. background-color: rgba(255,255,255,0.05);
  2802. color: ${theme.text};
  2803. border: none;
  2804. border-radius: 8px;
  2805. padding: 4px;
  2806. font-size: 16px;
  2807. cursor: pointer;
  2808. display: flex;
  2809. align-items: center;
  2810. justify-content: center;
  2811. min-width: 28px;
  2812. height: 28px;
  2813. transition: all 0.15s ease;
  2814. margin: 0;
  2815. outline: none;
  2816. `;
  2817. button.onmouseover = () => {
  2818. button.style.background = theme.hoverBg;
  2819. button.style.color = theme.hoverText;
  2820. };
  2821. button.onmouseout = () => {
  2822. button.style.background = "transparent";
  2823. button.style.color = theme.text;
  2824. };
  2825. return button;
  2826. }
  2827. async translateEditor(editor, reverse = false) {
  2828. const settings = this.translator.userSettings.settings;
  2829. if (!settings.inputTranslation?.enabled && !settings.shortcuts?.enabled) return;
  2830. if (this.isTranslating) return;
  2831. this.isTranslating = true;
  2832. const container = this.activeButtons.get(editor);
  2833. const button = container?.querySelector(
  2834. reverse
  2835. ? ".input-translate-button:last-child"
  2836. : ".input-translate-button:first-child"
  2837. );
  2838. const originalText = button?.innerHTML;
  2839. try {
  2840. const text = this.getEditorContent(editor);
  2841. if (!text) return;
  2842. button.innerHTML = "⌛";
  2843. button.style.opacity = "0.7";
  2844. const displayOptions =
  2845. this.translator.userSettings.settings.displayOptions;
  2846. const sourceLanguage =
  2847. displayOptions.sourceLanguage === "auto"
  2848. ? this.page.languageCode
  2849. : displayOptions.sourceLanguage;
  2850. console.log("sourceLanguage: ", sourceLanguage);
  2851. const translation = await this.translator.translate(
  2852. text,
  2853. null,
  2854. false,
  2855. false,
  2856. reverse ? sourceLanguage : displayOptions.targetLanguage
  2857. );
  2858. if (translation) {
  2859. this.setEditorContent(editor, translation);
  2860. }
  2861. } catch (error) {
  2862. console.error("Translation error:", error);
  2863. this.translator.ui.showNotification(
  2864. "Lỗi dịch: " + error.message,
  2865. "error"
  2866. );
  2867. } finally {
  2868. this.isTranslating = false;
  2869. if (button) {
  2870. button.innerHTML = originalText;
  2871. button.style.opacity = "1";
  2872. }
  2873. }
  2874. }
  2875. positionButtonContainer(container, editor) {
  2876. const rect = editor.getBoundingClientRect();
  2877. const toolbar = this.findEditorToolbar(editor);
  2878. if (toolbar) {
  2879. const toolbarRect = toolbar.getBoundingClientRect();
  2880. container.style.top = `${toolbarRect.top + window.scrollY}px`;
  2881. container.style.left = `${toolbarRect.right + 5}px`;
  2882. } else {
  2883. container.style.top = `${rect.top + window.scrollY}px`;
  2884. container.style.left = `${rect.right + 5}px`;
  2885. }
  2886. }
  2887. findEditorToolbar(editor) {
  2888. return (
  2889. editor.closest(".fr-box")?.querySelector(".fr-toolbar") ||
  2890. editor.closest(".xenForm")?.querySelector(".buttonGroup")
  2891. );
  2892. }
  2893. updateButtonPosition(editor) {
  2894. const container = this.activeButtons.get(editor);
  2895. if (container) {
  2896. this.positionButtonContainer(container, editor);
  2897. }
  2898. }
  2899. getCurrentTheme() {
  2900. const themeMode = this.translator.userSettings.settings.theme;
  2901. const theme = CONFIG.THEME[themeMode];
  2902. return {
  2903. backgroundColor: theme.background,
  2904. text: theme.text,
  2905. border: theme.border,
  2906. hoverBg: theme.background,
  2907. hoverText: theme.text,
  2908. };
  2909. }
  2910. updateAllButtonStyles() {
  2911. const theme = this.getCurrentTheme();
  2912. this.activeButtons.forEach((container) => {
  2913. container.style.background = theme.background;
  2914. container.style.borderColor = theme.border;
  2915. container
  2916. .querySelectorAll(".input-translate-button")
  2917. .forEach((button) => {
  2918. button.style.color = theme.text;
  2919. });
  2920. });
  2921. }
  2922. handleNewNode(node) {
  2923. if (this.isValidEditor(node)) {
  2924. this.addTranslateButton(node);
  2925. }
  2926. node.querySelectorAll(this.getEditorSelectors()).forEach((editor) => {
  2927. if (this.isValidEditor(editor)) {
  2928. this.addTranslateButton(editor);
  2929. }
  2930. });
  2931. }
  2932. handleRemovedNode(node) {
  2933. if (this.activeButtons.has(node)) {
  2934. this.removeTranslateButton(node);
  2935. }
  2936. node.querySelectorAll(this.getEditorSelectors()).forEach((editor) => {
  2937. if (this.activeButtons.has(editor)) {
  2938. this.removeTranslateButton(editor);
  2939. }
  2940. });
  2941. }
  2942. handleEditorFocus(editor) {
  2943. if (this.getEditorContent(editor)) {
  2944. this.addTranslateButton(editor);
  2945. }
  2946. }
  2947. handleEditorClick(editor) {
  2948. if (this.getEditorContent(editor)) {
  2949. this.addTranslateButton(editor);
  2950. }
  2951. }
  2952. removeTranslateButton(editor) {
  2953. const container = this.activeButtons.get(editor);
  2954. if (container) {
  2955. container.remove();
  2956. this.activeButtons.delete(editor);
  2957. this.resizeObserver.unobserve(editor);
  2958. }
  2959. }
  2960. initializeExistingEditors() {
  2961. const settings = this.translator.userSettings.settings;
  2962. if (!settings.inputTranslation?.enabled) return;
  2963. document.querySelectorAll(this.getEditorSelectors()).forEach((editor) => {
  2964. if (this.isValidEditor(editor) && this.getEditorContent(editor)) {
  2965. this.addTranslateButton(editor);
  2966. }
  2967. });
  2968. }
  2969. cleanup() {
  2970. this.mutationObserver.disconnect();
  2971. this.resizeObserver.disconnect();
  2972. this.activeButtons.forEach((container, editor) => {
  2973. this.removeTranslateButton(editor);
  2974. });
  2975. }
  2976. }
  2977. class OCRManager {
  2978. constructor(translator) {
  2979. if (!translator) {
  2980. throw new Error("Translator instance is required for OCRManager");
  2981. }
  2982. this.translator = translator;
  2983. this.isProcessing = false;
  2984. this.imageCache = new ImageCache();
  2985. }
  2986. async captureScreen() {
  2987. try {
  2988. this.translator.ui.showProcessingStatus(
  2989. "Đang chuẩn bị chụp màn hình..."
  2990. );
  2991. const elements = document.querySelectorAll(
  2992. ".translator-tools-container, .translator-notification, .center-translate-status"
  2993. );
  2994. elements.forEach((el) => {
  2995. if (el) el.style.visibility = "hidden";
  2996. });
  2997. await new Promise((resolve) => setTimeout(resolve, 100));
  2998. const options = {
  2999. useCORS: true,
  3000. allowTaint: true,
  3001. foreignObjectRendering: true,
  3002. scale: window.devicePixelRatio || 1,
  3003. logging: false,
  3004. width: window.innerWidth,
  3005. height: window.innerHeight,
  3006. windowWidth: document.documentElement.scrollWidth,
  3007. windowHeight: document.documentElement.scrollHeight,
  3008. x: window.pageXOffset,
  3009. y: window.pageYOffset,
  3010. onclone: function(clonedDoc) {
  3011. const elements = clonedDoc.querySelectorAll(
  3012. ".translator-tools-container, .translator-notification, .center-translate-status"
  3013. );
  3014. elements.forEach((el) => {
  3015. if (el) el.style.display = "none";
  3016. });
  3017. },
  3018. };
  3019. this.translator.ui.updateProcessingStatus("Đang chụp màn hình...", 30);
  3020. const canvas = await html2canvas(document.documentElement, options);
  3021. this.translator.ui.updateProcessingStatus("Đang xử lý ảnh...", 60);
  3022. const blob = await new Promise((resolve, reject) => {
  3023. try {
  3024. canvas.toBlob(
  3025. (blob) => {
  3026. if (blob) {
  3027. resolve(blob);
  3028. } else {
  3029. reject(new Error("Failed to create blob from canvas"));
  3030. }
  3031. },
  3032. "image/png",
  3033. 1.0
  3034. );
  3035. } catch (error) {
  3036. reject(error);
  3037. }
  3038. });
  3039. elements.forEach((el) => {
  3040. if (el) el.style.visibility = "";
  3041. });
  3042. this.translator.ui.updateProcessingStatus("Đang chuẩn bị OCR...", 80);
  3043. const file = new File([blob], "king1x32_screenshot.png", {
  3044. type: "image/png",
  3045. });
  3046. return file;
  3047. } catch (error) {
  3048. console.error("Screen capture error:", error);
  3049. const elements = document.querySelectorAll(
  3050. ".translator-tools-container, .translator-notification, .center-translate-status"
  3051. );
  3052. elements.forEach((el) => {
  3053. if (el) el.style.visibility = "";
  3054. });
  3055. throw new Error(`Không th chp màn hình: ${error.message}`);
  3056. }
  3057. }
  3058. async processImage(file) {
  3059. try {
  3060. this.isProcessing = true;
  3061. this.translator.ui.showProcessingStatus("Đang xử lý ảnh...");
  3062. const base64Image = await this.fileToBase64(file);
  3063. this.translator.ui.updateProcessingStatus("Đang kiểm tra cache...", 20);
  3064. if (
  3065. this.imageCache &&
  3066. this.translator.userSettings.settings.cacheOptions.image.enabled
  3067. ) {
  3068. const cachedResult = await this.imageCache.get(base64Image);
  3069. if (cachedResult) {
  3070. this.translator.ui.updateProcessingStatus(
  3071. "Đã tìm thấy trong cache",
  3072. 100
  3073. );
  3074. return cachedResult;
  3075. }
  3076. }
  3077. this.translator.ui.updateProcessingStatus("Đang nhận diện text...", 40);
  3078. const settings = this.translator.userSettings.settings;
  3079. const selectedModel = this.translator.api.getGeminiModel();
  3080. const prompt = this.translator.createPrompt("ocr", "ocr");
  3081. const requestBody = {
  3082. contents: [
  3083. {
  3084. parts: [
  3085. {
  3086. text: prompt,
  3087. },
  3088. {
  3089. inline_data: {
  3090. mime_type: file.type,
  3091. data: base64Image,
  3092. },
  3093. },
  3094. ],
  3095. },
  3096. ],
  3097. generationConfig: {
  3098. temperature: settings.ocrOptions.temperature,
  3099. topP: settings.ocrOptions.topP,
  3100. topK: settings.ocrOptions.topK,
  3101. },
  3102. };
  3103. this.translator.ui.updateProcessingStatus("Đang xử lý OCR...", 60);
  3104. const results =
  3105. await this.translator.api.keyManager.executeWithMultipleKeys(
  3106. async (key) => {
  3107. const response = await new Promise((resolve, reject) => {
  3108. GM_xmlhttpRequest({
  3109. method: "POST",
  3110. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`,
  3111. headers: { "Content-Type": "application/json" },
  3112. data: JSON.stringify(requestBody),
  3113. onload: (response) => {
  3114. if (response.status === 200) {
  3115. try {
  3116. const result = JSON.parse(response.responseText);
  3117. if (
  3118. result?.candidates?.[0]?.content?.parts?.[0]?.text
  3119. ) {
  3120. resolve(result.candidates[0].content.parts[0].text);
  3121. } else {
  3122. reject(new Error("Không thể đọc kết quả từ API"));
  3123. }
  3124. } catch (error) {
  3125. reject(new Error("Không thể parse kết quả API"));
  3126. }
  3127. } else if (
  3128. response.status === 429 ||
  3129. response.status === 403
  3130. ) {
  3131. reject(new Error("API key rate limit exceeded"));
  3132. } else {
  3133. reject(new Error(`API Error: ${response.status}`));
  3134. }
  3135. },
  3136. onerror: (error) =>
  3137. reject(new Error(`Li kết ni: ${error}`)),
  3138. });
  3139. });
  3140. return response;
  3141. },
  3142. settings.apiProvider
  3143. );
  3144. this.translator.ui.updateProcessingStatus("Đang hoàn thiện...", 80);
  3145. if (!results || results.length === 0) {
  3146. throw new Error("Không thể trích xuất text từ ảnh");
  3147. }
  3148. const finalResult = results[0];
  3149. if (this.imageCache && settings.cacheOptions.image.enabled) {
  3150. await this.imageCache.set(base64Image, finalResult);
  3151. }
  3152. this.translator.ui.updateProcessingStatus("Hoàn thành", 100);
  3153. return finalResult;
  3154. } catch (error) {
  3155. console.error("OCR processing error:", error);
  3156. throw error;
  3157. } finally {
  3158. this.isProcessing = false;
  3159. setTimeout(() => this.translator.ui.removeProcessingStatus(), 1000);
  3160. }
  3161. }
  3162. fileToBase64(file) {
  3163. return new Promise((resolve, reject) => {
  3164. const reader = new FileReader();
  3165. reader.onload = () => resolve(reader.result.split(",")[1]);
  3166. reader.onerror = () => reject(new Error("Không thể đọc file"));
  3167. reader.readAsDataURL(file);
  3168. });
  3169. }
  3170. }
  3171. class MediaManager {
  3172. constructor(translator) {
  3173. this.translator = translator;
  3174. this.isProcessing = false;
  3175. this.mediaCache = new MediaCache();
  3176. }
  3177. async processMediaFile(file) {
  3178. try {
  3179. if (!this.isValidFormat(file)) {
  3180. throw new Error("Định dạng file không được hỗ trợ");
  3181. }
  3182. if (!this.isValidSize(file)) {
  3183. throw new Error(
  3184. `File quá ln. Kích thước ti đa: ${this.getMaxSizeInMB(file)}MB`
  3185. );
  3186. }
  3187. this.isProcessing = true;
  3188. this.translator.ui.showProcessingStatus("Đang xử lý media...");
  3189. const base64Media = await this.fileToBase64(file);
  3190. this.translator.ui.updateProcessingStatus("Đang kiểm tra cache...", 20);
  3191. const cacheEnabled =
  3192. this.translator.userSettings.settings.cacheOptions.media?.enabled;
  3193. if (cacheEnabled && this.mediaCache) {
  3194. const cachedResult = await this.mediaCache.get(base64Media);
  3195. if (cachedResult) {
  3196. this.translator.ui.updateProcessingStatus(
  3197. "Đã tìm thấy trong cache",
  3198. 100
  3199. );
  3200. this.translator.ui.displayPopup(cachedResult, null, "Bản dịch");
  3201. return;
  3202. }
  3203. }
  3204. this.translator.ui.updateProcessingStatus(
  3205. "Đang xử lý audio/video...",
  3206. 40
  3207. );
  3208. const settings = this.translator.userSettings.settings;
  3209. const mediaSettings = settings.mediaOptions;
  3210. const selectedModel = this.translator.api.getGeminiModel();
  3211. const prompt = this.translator.createPrompt("media", "media");
  3212. const requestBody = {
  3213. contents: [
  3214. {
  3215. parts: [
  3216. {
  3217. text: prompt,
  3218. },
  3219. {
  3220. inline_data: {
  3221. mime_type: file.type,
  3222. data: base64Media,
  3223. },
  3224. },
  3225. ],
  3226. },
  3227. ],
  3228. generationConfig: {
  3229. temperature: mediaSettings.temperature,
  3230. topP: mediaSettings.topP,
  3231. topK: mediaSettings.topK,
  3232. },
  3233. };
  3234. this.translator.ui.updateProcessingStatus("Đang dịch...", 60);
  3235. const results =
  3236. await this.translator.api.keyManager.executeWithMultipleKeys(
  3237. async (key) => {
  3238. const response = await new Promise((resolve, reject) => {
  3239. GM_xmlhttpRequest({
  3240. method: "POST",
  3241. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`,
  3242. headers: { "Content-Type": "application/json" },
  3243. data: JSON.stringify(requestBody),
  3244. onload: (response) => {
  3245. if (response.status === 200) {
  3246. try {
  3247. const result = JSON.parse(response.responseText);
  3248. if (
  3249. result?.candidates?.[0]?.content?.parts?.[0]?.text
  3250. ) {
  3251. resolve(result.candidates[0].content.parts[0].text);
  3252. } else {
  3253. reject(new Error("Invalid response format"));
  3254. }
  3255. } catch (error) {
  3256. reject(new Error("Failed to parse response"));
  3257. }
  3258. } else if (
  3259. response.status === 429 ||
  3260. response.status === 403
  3261. ) {
  3262. reject(new Error("API key rate limit exceeded"));
  3263. } else {
  3264. reject(new Error(`API Error: ${response.status}`));
  3265. }
  3266. },
  3267. onerror: (error) =>
  3268. reject(new Error(`Connection error: ${error}`)),
  3269. });
  3270. });
  3271. return response;
  3272. },
  3273. settings.apiProvider
  3274. );
  3275. this.translator.ui.updateProcessingStatus("Đang hoàn thiện...", 80);
  3276. if (!results || results.length === 0) {
  3277. throw new Error("Không thể xử lý media");
  3278. }
  3279. const finalResult = results[0];
  3280. if (cacheEnabled && this.mediaCache) {
  3281. await this.mediaCache.set(base64Media, finalResult);
  3282. }
  3283. this.translator.ui.updateProcessingStatus("Hoàn thành", 100);
  3284. this.translator.ui.displayPopup(finalResult, null, "Bản dịch");
  3285. } catch (error) {
  3286. console.error("Media processing error:", error);
  3287. throw new Error(`Không th x lý file: ${error.message}`);
  3288. } finally {
  3289. this.isProcessing = false;
  3290. setTimeout(() => this.translator.ui.removeProcessingStatus(), 1000);
  3291. }
  3292. }
  3293. isValidFormat(file) {
  3294. const extension = file.name.split(".").pop().toLowerCase();
  3295. const mimeMapping = {
  3296. mp3: "audio/mp3",
  3297. wav: "audio/wav",
  3298. ogg: "audio/ogg",
  3299. m4a: "audio/m4a",
  3300. aac: "audio/aac",
  3301. flac: "audio/flac",
  3302. wma: "audio/wma",
  3303. opus: "audio/opus",
  3304. amr: "audio/amr",
  3305. midi: "audio/midi",
  3306. mid: "audio/midi",
  3307. mp4: "video/mp4",
  3308. webm: "video/webm",
  3309. ogv: "video/ogg",
  3310. avi: "video/x-msvideo",
  3311. mov: "video/quicktime",
  3312. wmv: "video/x-ms-wmv",
  3313. flv: "video/x-flv",
  3314. "3gp": "video/3gpp",
  3315. "3g2": "video/3gpp2",
  3316. mkv: "video/x-matroska",
  3317. };
  3318. const mimeType = mimeMapping[extension];
  3319. if (mimeType?.startsWith("audio/")) {
  3320. return CONFIG.MEDIA.audio.supportedFormats.includes(mimeType);
  3321. } else if (mimeType?.startsWith("video/")) {
  3322. return CONFIG.MEDIA.video.supportedFormats.includes(mimeType);
  3323. }
  3324. return false;
  3325. }
  3326. isValidSize(file) {
  3327. const maxSize = file.type.startsWith("audio/")
  3328. ? CONFIG.MEDIA.audio.maxSize
  3329. : CONFIG.MEDIA.video.maxSize;
  3330. return file.size <= maxSize;
  3331. }
  3332. getMaxSizeInMB(file) {
  3333. const maxSize = file.type.startsWith("audio/")
  3334. ? CONFIG.MEDIA.audio.maxSize
  3335. : CONFIG.MEDIA.video.maxSize;
  3336. return Math.floor(maxSize / (1024 * 1024));
  3337. }
  3338. fileToBase64(file) {
  3339. return new Promise((resolve, reject) => {
  3340. const reader = new FileReader();
  3341. reader.onload = () => resolve(reader.result.split(",")[1]);
  3342. reader.onerror = () => reject(new Error("Không thể đọc file"));
  3343. reader.readAsDataURL(file);
  3344. });
  3345. }
  3346. cleanup() {
  3347. try {
  3348. if (this.audioCtx) {
  3349. this.audioCtx.close();
  3350. this.audioCtx = null;
  3351. }
  3352. if (this.processor) {
  3353. this.processor.disconnect();
  3354. this.processor = null;
  3355. }
  3356. if (this.container) {
  3357. this.container.remove();
  3358. this.container = null;
  3359. }
  3360. this.mediaElement = null;
  3361. this.audioBuffer = null;
  3362. } catch (error) {
  3363. console.error("Error during cleanup:", error);
  3364. }
  3365. }
  3366. }
  3367. class RateLimiter {
  3368. constructor(translator) {
  3369. this.translator = translator;
  3370. this.queue = [];
  3371. this.lastRequestTime = 0;
  3372. this.requestCount = 0;
  3373. }
  3374. async waitForSlot() {
  3375. const now = Date.now();
  3376. const settings = this.translator.userSettings.settings;
  3377. const { maxRequests, perMilliseconds } = settings.rateLimit;
  3378. this.queue = this.queue.filter((time) => now - time < perMilliseconds);
  3379. if (this.queue.length >= maxRequests) {
  3380. const oldestRequest = this.queue[0];
  3381. const waitTime = perMilliseconds - (now - oldestRequest);
  3382. if (waitTime > 0) {
  3383. await new Promise((resolve) => setTimeout(resolve, waitTime));
  3384. }
  3385. this.queue.shift();
  3386. }
  3387. this.queue.push(now);
  3388. }
  3389. }
  3390. class PageTranslator {
  3391. constructor(translator) {
  3392. this.translator = translator;
  3393. this.MIN_TEXT_LENGTH = 100;
  3394. this.originalTexts = new Map();
  3395. this.isTranslated = false;
  3396. this.languageCode = this.detectLanguage().languageCode;
  3397. this.pageCache = new Map();
  3398. this.rateLimiter = new RateLimiter(translator);
  3399. this.pdfLoaded = true;
  3400. }
  3401. getExcludeSelectors() {
  3402. const settings = this.translator.userSettings.settings.pageTranslation;
  3403. if (!settings.useCustomSelectors) {
  3404. return settings.defaultSelectors;
  3405. }
  3406. return settings.combineWithDefault
  3407. ? [
  3408. ...new Set([
  3409. ...settings.defaultSelectors,
  3410. ...settings.customSelectors,
  3411. ]),
  3412. ]
  3413. : settings.customSelectors;
  3414. }
  3415. async detectLanguage() {
  3416. try {
  3417. let text = "";
  3418. if (document.body.innerText) {
  3419. text = document.body.innerText;
  3420. }
  3421. if (!text) {
  3422. const paragraphs = document.querySelectorAll("p");
  3423. paragraphs.forEach((p) => {
  3424. text += p.textContent + " ";
  3425. });
  3426. }
  3427. if (!text) {
  3428. const headings = document.querySelectorAll("h1, h2, h3");
  3429. headings.forEach((h) => {
  3430. text += h.textContent + " ";
  3431. });
  3432. }
  3433. if (!text) {
  3434. text = document.title;
  3435. }
  3436. text = text.slice(0, 1000).trim();
  3437. if (!text.trim()) {
  3438. throw new Error("Không tìm thấy nội dung để phát hiện ngôn ngữ");
  3439. }
  3440. const prompt =
  3441. "Detect language of this text and return only ISO code (e.g. 'en', 'vi'): " +
  3442. text;
  3443. if (!this.translator.api) {
  3444. throw new Error("API không khả dụng");
  3445. }
  3446. const response = await this.translator.api.request(prompt);
  3447. this.languageCode = response.trim().toLowerCase();
  3448. const targetLanguage =
  3449. this.translator.userSettings.settings.displayOptions.targetLanguage;
  3450. if (this.languageCode === targetLanguage) {
  3451. return {
  3452. isVietnamese: true,
  3453. message: `Trang web đã ngôn ng ${targetLanguage}`,
  3454. };
  3455. }
  3456. return {
  3457. isVietnamese: false,
  3458. message: `Đã phát hin ngôn ngữ: ${this.languageCode}`,
  3459. };
  3460. } catch (error) {
  3461. console.error("Language detection error:", error);
  3462. throw new Error("Không thể phát hiện ngôn ngữ: " + error.message);
  3463. }
  3464. }
  3465. async checkAndTranslate() {
  3466. try {
  3467. const settings = this.translator.userSettings.settings;
  3468. if (!settings.pageTranslation.autoTranslate) {
  3469. return {
  3470. success: false,
  3471. message: "Tự động dịch đang tắt",
  3472. };
  3473. }
  3474. const languageCheck = await this.detectLanguage();
  3475. if (languageCheck.isVietnamese) {
  3476. return {
  3477. success: false,
  3478. message: languageCheck.message,
  3479. };
  3480. }
  3481. const result = await this.translatePage();
  3482. if (result.success) {
  3483. const toolsContainer = document.querySelector(
  3484. ".translator-tools-container"
  3485. );
  3486. if (toolsContainer) {
  3487. const menuItem = toolsContainer.querySelector(
  3488. '[data-type="pageTranslate"]'
  3489. );
  3490. if (menuItem) {
  3491. const itemText = menuItem.querySelector(".item-text");
  3492. if (itemText) {
  3493. itemText.textContent = this.isTranslated
  3494. ? "Bản gốc"
  3495. : "Dịch trang";
  3496. }
  3497. }
  3498. }
  3499. const floatingButton = document.querySelector(
  3500. ".page-translate-button"
  3501. );
  3502. if (floatingButton) {
  3503. floatingButton.innerHTML = this.isTranslated
  3504. ? "📄 Bản gốc"
  3505. : "📄 Dịch trang";
  3506. }
  3507. this.translator.ui.showNotification(result.message, "success");
  3508. } else {
  3509. this.translator.ui.showNotification(result.message, "warning");
  3510. }
  3511. return result;
  3512. } catch (error) {
  3513. console.error("Translation check error:", error);
  3514. return {
  3515. success: false,
  3516. message: error.message,
  3517. };
  3518. }
  3519. }
  3520. async translatePage() {
  3521. try {
  3522. if (!this.domObserver) {
  3523. this.setupDOMObserver();
  3524. // this.domObserver.disconnect();
  3525. // this.domObserver = null;
  3526. }
  3527. if (this.isTranslated) {
  3528. for (const [node, originalText] of this.originalTexts.entries()) {
  3529. if (node && node.parentNode) {
  3530. node.textContent = originalText;
  3531. }
  3532. }
  3533. this.originalTexts.clear();
  3534. this.isTranslated = false;
  3535. this.updateUI("Dịch trang", "📄 Dịch trang");
  3536. return {
  3537. success: true,
  3538. message: "Đã chuyển về văn bản gốc",
  3539. };
  3540. }
  3541. const textNodes = this.collectTextNodes();
  3542. if (textNodes.length === 0) {
  3543. return {
  3544. success: false,
  3545. message: "Không tìm thấy nội dung cần dịch",
  3546. };
  3547. }
  3548. const settings = this.translator.userSettings.settings.displayOptions;
  3549. const mode = settings.translationMode;
  3550. const showSource = settings.languageLearning.showSource;
  3551. const chunks = this.createChunks(textNodes);
  3552. for (const chunk of chunks) {
  3553. const textsToTranslate = chunk
  3554. .map((node) => node.textContent.trim())
  3555. .filter((text) => text.length > 0)
  3556. .join("\n");
  3557. if (!textsToTranslate) continue;
  3558. try {
  3559. const prompt = this.translator.createPrompt(
  3560. textsToTranslate,
  3561. "page"
  3562. );
  3563. const translatedText = await this.translator.api.request(prompt);
  3564. if (translatedText) {
  3565. const translatedParts = translatedText.split("\n");
  3566. let translationIndex = 0;
  3567. for (let i = 0; i < chunk.length; i++) {
  3568. const node = chunk[i];
  3569. const text = node.textContent.trim();
  3570. if (text.length > 0 && node.parentNode) {
  3571. this.originalTexts.set(node, node.textContent);
  3572. if (translationIndex < translatedParts.length) {
  3573. let translated =
  3574. translatedParts[translationIndex++] || node.textContent;
  3575. let output = "";
  3576. if (mode === "translation_only") {
  3577. output = translated;
  3578. } else if (mode === "parallel") {
  3579. output = `[GC]: ${text} [DCH]: ${translated.split("<|>")[2]?.trim() || translated} `;
  3580. } else if (mode === "language_learning") {
  3581. let sourceHTML = "";
  3582. if (showSource) {
  3583. sourceHTML = `[GC]: ${text}`;
  3584. }
  3585. let pinyinHTML = "";
  3586. const pinyin = translated.split("<|>")[1]?.trim();
  3587. if (pinyin) {
  3588. pinyinHTML = `[PINYIN]: ${pinyin}`;
  3589. }
  3590. const translation =
  3591. translated.split("<|>")[2]?.trim() || translated;
  3592. const translationHTML = `[DCH]: ${translation}`;
  3593. output = `${sourceHTML} ${pinyinHTML} ${translationHTML} `;
  3594. }
  3595. node.textContent = output;
  3596. }
  3597. }
  3598. }
  3599. }
  3600. } catch (error) {
  3601. console.error("Error translating chunk:", error);
  3602. }
  3603. }
  3604. this.isTranslated = true;
  3605. return {
  3606. success: true,
  3607. message: "Đã dịch xong trang",
  3608. };
  3609. } catch (error) {
  3610. console.error("Page translation error:", error);
  3611. return {
  3612. success: false,
  3613. message: error.message,
  3614. };
  3615. }
  3616. }
  3617. async detectContext(text) {
  3618. const prompt = `Analyze the context and writing style of this text and return JSON format with these properties:
  3619. - style: formal/informal/technical/casual
  3620. - tone: professional/friendly/neutral/academic
  3621. - domain: general/technical/business/academic/other
  3622. Text: "${text}"`;
  3623. try {
  3624. const analysis = await this.translator.api.request(prompt);
  3625. const result = JSON.parse(analysis);
  3626. return {
  3627. style: result.style,
  3628. tone: result.tone,
  3629. domain: result.domain,
  3630. };
  3631. } catch (error) {
  3632. console.error("Context detection failed:", error);
  3633. return {
  3634. style: "neutral",
  3635. tone: "neutral",
  3636. domain: "general",
  3637. };
  3638. }
  3639. }
  3640. distributeChunks(chunks, groupCount) {
  3641. const groups = Array(groupCount)
  3642. .fill()
  3643. .map(() => []);
  3644. let currentSize = 0;
  3645. let currentGroup = 0;
  3646. chunks.forEach((chunk) => {
  3647. groups[currentGroup].push(chunk);
  3648. currentSize += chunk.length;
  3649. if (currentSize >= Math.ceil(chunks.length / groupCount)) {
  3650. currentGroup = (currentGroup + 1) % groupCount;
  3651. currentSize = 0;
  3652. }
  3653. });
  3654. return groups.filter((group) => group.length > 0);
  3655. }
  3656. async translateChunkGroup(chunks, apiKey) {
  3657. const results = [];
  3658. for (const chunk of chunks) {
  3659. try {
  3660. await this.rateLimiter.waitForSlot();
  3661. const result = await this.translateChunkWithKey(chunk, apiKey);
  3662. if (result) {
  3663. results.push(result);
  3664. }
  3665. } catch (error) {
  3666. if (
  3667. error.message.includes("rate limit") ||
  3668. error.message.includes("API key not valid")
  3669. ) {
  3670. this.translator.api.keyManager.markKeyAsFailed(apiKey);
  3671. throw error;
  3672. }
  3673. console.error("Chunk translation error:", error);
  3674. }
  3675. }
  3676. return results;
  3677. }
  3678. async translateChunkWithKey(chunk, apiKey) {
  3679. const textsToTranslate = chunk
  3680. .map((node) => node.textContent.trim())
  3681. .filter((text) => text.length > 0)
  3682. .join("\n");
  3683. if (!textsToTranslate) return false;
  3684. try {
  3685. const settings = this.translator.userSettings.settings;
  3686. const selectedModel = this.translator.api.getGeminiModel();
  3687. const prompt = this.translator.createPrompt(textsToTranslate, "page");
  3688. const response = await new Promise((resolve, reject) => {
  3689. GM_xmlhttpRequest({
  3690. method: "POST",
  3691. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${apiKey}`,
  3692. headers: { "Content-Type": "application/json" },
  3693. data: JSON.stringify({
  3694. contents: [
  3695. {
  3696. parts: [
  3697. {
  3698. text: prompt,
  3699. },
  3700. ],
  3701. },
  3702. ],
  3703. generationConfig: {
  3704. temperature: settings.ocrOptions.temperature,
  3705. topP: settings.ocrOptions.topP,
  3706. topK: settings.ocrOptions.topK,
  3707. },
  3708. }),
  3709. onload: (response) => {
  3710. if (response.status === 200) {
  3711. try {
  3712. const result = JSON.parse(response.responseText);
  3713. if (result?.candidates?.[0]?.content?.parts?.[0]?.text) {
  3714. resolve(result.candidates[0].content.parts[0].text);
  3715. } else {
  3716. reject(new Error("Invalid response format"));
  3717. }
  3718. } catch (error) {
  3719. reject(new Error("Failed to parse response"));
  3720. }
  3721. } else {
  3722. if (response.status === 400) {
  3723. reject(new Error("API key not valid"));
  3724. } else if (response.status === 429) {
  3725. reject(new Error("API key rate limit exceeded"));
  3726. } else {
  3727. reject(new Error(`API Error: ${response.status}`));
  3728. }
  3729. }
  3730. },
  3731. onerror: (error) => reject(error),
  3732. });
  3733. });
  3734. const translations = response.split("\n");
  3735. chunk.forEach((node, index) => {
  3736. if (translations[index]) {
  3737. this.originalTexts.set(node, node.textContent);
  3738. node.textContent = translations[index].trim();
  3739. }
  3740. });
  3741. return true;
  3742. } catch (error) {
  3743. throw error;
  3744. }
  3745. }
  3746. async translateHTML(htmlContent) {
  3747. try {
  3748. const parser = new DOMParser();
  3749. const doc = parser.parseFromString(htmlContent, "text/html");
  3750. const scripts = doc.getElementsByTagName("script");
  3751. const styles = doc.getElementsByTagName("style");
  3752. [...scripts, ...styles].forEach(element => element.remove());
  3753. const translatableNodes = this.getTranslatableHTMLNodes(doc.body);
  3754. const chunks = this.createHTMLChunks(translatableNodes);
  3755. this.translator.ui.showTranslatingStatus();
  3756. const { translationMode: mode } = this.translator.userSettings.settings.displayOptions;
  3757. const SEPARATOR = "<<|SPLIT|>>";
  3758. for (const chunk of chunks) {
  3759. const textsToTranslate = chunk.nodes
  3760. .map(node => ({
  3761. text: node.textContent.trim(),
  3762. type: node.nodeType,
  3763. isAttribute: node.isAttribute,
  3764. attributeName: node.attributeName
  3765. }))
  3766. .filter(item => item.text.length > 0);
  3767. if (textsToTranslate.length === 0) continue;
  3768. const textToTranslate = textsToTranslate
  3769. .map(item => item.text)
  3770. .join(SEPARATOR);
  3771. const prompt = this.translator.createPrompt(textToTranslate, "page");
  3772. const translatedText = await this.translator.api.request(prompt);
  3773. const translatedParts = translatedText.split(SEPARATOR);
  3774. chunk.nodes.forEach((node, index) => {
  3775. if (index >= translatedParts.length) return;
  3776. const translation = translatedParts[index];
  3777. if (!translation) return;
  3778. const originalText = node.textContent.trim();
  3779. let formattedTranslation = "";
  3780. if (mode === "translation_only") {
  3781. formattedTranslation = translation;
  3782. } else if (mode === "parallel") {
  3783. formattedTranslation = `[Gc]: ${originalText}\n [DCH]: ${translation.split("<|>")[2] || translation} `;
  3784. } else if (mode === "language_learning") {
  3785. let sourceHTML = "";
  3786. if (showSource) {
  3787. sourceHTML = `[Gc]: ${originalText}`;
  3788. }
  3789. formattedTranslation = `${sourceHTML}\n [Pinyin]: ${translation.split("<|>")[1] || ""}\n [Dch]: ${translation.split("<|>")[2] || translation} `;
  3790. }
  3791. if (node.isAttribute) {
  3792. node.ownerElement.setAttribute(node.attributeName, formattedTranslation.trim());
  3793. } else {
  3794. node.textContent = formattedTranslation.trim();
  3795. }
  3796. });
  3797. }
  3798. return doc.documentElement.outerHTML;
  3799. } catch (error) {
  3800. console.error("Lỗi dịch HTML:", error);
  3801. throw error;
  3802. } finally {
  3803. this.translator.ui.removeTranslatingStatus();
  3804. }
  3805. }
  3806. getTranslatableHTMLNodes(element) {
  3807. const translatableNodes = [];
  3808. const excludeSelectors = this.getExcludeSelectors();
  3809. const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
  3810. acceptNode: (node) => {
  3811. const parent = node.parentElement;
  3812. if (!parent) return NodeFilter.FILTER_REJECT;
  3813. if (excludeSelectors.some((selector) => parent.matches?.(selector))) {
  3814. return NodeFilter.FILTER_REJECT;
  3815. }
  3816. return node.textContent.trim()
  3817. ? NodeFilter.FILTER_ACCEPT
  3818. : NodeFilter.FILTER_REJECT;
  3819. },
  3820. });
  3821. let node;
  3822. while ((node = walker.nextNode())) {
  3823. translatableNodes.push(node);
  3824. }
  3825. const elements = element.getElementsByTagName("*");
  3826. const translatableAttributes = ["title", "alt", "placeholder"];
  3827. for (const el of elements) {
  3828. for (const attr of translatableAttributes) {
  3829. if (el.hasAttribute(attr)) {
  3830. const value = el.getAttribute(attr);
  3831. if (value && value.trim()) {
  3832. const node = document.createTextNode(value);
  3833. node.isAttribute = true;
  3834. node.attributeName = attr;
  3835. node.ownerElement = el;
  3836. translatableNodes.push(node);
  3837. }
  3838. }
  3839. }
  3840. }
  3841. return translatableNodes;
  3842. }
  3843. createHTMLChunks(nodes, maxChunkSize = 1000) {
  3844. const chunks = [];
  3845. let currentChunk = { nodes: [], size: 0 };
  3846. for (const node of nodes) {
  3847. const textLength = node.textContent.length;
  3848. if (currentChunk.size + textLength > maxChunkSize) {
  3849. if (currentChunk.nodes.length > 0) {
  3850. chunks.push(currentChunk);
  3851. }
  3852. currentChunk = { nodes: [node], size: textLength };
  3853. } else {
  3854. currentChunk.nodes.push(node);
  3855. currentChunk.size += textLength;
  3856. }
  3857. }
  3858. if (currentChunk.nodes.length > 0) {
  3859. chunks.push(currentChunk);
  3860. }
  3861. return chunks;
  3862. }
  3863. async loadPDFJS() {
  3864. if (!this.pdfLoaded) {
  3865. pdfjsLib.GlobalWorkerOptions.workerSrc =
  3866. "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
  3867. this.pdfLoaded = true;
  3868. }
  3869. }
  3870. async translatePDF(file) {
  3871. try {
  3872. await this.loadPDFJS();
  3873. const arrayBuffer = await file.arrayBuffer();
  3874. const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
  3875. let translatedContent = [];
  3876. const totalPages = pdf.numPages;
  3877. const canvas = document.createElement("canvas");
  3878. const ctx = canvas.getContext("2d");
  3879. const { translationMode: mode } = this.translator.userSettings.settings.displayOptions;
  3880. const showSource = mode === "language_learning" &&
  3881. this.translator.userSettings.settings.displayOptions.languageLearning.showSource;
  3882. for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
  3883. const page = await pdf.getPage(pageNum);
  3884. const viewport = page.getViewport({ scale: 2.0 });
  3885. canvas.height = viewport.height;
  3886. canvas.width = viewport.width;
  3887. await page.render({
  3888. canvasContext: ctx,
  3889. viewport: viewport,
  3890. }).promise;
  3891. const imageBlob = await new Promise((resolve) =>
  3892. canvas.toBlob(resolve, "image/png")
  3893. );
  3894. const imageFile = new File([imageBlob], "page.png", {
  3895. type: "image/png",
  3896. });
  3897. try {
  3898. const ocrResult = await this.translator.ocr.processImage(imageFile);
  3899. const processedTranslations = ocrResult.split('\n').map((trans) => {
  3900. switch (mode) {
  3901. case "translation_only":
  3902. return `${trans.split("<|>")[0]?.trim() || ''} `;
  3903. case "parallel":
  3904. return `[GC]: ${trans.split("<|>")[0]?.trim() || ''} [DCH]: ${trans.split("<|>")[2]?.trim() || ''} `;
  3905. case "language_learning":
  3906. let parts = [];
  3907. if (showSource) {
  3908. parts.push(`[GC]: ${trans.split("<|>")[0]?.trim() || ''}`);
  3909. }
  3910. const pinyin = trans.split("<|>")[1]?.trim();
  3911. if (pinyin) {
  3912. parts.push(`[PINYIN]: ${pinyin}`);
  3913. }
  3914. const translation = trans.split("<|>")[2]?.trim() || trans;
  3915. parts.push(`[DCH]: ${translation} `);
  3916. return parts.join(" ");
  3917. default:
  3918. return trans;
  3919. }
  3920. });
  3921. translatedContent.push({
  3922. pageNum,
  3923. original: ocrResult,
  3924. translations: processedTranslations,
  3925. displayMode: mode,
  3926. showSource
  3927. });
  3928. } catch (error) {
  3929. console.error(`Error processing page ${pageNum}:`, error);
  3930. translatedContent.push({
  3931. pageNum,
  3932. original: `[Error on page ${pageNum}: ${error.message}]`,
  3933. translations: [{
  3934. original: "",
  3935. translation: `[Translation Error: ${error.message}]`
  3936. }],
  3937. displayMode: mode,
  3938. showSource
  3939. });
  3940. }
  3941. this.translator.ui.updateProgress(
  3942. "Đang xử lý PDF",
  3943. Math.round((pageNum / totalPages) * 100)
  3944. );
  3945. ctx.clearRect(0, 0, canvas.width, canvas.height);
  3946. }
  3947. canvas.remove();
  3948. return this.generateEnhancedTranslatedPDF(translatedContent);
  3949. } catch (error) {
  3950. console.error("PDF translation error:", error);
  3951. throw error;
  3952. }
  3953. }
  3954. generateEnhancedTranslatedPDF(translatedContent) {
  3955. const htmlContent = `
  3956. <!DOCTYPE html>
  3957. <html>
  3958. <head>
  3959. <meta charset="UTF-8">
  3960. <style>
  3961. body {
  3962. font-family: Arial, sans-serif;
  3963. line-height: 1.6;
  3964. max-width: 900px;
  3965. margin: 0 auto;
  3966. padding: 20px;
  3967. }
  3968. .page {
  3969. margin-bottom: 40px;
  3970. padding: 20px;
  3971. border: 1px solid #ddd;
  3972. border-radius: 8px;
  3973. page-break-after: always;
  3974. }
  3975. .page-number {
  3976. font-size: 18px;
  3977. font-weight: bold;
  3978. margin-bottom: 20px;
  3979. color: #666;
  3980. }
  3981. .content {
  3982. margin-bottom: 20px;
  3983. }
  3984. .section {
  3985. margin-bottom: 15px;
  3986. padding: 15px;
  3987. background-color: #fff;
  3988. border: 1px solid #eee;
  3989. border-radius: 8px;
  3990. white-space: pre-wrap;
  3991. }
  3992. .section-title {
  3993. font-weight: bold;
  3994. color: #333;
  3995. margin-bottom: 10px;
  3996. }
  3997. .section-content {
  3998. white-space: pre-wrap;
  3999. line-height: 1.5;
  4000. }
  4001. h3 {
  4002. color: #333;
  4003. margin: 10px 0;
  4004. }
  4005. @media print {
  4006. .page {
  4007. page-break-after: always;
  4008. }
  4009. }
  4010. </style>
  4011. </head>
  4012. <body>
  4013. ${translatedContent.map(page => `
  4014. <div class="page">
  4015. <div class="page-number">Trang ${page.pageNum}</div>
  4016. <div class="content">
  4017. ${page.displayMode === "translation_only" ? `
  4018. <div class="section">
  4019. <div class="section-title">Bn dch:</div>
  4020. <div class="section-content">${this.formatTranslationContent(page.translations.join('\n'))}</div>
  4021. </div>
  4022. ` : page.displayMode === "parallel" ? `
  4023. <div class="section">
  4024. <div class="section-content">${this.formatTranslationContent(page.translations.join('\n'))}</div>
  4025. </div>
  4026. ` : `
  4027. ${page.showSource ? `
  4028. <div class="section">
  4029. <div class="section-title">Bn gc:</div>
  4030. <div class="section-content">${this.formatTranslationContent(page.original)}</div>
  4031. </div>
  4032. ` : ''}
  4033. ${page.translations.some(t => t.includes("[PINYIN]:")) ? `
  4034. <div class="section">
  4035. <div class="section-title">Phiên âm:</div>
  4036. <div class="section-content">${this.formatTranslationContent(
  4037. page.translations
  4038. .map(t => t.split("[PINYIN]:")[1]?.split("[DỊCH]:")[0]?.trim())
  4039. .filter(Boolean)
  4040. .join('\n')
  4041. )}</div>
  4042. </div>
  4043. ` : ''}
  4044. <div class="section">
  4045. <div class="section-title">Bn dch:</div>
  4046. <div class="section-content">${this.formatTranslationContent(
  4047. page.translations
  4048. .map(t => t.split("[DỊCH]:")[1]?.trim())
  4049. .filter(Boolean)
  4050. .join('\n')
  4051. )}</div>
  4052. </div>
  4053. `}
  4054. </div>
  4055. </div>
  4056. `).join('')}
  4057. </body>
  4058. </html>
  4059. `;
  4060. return new Blob([htmlContent], { type: "text/html" });
  4061. }
  4062. formatTranslationContent(content) {
  4063. if (!content) return '';
  4064. return content
  4065. .replace(/&/g, '&amp;')
  4066. .replace(/</g, '&lt;')
  4067. .replace(/>/g, '&gt;')
  4068. .replace(/"/g, '&quot;')
  4069. .replace(/'/g, '&#039;')
  4070. .replace(/\n/g, '<br>');
  4071. }
  4072. groupIntoParagraphs(textItems) {
  4073. let paragraphs = [];
  4074. let currentParagraph = {
  4075. text: "",
  4076. format: {},
  4077. type: "text",
  4078. };
  4079. for (let i = 0; i < textItems.length; i++) {
  4080. const item = textItems[i];
  4081. const nextItem = textItems[i + 1];
  4082. if (item.fontSize > 20) {
  4083. currentParagraph.type = "heading";
  4084. } else if (item.text.match(/^\d+\./)) {
  4085. currentParagraph.type = "list-item";
  4086. }
  4087. currentParagraph.text += item.text;
  4088. currentParagraph.format = {
  4089. fontSize: item.fontSize,
  4090. fontFamily: item.fontFamily,
  4091. isAnnotation: item.type === "annotation",
  4092. };
  4093. const shouldEndParagraph =
  4094. !nextItem ||
  4095. Math.abs(nextItem.y - item.y) > 1.5 * item.fontSize || // Khoảng cách dọc lớn
  4096. (nextItem.fontSize > 20 && item.fontSize <= 20) || // Chuyển từ text thường sang heading
  4097. item.text.endsWith(".") || // Kết thúc câu
  4098. item.text.endsWith("?") ||
  4099. item.text.endsWith("!");
  4100. if (shouldEndParagraph) {
  4101. if (currentParagraph.text.trim()) {
  4102. paragraphs.push({ ...currentParagraph });
  4103. }
  4104. currentParagraph = {
  4105. text: "",
  4106. format: {},
  4107. type: "text",
  4108. };
  4109. } else {
  4110. currentParagraph.text += " ";
  4111. }
  4112. }
  4113. return paragraphs;
  4114. }
  4115. splitIntoChunks(text, maxLength = 1000) {
  4116. const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
  4117. const chunks = [];
  4118. let currentChunk = "";
  4119. for (const sentence of sentences) {
  4120. if ((currentChunk + sentence).length > maxLength && currentChunk) {
  4121. chunks.push(currentChunk.trim());
  4122. currentChunk = "";
  4123. }
  4124. currentChunk += sentence + " ";
  4125. }
  4126. if (currentChunk) {
  4127. chunks.push(currentChunk.trim());
  4128. }
  4129. return chunks;
  4130. }
  4131. rateLimiter = {
  4132. queue: [],
  4133. lastRequestTime: 0,
  4134. requestCount: 0,
  4135. async waitForSlot() {
  4136. const now = Date.now();
  4137. const settings = this.translator.userSettings.settings;
  4138. const { maxRequests, perMilliseconds } = settings.rateLimit;
  4139. this.queue = this.queue.filter((time) => now - time < perMilliseconds);
  4140. if (this.queue.length >= maxRequests) {
  4141. const oldestRequest = this.queue[0];
  4142. const waitTime = perMilliseconds - (now - oldestRequest);
  4143. if (waitTime > 0) {
  4144. await new Promise((resolve) => setTimeout(resolve, waitTime));
  4145. }
  4146. this.queue.shift();
  4147. }
  4148. this.queue.push(now);
  4149. },
  4150. };
  4151. updateUI(menuText, buttonText) {
  4152. const toolsContainer = document.querySelector(
  4153. ".translator-tools-container"
  4154. );
  4155. if (toolsContainer) {
  4156. const menuItem = toolsContainer.querySelector(
  4157. '[data-type="pageTranslate"]'
  4158. );
  4159. if (menuItem) {
  4160. const itemText = menuItem.querySelector(".item-text");
  4161. if (itemText) {
  4162. itemText.textContent = menuText;
  4163. }
  4164. }
  4165. }
  4166. const floatingButton = document.querySelector(".page-translate-button");
  4167. if (floatingButton) {
  4168. floatingButton.innerHTML = buttonText;
  4169. }
  4170. }
  4171. async getPageCache(url) {
  4172. const settings = this.translator.userSettings.settings;
  4173. if (!settings.cacheOptions.page.enabled) return null;
  4174. const cacheData = this.pageCache.get(url);
  4175. if (
  4176. cacheData &&
  4177. Date.now() - cacheData.timestamp <
  4178. settings.cacheOptions.page.expirationTime
  4179. ) {
  4180. return cacheData;
  4181. }
  4182. return null;
  4183. }
  4184. async setPageCache(translation, url) {
  4185. const settings = this.translator.userSettings.settings;
  4186. if (!settings.cacheOptions.page.enabled) return;
  4187. if (this.pageCache.size >= settings.cacheOptions.page.maxSize) {
  4188. const oldestKey = this.pageCache.keys().next().value;
  4189. this.pageCache.delete(oldestKey);
  4190. }
  4191. this.pageCache.set(url, { translation, timestamp: Date.now() });
  4192. }
  4193. restoreOriginalText() {
  4194. for (const [node, originalText] of this.originalTexts) {
  4195. node.textContent = originalText;
  4196. }
  4197. this.originalTexts.clear();
  4198. }
  4199. applyTranslation(translation) {
  4200. const lines = translation.split("\n");
  4201. this.collectTextNodes().forEach((node, index) => {
  4202. node.textContent = lines[index] || "";
  4203. });
  4204. }
  4205. collectTextNodes() {
  4206. const excludeSelectors = this.getExcludeSelectors();
  4207. const walker = document.createTreeWalker(
  4208. document.body,
  4209. NodeFilter.SHOW_TEXT,
  4210. {
  4211. acceptNode: (node) => {
  4212. if (!node.textContent.trim()) {
  4213. return NodeFilter.FILTER_REJECT;
  4214. }
  4215. if (!node.parentNode) {
  4216. return NodeFilter.FILTER_REJECT;
  4217. }
  4218. let parent = node.parentElement;
  4219. while (parent) {
  4220. for (const selector of excludeSelectors) {
  4221. try {
  4222. if (parent.matches && parent.matches(selector)) {
  4223. return NodeFilter.FILTER_REJECT;
  4224. }
  4225. } catch (e) {
  4226. console.warn(`Invalid selector: ${selector}`, e);
  4227. }
  4228. }
  4229. if (
  4230. parent.getAttribute("translate") === "no" ||
  4231. parent.getAttribute("class")?.includes("notranslate") ||
  4232. parent.getAttribute("class")?.includes("no-translate")
  4233. ) {
  4234. return NodeFilter.FILTER_REJECT;
  4235. }
  4236. parent = parent.parentElement;
  4237. }
  4238. return NodeFilter.FILTER_ACCEPT;
  4239. },
  4240. }
  4241. );
  4242. const nodes = [];
  4243. let node;
  4244. while ((node = walker.nextNode())) {
  4245. nodes.push(node);
  4246. }
  4247. return nodes;
  4248. }
  4249. createChunks(nodes) {
  4250. const chunks = [];
  4251. let currentChunk = [];
  4252. let currentLength = 0;
  4253. const maxChunkLength = 1000;
  4254. for (const node of nodes) {
  4255. const text = node.textContent.trim();
  4256. if (
  4257. currentLength + text.length > maxChunkLength &&
  4258. currentChunk.length > 0
  4259. ) {
  4260. chunks.push(currentChunk);
  4261. currentChunk = [];
  4262. currentLength = 0;
  4263. }
  4264. currentChunk.push(node);
  4265. currentLength += text.length;
  4266. }
  4267. if (currentChunk.length > 0) {
  4268. chunks.push(currentChunk);
  4269. }
  4270. return chunks;
  4271. }
  4272. setupDOMObserver() {
  4273. if (this.domObserver) {
  4274. this.domObserver.disconnect();
  4275. this.domObserver = null;
  4276. }
  4277. this.domObserver = new MutationObserver((mutations) => {
  4278. const newTextNodes = [];
  4279. for (const mutation of mutations) {
  4280. if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
  4281. const nodes = this.getTextNodesFromNodeList(mutation.addedNodes);
  4282. if (nodes.length > 0) {
  4283. newTextNodes.push(...nodes);
  4284. }
  4285. }
  4286. }
  4287. if (newTextNodes.length > 0) {
  4288. const chunks = this.createChunks(newTextNodes);
  4289. Promise.all(
  4290. chunks.map((chunk) =>
  4291. this.translateChunkParallel(chunk).catch((error) => {
  4292. console.error("Translation error for chunk:", error);
  4293. })
  4294. )
  4295. );
  4296. }
  4297. });
  4298. this.domObserver.observe(document.body, {
  4299. childList: true,
  4300. subtree: true,
  4301. characterData: true,
  4302. });
  4303. }
  4304. getTextNodesFromNodeList(nodeList) {
  4305. const excludeSelectors = this.getExcludeSelectors();
  4306. const textNodes = [];
  4307. const shouldExclude = (node) => {
  4308. if (!node) return true;
  4309. let current = node;
  4310. while (current) {
  4311. if (
  4312. current.getAttribute &&
  4313. (current.getAttribute("translate") === "no" ||
  4314. current.getAttribute("data-notranslate") ||
  4315. current.classList?.contains("notranslate") ||
  4316. current.classList?.contains("no-translate"))
  4317. ) {
  4318. return true;
  4319. }
  4320. for (const selector of excludeSelectors) {
  4321. try {
  4322. if (current.matches && current.matches(selector)) {
  4323. return true;
  4324. }
  4325. } catch (e) {
  4326. console.warn(`Invalid selector: ${selector}`, e);
  4327. }
  4328. }
  4329. current = current.parentElement;
  4330. }
  4331. return false;
  4332. };
  4333. nodeList.forEach((node) => {
  4334. if (node.nodeType === Node.TEXT_NODE) {
  4335. if (node.textContent.trim() && !shouldExclude(node.parentElement)) {
  4336. textNodes.push(node);
  4337. }
  4338. } else if (
  4339. node.nodeType === Node.ELEMENT_NODE &&
  4340. !shouldExclude(node)
  4341. ) {
  4342. const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
  4343. acceptNode: (textNode) => {
  4344. if (
  4345. textNode.textContent.trim() &&
  4346. !shouldExclude(textNode.parentElement)
  4347. ) {
  4348. return NodeFilter.FILTER_ACCEPT;
  4349. }
  4350. return NodeFilter.FILTER_REJECT;
  4351. },
  4352. });
  4353. let textNode;
  4354. while ((textNode = walker.nextNode())) {
  4355. textNodes.push(textNode);
  4356. }
  4357. }
  4358. });
  4359. return textNodes;
  4360. }
  4361. async translateChunkParallel(chunk) {
  4362. try {
  4363. const textsToTranslate = chunk
  4364. .map((node) => node.textContent.trim())
  4365. .filter((text) => text.length > 0)
  4366. .join("\n");
  4367. if (!textsToTranslate) return;
  4368. const prompt = this.translator.createPrompt(textsToTranslate, "page");
  4369. const translatedText = await this.translator.api.request(prompt);
  4370. if (translatedText) {
  4371. const translatedParts = translatedText.split("\n");
  4372. let translationIndex = 0;
  4373. for (let i = 0; i < chunk.length; i++) {
  4374. const node = chunk[i];
  4375. const text = node.textContent.trim();
  4376. if (text.length > 0 && node.parentNode) {
  4377. this.originalTexts.set(node, node.textContent);
  4378. if (translationIndex < translatedParts.length) {
  4379. const translated = translatedParts[translationIndex++];
  4380. const settings =
  4381. this.translator.userSettings.settings.displayOptions;
  4382. const mode = settings.translationMode;
  4383. let output = this.formatTranslation(
  4384. text,
  4385. translated,
  4386. mode,
  4387. settings
  4388. );
  4389. node.textContent = output;
  4390. }
  4391. }
  4392. }
  4393. }
  4394. } catch (error) {
  4395. console.error("Chunk translation error:", error);
  4396. throw error;
  4397. }
  4398. }
  4399. formatTranslation(originalText, translatedText, mode, settings) {
  4400. const showSource = settings.languageLearning.showSource;
  4401. switch (mode) {
  4402. case "translation_only":
  4403. return translatedText;
  4404. case "parallel":
  4405. return `[GC]: ${originalText} [DCH]: ${translatedText.split("<|>")[2]?.trim() || translatedText} `;
  4406. case "language_learning":
  4407. let parts = [];
  4408. if (showSource) {
  4409. parts.push(`[GC]: ${originalText}`);
  4410. }
  4411. const pinyin = translatedText.split("<|>")[1]?.trim();
  4412. if (pinyin) {
  4413. parts.push(`[PINYIN]: ${pinyin}`);
  4414. }
  4415. const translation =
  4416. translatedText.split("<|>")[2]?.trim() || translatedText;
  4417. parts.push(`[DCH]: ${translation} `);
  4418. return parts.join(" ");
  4419. default:
  4420. return translatedText;
  4421. }
  4422. }
  4423. }
  4424. class ImageCache {
  4425. constructor() {
  4426. this.maxSize = CONFIG.CACHE.image.maxSize;
  4427. this.expirationTime = CONFIG.CACHE.image.expirationTime;
  4428. this.cache = new Map();
  4429. this.accessOrder = [];
  4430. }
  4431. async generateKey(imageData) {
  4432. const hashBuffer = await crypto.subtle.digest(
  4433. "SHA-256",
  4434. new TextEncoder().encode(imageData)
  4435. );
  4436. const hashArray = Array.from(new Uint8Array(hashBuffer));
  4437. return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
  4438. }
  4439. async set(imageData, ocrResult) {
  4440. const key = await this.generateKey(imageData);
  4441. if (this.cache.has(key)) {
  4442. const index = this.accessOrder.indexOf(key);
  4443. this.accessOrder.splice(index, 1);
  4444. this.accessOrder.push(key);
  4445. } else {
  4446. if (this.cache.size >= this.maxSize) {
  4447. const oldestKey = this.accessOrder.shift();
  4448. this.cache.delete(oldestKey);
  4449. }
  4450. this.accessOrder.push(key);
  4451. }
  4452. this.cache.set(key, {
  4453. result: ocrResult,
  4454. timestamp: Date.now(),
  4455. });
  4456. }
  4457. async get(imageData) {
  4458. const key = await this.generateKey(imageData);
  4459. const data = this.cache.get(key);
  4460. if (!data) return null;
  4461. if (Date.now() - data.timestamp > this.expirationTime) {
  4462. this.cache.delete(key);
  4463. const index = this.accessOrder.indexOf(key);
  4464. this.accessOrder.splice(index, 1);
  4465. return null;
  4466. }
  4467. const index = this.accessOrder.indexOf(key);
  4468. this.accessOrder.splice(index, 1);
  4469. this.accessOrder.push(key);
  4470. return data.result;
  4471. }
  4472. clear() {
  4473. this.cache.clear();
  4474. this.accessOrder = [];
  4475. }
  4476. }
  4477. class MediaCache {
  4478. constructor() {
  4479. this.maxSize = CONFIG.CACHE.media.maxSize;
  4480. this.expirationTime = CONFIG.CACHE.media.expirationTime;
  4481. this.cache = new Map();
  4482. this.accessOrder = [];
  4483. }
  4484. async generateKey(fileData) {
  4485. const hashBuffer = await crypto.subtle.digest(
  4486. "SHA-256",
  4487. new TextEncoder().encode(fileData)
  4488. );
  4489. const hashArray = Array.from(new Uint8Array(hashBuffer));
  4490. return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
  4491. }
  4492. async set(fileData, translation) {
  4493. const key = await this.generateKey(fileData);
  4494. if (this.cache.has(key)) {
  4495. const index = this.accessOrder.indexOf(key);
  4496. this.accessOrder.splice(index, 1);
  4497. this.accessOrder.push(key);
  4498. } else {
  4499. if (this.cache.size >= this.maxSize) {
  4500. const oldestKey = this.accessOrder.shift();
  4501. this.cache.delete(oldestKey);
  4502. }
  4503. this.accessOrder.push(key);
  4504. }
  4505. this.cache.set(key, {
  4506. translation,
  4507. timestamp: Date.now(),
  4508. });
  4509. }
  4510. async get(fileData) {
  4511. const key = await this.generateKey(fileData);
  4512. const data = this.cache.get(key);
  4513. if (!data) return null;
  4514. if (Date.now() - data.timestamp > this.expirationTime) {
  4515. this.cache.delete(key);
  4516. const index = this.accessOrder.indexOf(key);
  4517. this.accessOrder.splice(index, 1);
  4518. return null;
  4519. }
  4520. const index = this.accessOrder.indexOf(key);
  4521. this.accessOrder.splice(index, 1);
  4522. this.accessOrder.push(key);
  4523. return data.translation;
  4524. }
  4525. clear() {
  4526. this.cache.clear();
  4527. this.accessOrder = [];
  4528. }
  4529. }
  4530. class TranslationCache {
  4531. constructor(maxSize, expirationTime) {
  4532. this.maxSize = maxSize;
  4533. this.expirationTime = expirationTime;
  4534. this.cache = new Map();
  4535. this.accessOrder = [];
  4536. }
  4537. generateKey(text, isAdvanced, targetLanguage) {
  4538. return `${text}_${isAdvanced}_${targetLanguage}`;
  4539. }
  4540. set(text, translation, isAdvanced, targetLanguage) {
  4541. const key = this.generateKey(text, isAdvanced, targetLanguage);
  4542. if (this.cache.has(key)) {
  4543. const index = this.accessOrder.indexOf(key);
  4544. this.accessOrder.splice(index, 1);
  4545. this.accessOrder.push(key);
  4546. } else {
  4547. if (this.cache.size >= this.maxSize) {
  4548. const oldestKey = this.accessOrder.shift();
  4549. this.cache.delete(oldestKey);
  4550. }
  4551. this.accessOrder.push(key);
  4552. }
  4553. this.cache.set(key, {
  4554. translation,
  4555. timestamp: Date.now(),
  4556. });
  4557. }
  4558. get(text, isAdvanced, targetLanguage) {
  4559. const key = this.generateKey(text, isAdvanced, targetLanguage);
  4560. const data = this.cache.get(key);
  4561. if (!data) return null;
  4562. if (Date.now() - data.timestamp > this.expirationTime) {
  4563. this.cache.delete(key);
  4564. const index = this.accessOrder.indexOf(key);
  4565. this.accessOrder.splice(index, 1);
  4566. return null;
  4567. }
  4568. const index = this.accessOrder.indexOf(key);
  4569. this.accessOrder.splice(index, 1);
  4570. this.accessOrder.push(key);
  4571. return data.translation;
  4572. }
  4573. clear() {
  4574. this.cache.clear();
  4575. this.accessOrder = [];
  4576. }
  4577. optimizeStorage() {
  4578. if (this.cache.size > this.maxSize * 0.9) {
  4579. const itemsToKeep = Math.floor(this.maxSize * 0.7);
  4580. const sortedItems = [...this.accessOrder].slice(-itemsToKeep);
  4581. const tempCache = new Map();
  4582. sortedItems.forEach((key) => {
  4583. if (this.cache.has(key)) {
  4584. tempCache.set(key, this.cache.get(key));
  4585. }
  4586. });
  4587. this.cache = tempCache;
  4588. this.accessOrder = sortedItems;
  4589. }
  4590. }
  4591. async initDB() {
  4592. if (!window.indexedDB) {
  4593. console.warn("IndexedDB not supported");
  4594. return;
  4595. }
  4596. return new Promise((resolve, reject) => {
  4597. const request = indexedDB.open("translatorCache", 1);
  4598. request.onerror = () => reject(request.error);
  4599. request.onsuccess = () => resolve(request.result);
  4600. request.onupgradeneeded = (event) => {
  4601. const db = event.target.result;
  4602. if (!db.objectStoreNames.contains("translations")) {
  4603. db.createObjectStore("translations", { keyPath: "id" });
  4604. }
  4605. };
  4606. });
  4607. }
  4608. async saveToIndexedDB(key, value) {
  4609. const db = await this.initDB();
  4610. return new Promise((resolve, reject) => {
  4611. const transaction = db.transaction(["translations"], "readwrite");
  4612. const store = transaction.objectStore("translations");
  4613. const request = store.put({ id: key, value, timestamp: Date.now() });
  4614. request.onsuccess = () => resolve();
  4615. request.onerror = () => reject(request.error);
  4616. });
  4617. }
  4618. async loadFromIndexedDB(key) {
  4619. const db = await this.initDB();
  4620. return new Promise((resolve, reject) => {
  4621. const transaction = db.transaction(["translations"], "readonly");
  4622. const store = transaction.objectStore("translations");
  4623. const request = store.get(key);
  4624. request.onsuccess = () => resolve(request.result?.value);
  4625. request.onerror = () => reject(request.error);
  4626. });
  4627. }
  4628. }
  4629. class UIManager {
  4630. constructor(translator) {
  4631. if (!translator) {
  4632. throw new Error("Translator instance is required");
  4633. }
  4634. // Khởi tạo các thuộc tính cơ bản trước
  4635. this.translator = translator;
  4636. this.isTranslating = false;
  4637. this.translatingStatus = null;
  4638. this.ignoreNextSelectionChange = false;
  4639. this.touchCount = 0;
  4640. this.currentTranslateButton = null;
  4641. // Khởi tạo trạng thái tools
  4642. if (localStorage.getItem("translatorToolsEnabled") === null) {
  4643. localStorage.setItem("translatorToolsEnabled", "true");
  4644. }
  4645. // CSS tổng hợp cho settings
  4646. const themeMode = this.translator.userSettings.settings.theme;
  4647. const theme = CONFIG.THEME[themeMode];
  4648. const isDark = themeMode === "dark";
  4649. GM_addStyle(`
  4650. .translator-settings-container {
  4651. z-index: 2147483647 !important;
  4652. position: fixed !important;
  4653. background-color: ${theme.background} !important;
  4654. color: ${theme.text} !important;
  4655. padding: 20px !important;
  4656. border-radius: 12px !important;
  4657. box-shadow: 0 2px 10px rgba(0,0,0,0.3) !important;
  4658. width: auto !important;
  4659. min-width: 320px !important;
  4660. max-width: 90vw !important;
  4661. max-height: 90vh !important;
  4662. overflow-y: auto !important;
  4663. top: 50% !important;
  4664. left: 50% !important;
  4665. transform: translate(-50%, -50%) !important;
  4666. display: block !important;
  4667. visibility: visible !important;
  4668. opacity: 1 !important;
  4669. font-size: 14px !important;
  4670. line-height: 1.4 !important;
  4671. }
  4672. .translator-settings-container * {
  4673. font-family: Arial, sans-serif !important;
  4674. box-sizing: border-box !important;
  4675. }
  4676. .translator-settings-container input[type="checkbox"],
  4677. .translator-settings-container input[type="radio"] {
  4678. appearance: auto !important;
  4679. -webkit-appearance: auto !important;
  4680. -moz-appearance: auto !important;
  4681. position: relative !important;
  4682. width: 16px !important;
  4683. height: 16px !important;
  4684. margin: 3px 5px !important;
  4685. padding: 0 !important;
  4686. accent-color: #0000aa !important;
  4687. border: 1px solid ${theme.border} !important;
  4688. opacity: 1 !important;
  4689. visibility: visible !important;
  4690. cursor: pointer !important;
  4691. }
  4692. .radio-group {
  4693. display: flex !important;
  4694. gap: 15px !important;
  4695. align-items: center !important;
  4696. }
  4697. .radio-group label {
  4698. flex: 1 !important;
  4699. display: flex !important;
  4700. align-items: center !important;
  4701. justify-content: center !important;
  4702. padding: 5px !important;
  4703. gap: 5px !important;
  4704. }
  4705. .radio-group input[type="radio"] {
  4706. margin: 0 !important;
  4707. position: relative !important;
  4708. top: 0 !important;
  4709. }
  4710. .translator-settings-container input[type="radio"] {
  4711. border-radius: 50% !important;
  4712. }
  4713. .translator-settings-container input[type="checkbox"] {
  4714. display: flex !important;
  4715. position: relative !important;
  4716. margin: 5px 53% 5px 47% !important;
  4717. align-items: center !important;
  4718. justify-content: center !important;
  4719. }
  4720. .settings-grid input[type="text"],
  4721. .settings-grid input[type="number"],
  4722. .settings-grid select {
  4723. appearance: auto !important;
  4724. -webkit-appearance: auto !important;
  4725. -moz-appearance: auto !important;
  4726. background-color: ${isDark ? "#202020" : "#eeeeee"} !important;
  4727. color: ${theme.text} !important;
  4728. border: 1px solid ${theme.border} !important;
  4729. border-radius: 8px !important;
  4730. padding: 7px 10px !important;
  4731. margin: 5px !important;
  4732. font-size: 14px !important;
  4733. line-height: normal !important;
  4734. height: auto !important;
  4735. width: auto !important;
  4736. min-width: 100px !important;
  4737. display: inline-block !important;
  4738. visibility: visible !important;
  4739. opacity: 1 !important;
  4740. }
  4741. .settings-grid select {
  4742. padding-right: 20px !important;
  4743. }
  4744. .settings-grid label {
  4745. display: inline-flex !important;
  4746. align-items: center !important;
  4747. margin: 3px 10px !important;
  4748. color: ${theme.text} !important;
  4749. cursor: pointer !important;
  4750. user-select: none !important;
  4751. }
  4752. .settings-grid input:not([type="hidden"]),
  4753. .settings-grid select,
  4754. .settings-grid textarea {
  4755. display: inline-block !important;
  4756. opacity: 1 !important;
  4757. visibility: visible !important;
  4758. position: static !important;
  4759. }
  4760. .settings-grid input:disabled,
  4761. .settings-grid select:disabled {
  4762. opacity: 0.5 !important;
  4763. cursor: not-allowed !important;
  4764. }
  4765. .translator-settings-container input[type="checkbox"]:hover,
  4766. .translator-settings-container input[type="radio"]:hover {
  4767. border-color: ${theme.mode === "dark" ? "#777" : "#444"} !important;
  4768. }
  4769. .settings-grid input:focus,
  4770. .settings-grid select:focus {
  4771. outline: 2px solid rgba(74, 144, 226, 0.5) !important;
  4772. outline-offset: 1px !important;
  4773. }
  4774. .settings-grid input::before,
  4775. .settings-grid input::after {
  4776. content: none !important;
  4777. display: none !important;
  4778. }
  4779. .translator-settings-container button {
  4780. display: inline-flex !important;
  4781. align-items: center !important;
  4782. justify-content: center !important;
  4783. gap: 8px !important;
  4784. line-height: 1 !important;
  4785. }
  4786. .translator-settings-container .api-key-entry input[type="text"].gemini-key,
  4787. .translator-settings-container .api-key-entry input[type="text"].openai-key {
  4788. padding: 8px 10px !important;
  4789. margin: 0px 3px 3px 15px !important;
  4790. appearance: auto !important;
  4791. -webkit-appearance: auto !important;
  4792. -moz-appearance: auto !important;
  4793. font-size: 14px !important;
  4794. line-height: normal !important;
  4795. width: auto !important;
  4796. min-width: 100px !important;
  4797. display: inline-block !important;
  4798. visibility: visible !important;
  4799. opacity: 1 !important;
  4800. border: 1px solid ${theme.border} !important;
  4801. border-radius: 10px !important;
  4802. box-sizing: border-box !important;
  4803. font-family: Arial, sans-serif !important;
  4804. text-align: left !important;
  4805. vertical-align: middle !important;
  4806. background-color: ${isDark ? "#202020" : "#eeeeee"} !important;
  4807. color: ${theme.text} !important;
  4808. }
  4809. .translator-settings-container .api-key-entry input[type="text"].gemini-key:focus,
  4810. .translator-settings-container .api-key-entry input[type="text"].openai-key:focus {
  4811. outline: 3px solid rgba(74, 144, 226, 0.5) !important;
  4812. outline-offset: 1px !important;
  4813. box-shadow: none !important;
  4814. }
  4815. .translator-settings-container .api-key-entry {
  4816. display: flex !important;
  4817. gap: 10px !important;
  4818. align-items: center !important;
  4819. }
  4820. .remove-key {
  4821. display: inline-flex !important;
  4822. align-items: center !important;
  4823. justify-content: center !important;
  4824. width: 24px !important;
  4825. height: 24px !important;
  4826. padding: 0 !important;
  4827. line-height: 1 !important;
  4828. }
  4829. .translator-settings-container::-webkit-scrollbar {
  4830. width: 8px !important;
  4831. }
  4832. .translator-settings-container::-webkit-scrollbar-track {
  4833. background-color: ${theme.mode === "dark" ? "#222" : "#eeeeee"} !important;
  4834. border-radius: 8px !important;
  4835. }
  4836. .translator-settings-container::-webkit-scrollbar-thumb {
  4837. background-color: ${theme.mode === "dark" ? "#666" : "#888"} !important;
  4838. border-radius: 8px !important;
  4839. }
  4840. `);
  4841. GM_addStyle(`
  4842. .translator-tools-container {
  4843. position: fixed !important;
  4844. bottom: 40px;
  4845. right: 25px;
  4846. color: ${theme.text} !important;
  4847. border-radius: 10px !important;
  4848. z-index: 2147483647 !important;
  4849. display: block !important;
  4850. visibility: visible !important;
  4851. opacity: 1 !important;
  4852. }
  4853. .translator-tools-container * {
  4854. font-family: Arial, sans-serif !important;
  4855. box-sizing: border-box !important;
  4856. }
  4857. .translator-tools-button {
  4858. display: flex !important;
  4859. align-items: center !important;
  4860. gap: 8px !important;
  4861. padding: 12px 20px !important;
  4862. border: none !important;
  4863. border-radius: 9px !important;
  4864. background-color: rgba(74,144,226,0.4) !important;
  4865. color: white !important;
  4866. cursor: pointer !important;
  4867. transition: all 0.3s ease !important;
  4868. box-shadow: 0 2px 10px rgba(0,0,0,0.2) !important;
  4869. font-size: 15px !important;
  4870. line-height: 1 !important;
  4871. visibility: visible !important;
  4872. opacity: 1 !important;
  4873. }
  4874. .translator-tools-dropdown {
  4875. display: none;
  4876. position: absolute !important;
  4877. bottom: 100% !important;
  4878. right: 0 !important;
  4879. margin-bottom: 10px !important;
  4880. background-color: ${theme.background} !important;
  4881. color: ${theme.text} !important;
  4882. border-radius: 10px !important;
  4883. box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important;
  4884. padding: 15px 12px 9px 12px !important;
  4885. min-width: 205px !important;
  4886. z-index: 2147483647 !important;
  4887. visibility: visible !important;
  4888. opacity: 1 !important;
  4889. }
  4890. .translator-tools-item {
  4891. display: flex !important;
  4892. align-items: center !important;
  4893. gap: 10px !important;
  4894. padding: 10px !important;
  4895. margin-bottom: 5px !important;
  4896. cursor: pointer !important;
  4897. transition: all 0.2s ease !important;
  4898. border-radius: 10px !important;
  4899. background-color: ${theme.backgroundShadow} !important;
  4900. color: ${theme.text} !important;
  4901. border: 1px solid ${theme.border} !important;
  4902. visibility: visible !important;
  4903. opacity: 1 !important;
  4904. }
  4905. .item-icon, .item-text {
  4906. font-family: Arial, sans-serif !important;
  4907. visibility: visible !important;
  4908. opacity: 1 !important;
  4909. }
  4910. .item-icon {
  4911. font-size: 18px !important;
  4912. }
  4913. .item-text {
  4914. font-size: 14px !important;
  4915. }
  4916. .translator-tools-item:hover {
  4917. background-color: ${theme.button.translate.background} !important;
  4918. color: ${theme.button.translate.text} !important;
  4919. }
  4920. .translator-tools-item:active {
  4921. transform: scale(0.98) !important;
  4922. }
  4923. `);
  4924. GM_addStyle(`
  4925. .settings-label,
  4926. .settings-section-title,
  4927. .shortcut-prefix,
  4928. .item-text,
  4929. .translator-settings-container label {
  4930. color: ${theme.text} !important;
  4931. margin: 2px 10px !important;
  4932. }
  4933. .translator-settings-container input[type="text"],
  4934. .translator-settings-container input[type="number"],
  4935. .translator-settings-container select {
  4936. background-color: ${isDark ? "#202020" : "#eeeeee"} !important;
  4937. color: ${theme.text} !important;
  4938. }
  4939. /* Đảm bảo input không ghi đè lên label */
  4940. .translator-settings-container input {
  4941. color: inherit !important;
  4942. }
  4943. `);
  4944. GM_addStyle(`
  4945. @keyframes spin {
  4946. 0% { transform: rotate(0deg); }
  4947. 100% { transform: rotate(360deg); }
  4948. }
  4949. .processing-spinner {
  4950. width: 30px;
  4951. height: 30px;
  4952. color: white;
  4953. border: 3px solid rgba(255,255,255,0.3);
  4954. border-radius: 50%;
  4955. border-top-color: white;
  4956. animation: spin 1s ease-in-out infinite;
  4957. margin: 0 auto 10px auto;
  4958. }
  4959. .processing-message {
  4960. margin-bottom: 10px;
  4961. font-size: 14px;
  4962. }
  4963. .processing-progress {
  4964. font-size: 12px;
  4965. opacity: 0.8;
  4966. }
  4967. .translation-div p {
  4968. margin: 5px 0;
  4969. }
  4970. .translation-div strong {
  4971. font-weight: bold;
  4972. }
  4973. `);
  4974. // Khởi tạo các managers
  4975. this.mobileOptimizer = new MobileOptimizer(this);
  4976. this.ss = new UserSettings(translator);
  4977. this.ocr = new OCRManager(translator);
  4978. this.media = new MediaManager(translator);
  4979. this.page = new PageTranslator(translator);
  4980. this.input = new InputTranslator(translator);
  4981. // Bind các methods
  4982. this.handleSettingsShortcut = this.handleSettingsShortcut.bind(this);
  4983. this.handleTranslationShortcuts =
  4984. this.handleTranslationShortcuts.bind(this);
  4985. this.handleTranslateButtonClick =
  4986. this.handleTranslateButtonClick.bind(this);
  4987. this.setupClickHandlers = this.setupClickHandlers.bind(this);
  4988. this.setupSelectionHandlers = this.setupSelectionHandlers.bind(this);
  4989. this.showTranslatingStatus = this.showTranslatingStatus.bind(this);
  4990. this.removeTranslatingStatus = this.removeTranslatingStatus.bind(this);
  4991. this.resetState = this.resetState.bind(this);
  4992. // Gán các listeners
  4993. this.settingsShortcutListener = this.handleSettingsShortcut;
  4994. this.translationShortcutListener = this.handleTranslationShortcuts;
  4995. // Khởi tạo các trạng thái UI
  4996. this.translationButtonEnabled = true;
  4997. this.translationTapEnabled = true;
  4998. this.mediaElement = null;
  4999. this.container = null;
  5000. // Setup event listeners sau khi mọi thứ đã được khởi tạo
  5001. this.setupEventListeners();
  5002. // Setup page translation
  5003. if (document.readyState === "complete") {
  5004. if (
  5005. this.translator.userSettings.settings.pageTranslation.autoTranslate
  5006. ) {
  5007. this.page.checkAndTranslate();
  5008. }
  5009. if (
  5010. this.translator.userSettings.settings.pageTranslation
  5011. .showInitialButton
  5012. ) {
  5013. this.setupQuickTranslateButton();
  5014. }
  5015. } else {
  5016. window.addEventListener("load", () => {
  5017. if (
  5018. this.translator.userSettings.settings.pageTranslation.autoTranslate
  5019. ) {
  5020. this.page.checkAndTranslate();
  5021. }
  5022. if (
  5023. this.translator.userSettings.settings.pageTranslation
  5024. .showInitialButton
  5025. ) {
  5026. this.setupQuickTranslateButton();
  5027. }
  5028. });
  5029. }
  5030. document.addEventListener("DOMContentLoaded", () => {
  5031. const isEnabled =
  5032. localStorage.getItem("translatorToolsEnabled") === "true";
  5033. if (isEnabled) {
  5034. this.setupTranslatorTools();
  5035. }
  5036. });
  5037. setTimeout(() => {
  5038. if (!document.querySelector(".translator-tools-container")) {
  5039. const isEnabled =
  5040. localStorage.getItem("translatorToolsEnabled") === "true";
  5041. if (isEnabled) {
  5042. this.setupTranslatorTools();
  5043. }
  5044. }
  5045. }, 1000);
  5046. this.debouncedCreateButton = debounce((selection, x, y) => {
  5047. this.createTranslateButton(selection, x, y);
  5048. }, 100);
  5049. }
  5050. createCloseButton() {
  5051. const button = document.createElement("span");
  5052. button.textContent = "x";
  5053. Object.assign(button.style, {
  5054. position: "absolute",
  5055. top: "0px" /* Đẩy lên trên một chút */,
  5056. right: "0px" /* Đẩy sang phải một chút */,
  5057. cursor: "pointer",
  5058. color: "black",
  5059. fontSize: "14px",
  5060. fontWeight: "bold",
  5061. padding: "4px 8px" /* Tăng kích thước */,
  5062. lineHeight: "14px",
  5063. });
  5064. button.onclick = () => button.parentElement.remove();
  5065. return button;
  5066. }
  5067. showTranslationBelow(translatedText, targetElement, text) {
  5068. const selection = window.getSelection();
  5069. const lastSelectedNode = selection.focusNode;
  5070. let lastSelectedParagraph = lastSelectedNode.parentElement;
  5071. while (lastSelectedParagraph && lastSelectedParagraph.tagName !== "P") {
  5072. lastSelectedParagraph = lastSelectedParagraph.parentElement;
  5073. }
  5074. if (!lastSelectedParagraph) {
  5075. lastSelectedParagraph = targetElement;
  5076. }
  5077. if (
  5078. lastSelectedParagraph.nextElementSibling?.classList.contains(
  5079. "translation-div"
  5080. )
  5081. ) {
  5082. return;
  5083. }
  5084. const settings = this.translator.userSettings.settings.displayOptions;
  5085. const mode = settings.translationMode;
  5086. const showSource = settings.languageLearning.showSource;
  5087. let formattedTranslation = "";
  5088. if (mode === "translation_only") {
  5089. formattedTranslation = translatedText;
  5090. } else if (mode === "parallel") {
  5091. formattedTranslation = `<div style="margin-bottom: 8px">Gc: ${text}</div>
  5092. <div>Dch: ${translatedText.split("<|>")[2] || translatedText}</div>`;
  5093. } else if (mode === "language_learning") {
  5094. let sourceHTML = "";
  5095. if (showSource) {
  5096. sourceHTML = `<div style="margin-bottom: 8px">[Gc]: ${text}</div>`;
  5097. }
  5098. formattedTranslation = `${sourceHTML}
  5099. <div>[Pinyin]: ${translatedText.split("<|>")[1] || ""}</div>
  5100. <div>[Dch]: ${translatedText.split("<|>")[2] || translatedText}</div>`;
  5101. }
  5102. const translationDiv = document.createElement("div");
  5103. translationDiv.classList.add("translation-div");
  5104. Object.assign(translationDiv.style, {
  5105. ...CONFIG.STYLES.translation,
  5106. fontSize: settings.fontSize,
  5107. });
  5108. translationDiv.innerHTML = formattedTranslation;
  5109. const themeMode = this.translator.userSettings.settings.theme;
  5110. const theme = CONFIG.THEME[themeMode];
  5111. translationDiv.appendChild(this.createCloseButton());
  5112. lastSelectedParagraph.parentNode.appendChild(translationDiv);
  5113. translationDiv.style.cssText = `
  5114. display: block; /* Giữ cho phần dịch không bị kéo dài hết chiều ngang */
  5115. max-width: fit-content; /* Giới hạn chiều rộng */
  5116. width: auto; /* Để nó co giãn theo nội dung */
  5117. min-width: 150px;
  5118. color: ${theme.text} !important;
  5119. background-color: ${theme.background} !important;
  5120. padding: 10px 20px 10px 10px;
  5121. margin-top: 10px;
  5122. border-radius: 8px;
  5123. position: relative;
  5124. z-index: 2147483647 !important;
  5125. border: 1px solid ${theme.border} !important;
  5126. white-space: normal; /* Cho phép xuống dòng nếu quá dài */
  5127. overflow-wrap: break-word; /* Ngắt từ nếu quá dài */
  5128. `;
  5129. }
  5130. displayPopup(
  5131. translatedText,
  5132. originalText,
  5133. title = "Bản dịch",
  5134. pinyin = ""
  5135. ) {
  5136. this.removeTranslateButton();
  5137. const themeMode = this.translator.userSettings.settings.theme;
  5138. const theme = CONFIG.THEME[themeMode];
  5139. const isDark = themeMode === "dark";
  5140. const displayOptions =
  5141. this.translator.userSettings.settings.displayOptions;
  5142. const popup = document.createElement("div");
  5143. popup.classList.add("draggable");
  5144. const popupStyle = {
  5145. ...CONFIG.STYLES.popup,
  5146. backgroundColor: theme.background,
  5147. borderColor: theme.border,
  5148. color: theme.text,
  5149. minWidth: displayOptions.minPopupWidth,
  5150. maxWidth: displayOptions.maxPopupWidth,
  5151. fontSize: displayOptions.fontSize,
  5152. padding: "0",
  5153. overflow: "hidden",
  5154. display: "flex",
  5155. flexDirection: "column",
  5156. };
  5157. Object.assign(popup.style, popupStyle);
  5158. const dragHandle = document.createElement("div");
  5159. Object.assign(dragHandle.style, {
  5160. ...CONFIG.STYLES.dragHandle,
  5161. backgroundColor: "#2c3e50",
  5162. borderColor: "transparent",
  5163. color: "#ffffff",
  5164. padding: "12px 15px",
  5165. borderTopLeftRadius: "15px",
  5166. borderTopRightRadius: "15px",
  5167. boxShadow: "0 1px 3px rgba(0,0,0,0.12)",
  5168. });
  5169. const titleSpan = document.createElement("span");
  5170. titleSpan.textContent = title;
  5171. Object.assign(titleSpan.style, {
  5172. fontWeight: "bold",
  5173. color: "#ffffff",
  5174. fontSize: "15px",
  5175. });
  5176. const closeButton = document.createElement("span");
  5177. closeButton.innerHTML = "×";
  5178. Object.assign(closeButton.style, {
  5179. cursor: "pointer",
  5180. fontSize: "22px",
  5181. color: "#ffffff",
  5182. padding: "0 10px",
  5183. opacity: "0.8",
  5184. transition: "all 0.2s ease",
  5185. fontWeight: "bold",
  5186. display: "flex",
  5187. alignItems: "center",
  5188. justifyContent: "center",
  5189. width: "30px",
  5190. height: "30px",
  5191. borderRadius: "50%",
  5192. });
  5193. closeButton.onmouseover = () => {
  5194. Object.assign(closeButton.style, {
  5195. opacity: "1",
  5196. backgroundColor: "#ff4444",
  5197. });
  5198. };
  5199. closeButton.onmouseout = () => {
  5200. Object.assign(closeButton.style, {
  5201. opacity: "0.8",
  5202. backgroundColor: "transparent",
  5203. });
  5204. };
  5205. closeButton.onclick = () => popup.remove();
  5206. dragHandle.appendChild(titleSpan);
  5207. dragHandle.appendChild(closeButton);
  5208. const contentContainer = document.createElement("div");
  5209. Object.assign(contentContainer.style, {
  5210. padding: "15px 20px",
  5211. maxHeight: "70vh",
  5212. overflowY: "auto",
  5213. overflowX: "hidden",
  5214. });
  5215. const scrollbarStyle = document.createElement("style");
  5216. scrollbarStyle.textContent = `
  5217. .translator-content::-webkit-scrollbar {
  5218. width: 8px;
  5219. }
  5220. .translator-content::-webkit-scrollbar-track {
  5221. background-color: ${isDark ? "#202020" : "#eeeeee"};
  5222. border-radius: 8px;
  5223. }
  5224. .translator-content::-webkit-scrollbar-thumb {
  5225. background-color: ${isDark ? "#666" : "#888"};
  5226. border-radius: 8px;
  5227. }
  5228. .translator-content::-webkit-scrollbar-thumb:hover {
  5229. background-color: ${isDark ? "#888" : "#555"};
  5230. }
  5231. `;
  5232. document.head.appendChild(scrollbarStyle);
  5233. contentContainer.classList.add("translator-content");
  5234. const cleanedText = translatedText.replace(/(\*\*)(.*?)\1/g, `<b style="color: ${theme.text};">$2</b>`);
  5235. const textContainer = document.createElement("div");
  5236. Object.assign(textContainer.style, {
  5237. display: "flex",
  5238. flexDirection: "column",
  5239. zIndex: "2147483647 !important",
  5240. gap: "15px"
  5241. });
  5242. if (
  5243. displayOptions.translationMode == "parallel" &&
  5244. originalText
  5245. ) {
  5246. const originalContainer = document.createElement("div");
  5247. Object.assign(originalContainer.style, {
  5248. color: theme.text,
  5249. padding: "10px 15px",
  5250. backgroundColor: `${theme.backgroundShadow}`,
  5251. borderRadius: "8px",
  5252. border: `1px solid ${theme.border}`,
  5253. wordBreak: "break-word",
  5254. zIndex: "2147483647 !important",
  5255. });
  5256. originalContainer.innerHTML = `
  5257. <div style="font-weight: 500; margin-bottom: 5px; color: ${theme.title};">Bn gc:</div>
  5258. <div style="line-height: 1.5; color: ${theme.text};">&nbsp;&nbsp;&nbsp;&nbsp; ${originalText}</div>
  5259. `;
  5260. textContainer.appendChild(originalContainer);
  5261. }
  5262. if (
  5263. displayOptions.translationMode == "language_learning" && displayOptions.languageLearning.showSource === true &&
  5264. originalText
  5265. ) {
  5266. const originalContainer = document.createElement("div");
  5267. Object.assign(originalContainer.style, {
  5268. color: theme.text,
  5269. padding: "10px 15px",
  5270. backgroundColor: `${theme.backgroundShadow
  5271. }`,
  5272. borderRadius: "8px",
  5273. border: `1px solid ${theme.border}`,
  5274. wordBreak: "break-word",
  5275. zIndex: "2147483647 !important",
  5276. });
  5277. originalContainer.innerHTML = `
  5278. <div style="font-weight: 500; margin-bottom: 5px; color: ${theme.title};">Bn gc:</div>
  5279. <div style="line-height: 1.5; color: ${theme.text};">&nbsp;&nbsp;&nbsp;&nbsp; ${originalText}</div>
  5280. `;
  5281. textContainer.appendChild(originalContainer);
  5282. }
  5283. if (
  5284. displayOptions.translationMode == "language_learning" &&
  5285. pinyin
  5286. ) {
  5287. const pinyinContainer = document.createElement("div");
  5288. Object.assign(pinyinContainer.style, {
  5289. color: theme.text,
  5290. padding: "10px 15px",
  5291. backgroundColor: `${theme.backgroundShadow
  5292. }`,
  5293. borderRadius: "8px",
  5294. border: `1px solid ${theme.border}`,
  5295. wordBreak: "break-word",
  5296. zIndex: "2147483647 !important",
  5297. });
  5298. pinyinContainer.innerHTML = `
  5299. <div style="font-weight: 500; margin-bottom: 5px; color: ${theme.title};">Pinyin:</div>
  5300. <div style="line-height: 1.5; color: ${theme.text};">&nbsp;&nbsp;&nbsp;&nbsp; ${pinyin}</div>
  5301. `;
  5302. textContainer.appendChild(pinyinContainer);
  5303. }
  5304. const translationContainer = document.createElement("div");
  5305. Object.assign(translationContainer.style, {
  5306. color: theme.text,
  5307. padding: "10px 15px",
  5308. backgroundColor: `${theme.backgroundShadow}`,
  5309. borderRadius: "8px",
  5310. border: `1px solid ${theme.border}`,
  5311. wordBreak: "break-word",
  5312. zIndex: "2147483647 !important",
  5313. });
  5314. translationContainer.innerHTML = `
  5315. <div style="font-weight: 500; margin-bottom: 5px; color: ${theme.title
  5316. };">Bn dch:</div>
  5317. <div style="line-height: 1.5; color: ${theme.text};">${this.formatTranslation(cleanedText, theme)}</div>
  5318. `;
  5319. textContainer.appendChild(translationContainer);
  5320. contentContainer.appendChild(textContainer);
  5321. popup.appendChild(dragHandle);
  5322. popup.appendChild(contentContainer);
  5323. Object.assign(popup.style, {
  5324. ...popupStyle,
  5325. maxHeight: "85vh",
  5326. display: "flex",
  5327. flexDirection: "column",
  5328. });
  5329. this.makeDraggable(popup, dragHandle);
  5330. document.body.appendChild(popup);
  5331. }
  5332. makeDraggable(element, handle) {
  5333. let pos1 = 0,
  5334. pos2 = 0,
  5335. pos3 = 0,
  5336. pos4 = 0;
  5337. handle.onmousedown = dragMouseDown;
  5338. function dragMouseDown(e) {
  5339. e.preventDefault();
  5340. pos3 = e.clientX;
  5341. pos4 = e.clientY;
  5342. document.onmouseup = closeDragElement;
  5343. document.onmousemove = elementDrag;
  5344. }
  5345. function elementDrag(e) {
  5346. e.preventDefault();
  5347. pos1 = pos3 - e.clientX;
  5348. pos2 = pos4 - e.clientY;
  5349. pos3 = e.clientX;
  5350. pos4 = e.clientY;
  5351. element.style.top = element.offsetTop - pos2 + "px";
  5352. element.style.left = element.offsetLeft - pos1 + "px";
  5353. }
  5354. function closeDragElement() {
  5355. document.onmouseup = null;
  5356. document.onmousemove = null;
  5357. }
  5358. }
  5359. formatTranslation(text, theme) {
  5360. return text
  5361. .split("<br>")
  5362. .map((line) => {
  5363. if (line.startsWith(`<b style="color: ${theme.text};">KEYWORD</b>:`)) {
  5364. return `<h4 style="margin-bottom: 5px; color: ${theme.text};">${line}</h4>`;
  5365. }
  5366. return `<p style="margin-left: 20px; margin-bottom: 10px; white-space: pre-wrap; word-wrap: break-word; text-align: justify; color: ${theme.text};">${line}</p>`;
  5367. })
  5368. .join("");
  5369. }
  5370. setupSelectionHandlers() {
  5371. if (this.isTranslating) return;
  5372. if (this.ignoreNextSelectionChange || this.isTranslating) {
  5373. this.ignoreNextSelectionChange = false;
  5374. return;
  5375. }
  5376. if (!this.translationButtonEnabled) return;
  5377. document.addEventListener('mousedown', (e) => {
  5378. if (!e.target.classList.contains('translator-button')) {
  5379. this.isSelecting = true;
  5380. this.removeTranslateButton();
  5381. }
  5382. });
  5383. document.addEventListener('mousemove', (e) => {
  5384. if (this.isSelecting) {
  5385. const selection = window.getSelection();
  5386. const selectedText = selection.toString().trim();
  5387. if (selectedText) {
  5388. this.removeTranslateButton();
  5389. this.debouncedCreateButton(selection, e.clientX, e.clientY);
  5390. }
  5391. }
  5392. });
  5393. document.addEventListener('mouseup', (e) => {
  5394. if (!e.target.classList.contains('translator-button')) {
  5395. const selection = window.getSelection();
  5396. const selectedText = selection.toString().trim();
  5397. if (selectedText) {
  5398. this.removeTranslateButton();
  5399. this.createTranslateButton(selection, e.clientX, e.clientY);
  5400. }
  5401. }
  5402. this.isSelecting = false;
  5403. });
  5404. document.addEventListener('touchend', (e) => {
  5405. if (!e.target.classList.contains('translator-button')) {
  5406. const selection = window.getSelection();
  5407. const selectedText = selection.toString().trim();
  5408. if (selectedText && e.changedTouches?.[0]) {
  5409. const touch = e.changedTouches[0];
  5410. this.createTranslateButton(selection, touch.clientX, touch.clientY);
  5411. }
  5412. }
  5413. });
  5414. }
  5415. createTranslateButton(selection, x, y) {
  5416. this.removeTranslateButton();
  5417. const button = document.createElement('button');
  5418. button.className = 'translator-button';
  5419. button.textContent = 'Dịch';
  5420. const viewportWidth = window.innerWidth;
  5421. const viewportHeight = window.innerHeight;
  5422. const buttonWidth = 60;
  5423. const buttonHeight = 30;
  5424. const padding = 10;
  5425. let left = Math.min(x + padding, viewportWidth - buttonWidth - padding);
  5426. let top = Math.min(y + 30, viewportHeight - buttonHeight - 30);
  5427. left = Math.max(padding, left);
  5428. top = Math.max(30, top);
  5429. const themeMode = this.translator.userSettings.settings.theme;
  5430. const theme = CONFIG.THEME[themeMode];
  5431. Object.assign(button.style, {
  5432. ...CONFIG.STYLES.button,
  5433. backgroundColor: theme.button.translate.background,
  5434. color: theme.button.translate.text,
  5435. position: 'fixed',
  5436. left: `${left}px`,
  5437. top: `${top}px`,
  5438. zIndex: '2147483647',
  5439. userSelect: 'none'
  5440. });
  5441. document.body.appendChild(button);
  5442. this.currentTranslateButton = button;
  5443. this.setupClickHandlers(selection);
  5444. }
  5445. handleTranslateButtonClick = async (selection, translateType) => {
  5446. try {
  5447. const selectedText = selection.toString().trim();
  5448. if (!selectedText) {
  5449. console.log("No text selected");
  5450. return;
  5451. }
  5452. const targetElement = selection.anchorNode?.parentElement;
  5453. if (!targetElement) {
  5454. console.log("No target element found");
  5455. return;
  5456. }
  5457. this.removeTranslateButton();
  5458. this.showTranslatingStatus();
  5459. console.log("Starting translation with type:", translateType);
  5460. if (!this.translator) {
  5461. throw new Error("Translator instance not found");
  5462. }
  5463. switch (translateType) {
  5464. case "quick":
  5465. await this.translator.translate(selectedText, targetElement);
  5466. break;
  5467. case "popup":
  5468. await this.translator.translate(
  5469. selectedText,
  5470. targetElement,
  5471. false,
  5472. true
  5473. );
  5474. break;
  5475. case "advanced":
  5476. await this.translator.translate(selectedText, targetElement, true);
  5477. break;
  5478. default:
  5479. console.log("Unknown translation type:", translateType);
  5480. }
  5481. } catch (error) {
  5482. console.error("Translation error:", error);
  5483. } finally {
  5484. if (this.isDouble) {
  5485. const newSelection = window.getSelection();
  5486. if (newSelection.toString().trim()) {
  5487. this.resetState();
  5488. this.setupSelectionHandlers();
  5489. }
  5490. } else {
  5491. this.resetState();
  5492. return;
  5493. }
  5494. }
  5495. };
  5496. debug(message) {
  5497. console.log(`[UIManager] ${message}`);
  5498. }
  5499. showTranslatingStatus() {
  5500. this.debug("Showing translating status");
  5501. if (!document.getElementById("translator-animation-style")) {
  5502. const style = document.createElement("style");
  5503. style.id = "translator-animation-style";
  5504. style.textContent = `
  5505. @keyframes spin {
  5506. 0% { transform: rotate(0deg); }
  5507. 100% { transform: rotate(360deg); }
  5508. }
  5509. .center-translate-status {
  5510. position: fixed;
  5511. top: 50%;
  5512. left: 50%;
  5513. transform: translate(-50%, -50%);
  5514. background-color: rgba(0, 0, 0, 0.8);
  5515. color: white;
  5516. padding: 15px 25px;
  5517. border-radius: 8px;
  5518. z-index: 2147483647 !important;
  5519. display: flex;
  5520. align-items: center;
  5521. gap: 12px;
  5522. font-family: Arial, sans-serif;
  5523. font-size: 14px;
  5524. box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  5525. }
  5526. .spinner {
  5527. display: inline-block;
  5528. width: 20px;
  5529. height: 20px;
  5530. border: 3px solid rgba(255,255,255,0.3);
  5531. border-radius: 50%;
  5532. border-top-color: #ddd;
  5533. animation: spin 1s ease-in-out infinite;
  5534. }
  5535. `;
  5536. document.head.appendChild(style);
  5537. }
  5538. this.removeTranslatingStatus();
  5539. const status = document.createElement("div");
  5540. status.className = "center-translate-status";
  5541. status.innerHTML = `
  5542. <div class="spinner" style="color: white"></div>
  5543. <span style="color: white"ang dch...</span>
  5544. `;
  5545. document.body.appendChild(status);
  5546. this.translatingStatus = status;
  5547. this.debug("Translation status shown");
  5548. }
  5549. setupClickHandlers(selection) {
  5550. this.pressTimer = null;
  5551. this.isLongPress = false;
  5552. this.isDown = false;
  5553. this.isDouble = false;
  5554. this.lastTime = 0;
  5555. this.count = 0;
  5556. this.timer = 0;
  5557. const handleStart = (e) => {
  5558. e.preventDefault();
  5559. e.stopPropagation();
  5560. this.ignoreNextSelectionChange = true;
  5561. this.isDown = true;
  5562. this.isLongPress = false;
  5563. const currentTime = Date.now();
  5564. if (currentTime - this.lastTime < 400) {
  5565. this.count++;
  5566. clearTimeout(this.pressTimer);
  5567. clearTimeout(this.timer);
  5568. } else {
  5569. this.count = 1;
  5570. }
  5571. this.lastTime = currentTime;
  5572. this.pressTimer = setTimeout(() => {
  5573. if (!this.isDown) return;
  5574. this.isLongPress = true;
  5575. this.count = 0;
  5576. const holdType =
  5577. this.translator.userSettings.settings.clickOptions.hold
  5578. .translateType;
  5579. this.handleTranslateButtonClick(selection, holdType);
  5580. }, 500);
  5581. };
  5582. const handleEnd = (e) => {
  5583. e.preventDefault();
  5584. e.stopPropagation();
  5585. if (!this.isDown) return;
  5586. clearTimeout(this.pressTimer);
  5587. if (this.isLongPress) return;
  5588. if (this.count === 1) {
  5589. clearTimeout(this.timer);
  5590. this.timer = setTimeout(() => {
  5591. if (this.count !== 1) return;
  5592. const singleClickType =
  5593. this.translator.userSettings.settings.clickOptions.singleClick
  5594. .translateType;
  5595. this.handleTranslateButtonClick(selection, singleClickType);
  5596. }, 400);
  5597. } else if (this.count >= 2) {
  5598. this.isDouble = true;
  5599. const doubleClickType =
  5600. this.translator.userSettings.settings.clickOptions.doubleClick
  5601. .translateType;
  5602. this.handleTranslateButtonClick(selection, doubleClickType);
  5603. }
  5604. this.isDown = false;
  5605. };
  5606. // PC Events
  5607. this.currentTranslateButton.addEventListener("mousedown", handleStart);
  5608. this.currentTranslateButton.addEventListener("mouseup", handleEnd);
  5609. this.currentTranslateButton.addEventListener("mouseleave", () => {
  5610. if (this.translateType) {
  5611. this.resetState();
  5612. }
  5613. });
  5614. // Mobile Events
  5615. this.currentTranslateButton.addEventListener("touchstart", handleStart);
  5616. this.currentTranslateButton.addEventListener("touchend", handleEnd);
  5617. this.currentTranslateButton.addEventListener("touchcancel", () => {
  5618. if (this.translateType) {
  5619. this.resetState();
  5620. }
  5621. });
  5622. }
  5623. setupDocumentTapHandler() {
  5624. let touchCount = 0;
  5625. let touchTimer = null;
  5626. let isProcessingTouch = false;
  5627. const handleTouchStart = async (e) => {
  5628. if (this.isTranslating) return;
  5629. const touchOptions = this.translator.userSettings.settings.touchOptions;
  5630. if (!touchOptions?.enabled) return;
  5631. const target = e.target;
  5632. if (
  5633. target.closest(".translation-div") ||
  5634. target.closest(".draggable")
  5635. ) {
  5636. return;
  5637. }
  5638. if (touchTimer) {
  5639. clearTimeout(touchTimer);
  5640. }
  5641. touchCount = e.touches.length;
  5642. touchTimer = setTimeout(async () => {
  5643. if (isProcessingTouch) return;
  5644. switch (touchCount) {
  5645. case 2:
  5646. const twoFingersType = touchOptions.twoFingers?.translateType;
  5647. if (twoFingersType) {
  5648. const selection = window.getSelection();
  5649. const selectedText = selection?.toString().trim();
  5650. if (selectedText) {
  5651. e.preventDefault();
  5652. await this.handleTranslateButtonClick(
  5653. selection,
  5654. twoFingersType
  5655. );
  5656. }
  5657. }
  5658. break;
  5659. case 3:
  5660. const threeFingersType = touchOptions.threeFingers?.translateType;
  5661. if (threeFingersType) {
  5662. const selection = window.getSelection();
  5663. const selectedText = selection?.toString().trim();
  5664. if (selectedText) {
  5665. e.preventDefault();
  5666. await this.handleTranslateButtonClick(
  5667. selection,
  5668. threeFingersType
  5669. );
  5670. }
  5671. }
  5672. break;
  5673. case 4:
  5674. e.preventDefault();
  5675. const settingsUI =
  5676. this.translator.userSettings.createSettingsUI();
  5677. document.body.appendChild(settingsUI);
  5678. break;
  5679. case 5:
  5680. e.preventDefault();
  5681. isProcessingTouch = true;
  5682. this.toggleTranslatorTools();
  5683. setTimeout(() => {
  5684. isProcessingTouch = false;
  5685. }, 350);
  5686. break;
  5687. }
  5688. touchCount = 0;
  5689. touchTimer = null;
  5690. }, touchOptions.sensitivity || 100);
  5691. };
  5692. const handleTouch = () => {
  5693. if (touchTimer) {
  5694. clearTimeout(touchTimer);
  5695. touchTimer = null;
  5696. }
  5697. touchCount = 0;
  5698. };
  5699. document.addEventListener("touchstart", handleTouchStart.bind(this), {
  5700. passive: false,
  5701. });
  5702. document.addEventListener("touchend", handleTouch.bind(this));
  5703. document.addEventListener("touchcancel", handleTouch.bind(this));
  5704. }
  5705. toggleTranslatorTools() {
  5706. if (this.isTogglingTools) return;
  5707. this.isTogglingTools = true;
  5708. try {
  5709. const currentState =
  5710. localStorage.getItem("translatorToolsEnabled") === "true";
  5711. const newState = !currentState;
  5712. localStorage.setItem("translatorToolsEnabled", newState.toString());
  5713. const settings = this.translator.userSettings.settings;
  5714. settings.showTranslatorTools.enabled = newState;
  5715. this.translator.userSettings.saveSettings();
  5716. this.removeToolsContainer();
  5717. this.resetState();
  5718. const overlays = document.querySelectorAll(".translator-overlay");
  5719. overlays.forEach((overlay) => overlay.remove());
  5720. if (newState) {
  5721. this.setupTranslatorTools();
  5722. }
  5723. this.showNotification(
  5724. newState ? "Đã bật Translator Tools" : "Đã tắt Translator Tools"
  5725. );
  5726. } finally {
  5727. setTimeout(() => {
  5728. this.isTogglingTools = false;
  5729. }, 350);
  5730. }
  5731. }
  5732. removeToolsContainer() {
  5733. const container = document.querySelector(".translator-tools-container");
  5734. if (container) {
  5735. const ocrInput = container.querySelector("#translator-ocr-input");
  5736. const mediaInput = container.querySelector("#translator-media-input");
  5737. if (ocrInput)
  5738. ocrInput.removeEventListener("change", this.handleOCRInput);
  5739. if (mediaInput)
  5740. mediaInput.removeEventListener("change", this.handleMediaInput);
  5741. const mainButton = container.querySelector(".translator-tools-button");
  5742. if (mainButton) {
  5743. mainButton.removeEventListener("click", this.handleButtonClick);
  5744. }
  5745. const menuItems = container.querySelectorAll(".translator-tools-item");
  5746. menuItems.forEach((item) => {
  5747. if (item.handler) {
  5748. item.removeEventListener("click", item.handler);
  5749. }
  5750. });
  5751. container.remove();
  5752. }
  5753. }
  5754. async handlePageTranslation() {
  5755. const settings = this.translator.userSettings.settings;
  5756. if (!settings.pageTranslation?.enabled && !settings.shortcuts?.enabled) {
  5757. this.showNotification("Tính năng dịch trang đang bị tắt", "warning");
  5758. return;
  5759. }
  5760. try {
  5761. this.showTranslatingStatus();
  5762. const result = await this.page.translatePage();
  5763. if (result.success) {
  5764. const toolsContainer = document.querySelector(
  5765. ".translator-tools-container"
  5766. );
  5767. if (toolsContainer) {
  5768. const menuItem = toolsContainer.querySelector(
  5769. '[data-type="pageTranslate"]'
  5770. );
  5771. if (menuItem) {
  5772. const itemText = menuItem.querySelector(".item-text");
  5773. if (itemText) {
  5774. itemText.textContent = this.page.isTranslated
  5775. ? "Bản gốc"
  5776. : "Dịch trang";
  5777. }
  5778. }
  5779. }
  5780. const floatingButton = document.querySelector(
  5781. ".page-translate-button"
  5782. );
  5783. if (floatingButton) {
  5784. floatingButton.innerHTML = this.page.isTranslated
  5785. ? "📄 Bản gốc"
  5786. : "📄 Dịch trang";
  5787. }
  5788. this.showNotification(result.message, "success");
  5789. } else {
  5790. this.showNotification(result.message, "warning");
  5791. }
  5792. } catch (error) {
  5793. console.error("Page translation error:", error);
  5794. this.showNotification(error.message, "error");
  5795. } finally {
  5796. this.removeTranslatingStatus();
  5797. }
  5798. }
  5799. setupQuickTranslateButton() {
  5800. const settings = this.translator.userSettings.settings;
  5801. if (!settings.pageTranslation?.enabled && !settings.shortcuts?.enabled) {
  5802. this.showNotification("Tính năng dịch trang đang bị tắt", "warning");
  5803. return;
  5804. }
  5805. const style = document.createElement("style");
  5806. style.textContent = `
  5807. .page-translate-button {
  5808. position: fixed;
  5809. bottom: 20px;
  5810. left: 20px;
  5811. z-index: 2147483647 !important;
  5812. padding: 8px 16px;
  5813. background-color: #4CAF50;
  5814. color: white;
  5815. border: none;
  5816. border-radius: 8px;
  5817. cursor: pointer;
  5818. font-size: 14px;
  5819. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  5820. transition: all 0.3s ease;
  5821. }
  5822. .page-translate-button:hover {
  5823. background-color: #45a049;
  5824. transform: translateY(-2px);
  5825. }
  5826. `;
  5827. document.head.appendChild(style);
  5828. const button = document.createElement("button");
  5829. button.className = "page-translate-button";
  5830. button.innerHTML = this.page.isTranslated
  5831. ? "📄 Bản gốc"
  5832. : "📄 Dịch trang";
  5833. button.onclick = async () => {
  5834. try {
  5835. this.showTranslatingStatus();
  5836. const result = await this.page.translatePage();
  5837. if (result.success) {
  5838. button.innerHTML = this.page.isTranslated
  5839. ? "📄 Bản gốc"
  5840. : "📄 Dịch trang";
  5841. const toolsContainer = document.querySelector(
  5842. ".translator-tools-container"
  5843. );
  5844. if (toolsContainer) {
  5845. const menuItem = toolsContainer.querySelector(
  5846. '[data-type="pageTranslate"]'
  5847. );
  5848. if (menuItem && menuItem.querySelector(".item-text")) {
  5849. menuItem.querySelector(".item-text").textContent = this.page
  5850. .isTranslated
  5851. ? "Bản gốc"
  5852. : "Dịch trang";
  5853. }
  5854. }
  5855. this.showNotification(result.message, "success");
  5856. } else {
  5857. this.showNotification(result.message, "warning");
  5858. }
  5859. } catch (error) {
  5860. console.error("Page translation error:", error);
  5861. this.showNotification(error.message, "error");
  5862. } finally {
  5863. this.removeTranslatingStatus();
  5864. }
  5865. };
  5866. document.body.appendChild(button);
  5867. setTimeout(() => {
  5868. if (button && button.parentNode) {
  5869. button.parentNode.removeChild(button);
  5870. }
  5871. if (style && style.parentNode) {
  5872. style.parentNode.removeChild(style);
  5873. }
  5874. }, 10000);
  5875. }
  5876. setupTranslatorTools() {
  5877. const isEnabled =
  5878. localStorage.getItem("translatorToolsEnabled") === "true";
  5879. if (!isEnabled) return;
  5880. if (document.querySelector(".translator-tools-container")) {
  5881. return;
  5882. }
  5883. this.removeToolsContainer();
  5884. // bypassCSP();
  5885. const observer = new MutationObserver(() => {
  5886. const container = document.querySelector(".translator-tools-container");
  5887. if (
  5888. !container ||
  5889. container.style.display === "none" ||
  5890. container.style.visibility === "hidden"
  5891. ) {
  5892. this.createToolsContainer();
  5893. }
  5894. });
  5895. observer.observe(document.body, {
  5896. childList: true,
  5897. subtree: true,
  5898. attributes: true,
  5899. attributeFilter: ["style", "class"],
  5900. });
  5901. this.createToolsContainer();
  5902. }
  5903. createToolsContainer() {
  5904. this.removeToolsContainer();
  5905. const container = document.createElement("div");
  5906. container.className = "translator-tools-container";
  5907. container.setAttribute("data-permanent", "true");
  5908. container.setAttribute("data-translator-tool", "true");
  5909. this.handleOCRInput = async (e) => {
  5910. const file = e.target.files?.[0];
  5911. if (!file) return;
  5912. try {
  5913. this.showTranslatingStatus();
  5914. const result = await this.ocr.processImage(file);
  5915. this.displayPopup(result, null, "OCR Result");
  5916. } catch (error) {
  5917. this.showNotification(error.message);
  5918. } finally {
  5919. e.target.value = "";
  5920. this.removeTranslatingStatus();
  5921. }
  5922. };
  5923. this.handleMediaInput = async (e) => {
  5924. const file = e.target.files?.[0];
  5925. if (!file) return;
  5926. try {
  5927. this.showTranslatingStatus();
  5928. await this.media.processMediaFile(file);
  5929. } catch (error) {
  5930. this.showNotification(error.message);
  5931. } finally {
  5932. e.target.value = "";
  5933. this.removeTranslatingStatus();
  5934. }
  5935. };
  5936. const ocrInput = document.createElement("input");
  5937. ocrInput.type = "file";
  5938. ocrInput.accept = "image/*";
  5939. ocrInput.style.display = "none";
  5940. ocrInput.id = "translator-ocr-input";
  5941. ocrInput.addEventListener("change", this.handleOCRInput);
  5942. const mediaInput = document.createElement("input");
  5943. mediaInput.type = "file";
  5944. mediaInput.accept = "audio/*, video/*";
  5945. mediaInput.style.display = "none";
  5946. mediaInput.id = "translator-media-input";
  5947. mediaInput.addEventListener("change", this.handleMediaInput);
  5948. const mainButton = document.createElement("button");
  5949. mainButton.className = "translator-tools-button";
  5950. mainButton.innerHTML = `
  5951. <span class="tools-icon">⚙️</span>
  5952. `;
  5953. const dropdown = document.createElement("div");
  5954. dropdown.className = "translator-tools-dropdown";
  5955. const menuItems = [];
  5956. const settings = this.translator.userSettings.settings;
  5957. if (settings.pageTranslation?.enabled) {
  5958. menuItems.push({
  5959. icon: "📄",
  5960. text: this.page.isTranslated ? "Bản gốc" : "Dịch trang",
  5961. "data-type": "pageTranslate",
  5962. handler: async () => {
  5963. try {
  5964. dropdown.style.display = "none";
  5965. this.showTranslatingStatus();
  5966. const result = await this.page.translatePage();
  5967. if (result.success) {
  5968. const menuItem = dropdown.querySelector(
  5969. '[data-type="pageTranslate"]'
  5970. );
  5971. if (menuItem) {
  5972. const itemText = menuItem.querySelector(".item-text");
  5973. if (itemText) {
  5974. itemText.textContent = this.page.isTranslated
  5975. ? "Bản gốc"
  5976. : "Dịch trang";
  5977. }
  5978. }
  5979. this.showNotification(result.message, "success");
  5980. } else {
  5981. this.showNotification(result.message, "warning");
  5982. }
  5983. } catch (error) {
  5984. console.error("Page translation error:", error);
  5985. this.showNotification(error.message, "error");
  5986. } finally {
  5987. this.removeTranslatingStatus();
  5988. }
  5989. },
  5990. });
  5991. }
  5992. if (settings.ocrOptions?.enabled) {
  5993. menuItems.push(
  5994. {
  5995. icon: "📷",
  5996. text: "Dịch Ảnh",
  5997. handler: () => ocrInput.click(),
  5998. },
  5999. {
  6000. icon: "📸",
  6001. text: "Dịch Màn hình",
  6002. handler: async () => {
  6003. try {
  6004. dropdown.style.display = "none";
  6005. await new Promise((resolve) => setTimeout(resolve, 100));
  6006. console.log("Starting screen translation...");
  6007. this.showTranslatingStatus();
  6008. const screenshot = await this.ocr.captureScreen();
  6009. if (!screenshot) {
  6010. throw new Error("Không thể tạo ảnh chụp màn hình");
  6011. }
  6012. const result = await this.ocr.processImage(screenshot);
  6013. if (!result) {
  6014. throw new Error("Không thể xử lý ảnh chụp màn hình");
  6015. }
  6016. this.displayPopup(result, null, "OCR Màn hình");
  6017. } catch (error) {
  6018. console.error("Screen translation error:", error);
  6019. this.showNotification(error.message, "error");
  6020. } finally {
  6021. this.removeTranslatingStatus();
  6022. }
  6023. },
  6024. },
  6025. {
  6026. icon: "🖼️",
  6027. text: "Dịch Ảnh Web",
  6028. handler: () => {
  6029. dropdown.style.display = "none";
  6030. this.startWebImageOCR();
  6031. },
  6032. },
  6033. {
  6034. icon: "📚",
  6035. text: "Dịch Manga",
  6036. handler: () => {
  6037. dropdown.style.display = "none";
  6038. this.startMangaTranslation();
  6039. },
  6040. }
  6041. );
  6042. }
  6043. if (settings.mediaOptions?.enabled) {
  6044. menuItems.push({
  6045. icon: "🎵",
  6046. text: "Dịch Media",
  6047. handler: () => mediaInput.click(),
  6048. });
  6049. }
  6050. menuItems.push({
  6051. icon: "📄",
  6052. text: "Dịch File HTML",
  6053. handler: () => {
  6054. const input = document.createElement("input");
  6055. input.type = "file";
  6056. input.accept = ".html,.htm";
  6057. input.style.display = "none";
  6058. input.onchange = async (e) => {
  6059. const file = e.target.files[0];
  6060. if (!file) return;
  6061. try {
  6062. this.showTranslatingStatus();
  6063. const content = await this.readFileContent(file);
  6064. const translatedHTML = await this.page.translateHTML(content);
  6065. const blob = new Blob([translatedHTML], { type: "text/html" });
  6066. const url = URL.createObjectURL(blob);
  6067. const a = document.createElement("a");
  6068. a.href = url;
  6069. a.download = `king1x32_translated_${file.name}`;
  6070. a.click();
  6071. URL.revokeObjectURL(url);
  6072. this.showNotification("Dịch file HTML thành công", "success");
  6073. } catch (error) {
  6074. console.error("Lỗi dịch file HTML:", error);
  6075. this.showNotification(error.message, "error");
  6076. } finally {
  6077. this.removeTranslatingStatus();
  6078. input.value = "";
  6079. }
  6080. };
  6081. input.click();
  6082. },
  6083. });
  6084. menuItems.push({
  6085. icon: "📑",
  6086. text: "Dịch File PDF",
  6087. handler: () => {
  6088. const input = document.createElement("input");
  6089. input.type = "file";
  6090. input.accept = ".pdf";
  6091. input.style.display = "none";
  6092. input.onchange = async (e) => {
  6093. const file = e.target.files[0];
  6094. if (!file) return;
  6095. try {
  6096. this.showLoadingStatus("Đang xử lý PDF...");
  6097. const translatedBlob = await this.page.translatePDF(file);
  6098. const url = URL.createObjectURL(translatedBlob);
  6099. const a = document.createElement("a");
  6100. a.href = url;
  6101. a.download = `king1x32_translated_${file.name.replace(".pdf", ".html")}`;
  6102. a.click();
  6103. URL.revokeObjectURL(url);
  6104. this.showNotification("Dịch PDF thành công", "success");
  6105. } catch (error) {
  6106. console.error("Lỗi dịch PDF:", error);
  6107. this.showNotification(error.message, "error");
  6108. } finally {
  6109. this.removeLoadingStatus();
  6110. input.value = "";
  6111. }
  6112. };
  6113. input.click();
  6114. },
  6115. });
  6116. menuItems.forEach((item) => {
  6117. const menuItem = document.createElement("div");
  6118. menuItem.className = "translator-tools-item";
  6119. if (item["data-type"]) {
  6120. menuItem.setAttribute("data-type", item["data-type"]);
  6121. }
  6122. menuItem.innerHTML = `
  6123. <span class="item-icon">${item.icon}</span>
  6124. <span class="item-text">${item.text}</span>
  6125. `;
  6126. menuItem.handler = item.handler;
  6127. menuItem.addEventListener("click", item.handler);
  6128. dropdown.appendChild(menuItem);
  6129. });
  6130. this.handleButtonClick = (e) => {
  6131. e.stopPropagation();
  6132. dropdown.style.display =
  6133. dropdown.style.display === "none" ? "block" : "none";
  6134. };
  6135. mainButton.addEventListener("click", this.handleButtonClick);
  6136. this.handleClickOutside = () => {
  6137. dropdown.style.display = "none";
  6138. };
  6139. document.addEventListener("click", this.handleClickOutside);
  6140. container.appendChild(mainButton);
  6141. container.appendChild(dropdown);
  6142. container.appendChild(ocrInput);
  6143. container.appendChild(mediaInput);
  6144. document.body.appendChild(container);
  6145. GM_addStyle(`
  6146. .translator-tools-button:hover {
  6147. transform: translateY(-2px);
  6148. background-color: #357abd;
  6149. }
  6150. .translator-tools-button:disabled {
  6151. opacity: 0.7;
  6152. cursor: not-allowed;
  6153. }
  6154. .translator-overlay {
  6155. position: fixed;
  6156. top: 0;
  6157. left: 0;
  6158. width: 100%;
  6159. height: 100%;
  6160. background-color: rgba(0,0,0,0.3);
  6161. z-index: 2147483647 !important;
  6162. cursor: crosshair;
  6163. }
  6164. .translator-guide {
  6165. position: fixed;
  6166. top: 20px;
  6167. left: 50%;
  6168. transform: translateX(-50%);
  6169. background-color: rgba(0,0,0,0.8);
  6170. color: white;
  6171. padding: 10px 20px;
  6172. border-radius: 8px;
  6173. font-size: 14px;
  6174. z-index: 2147483647 !important;
  6175. }
  6176. .translator-cancel {
  6177. position: fixed;
  6178. top: 20px;
  6179. right: 20px;
  6180. background-color: #ff4444;
  6181. color: white;
  6182. border: none;
  6183. border-radius: 50%;
  6184. width: 30px;
  6185. height: 30px;
  6186. font-size: 16px;
  6187. cursor: pointer;
  6188. display: flex;
  6189. align-items: center;
  6190. justify-content: center;
  6191. z-index: 2147483647 !important;
  6192. transition: all 0.3s ease;
  6193. }
  6194. .translator-cancel:hover {
  6195. background-color: #ff0000;
  6196. transform: scale(1.1);
  6197. }
  6198. /* Animation */
  6199. @keyframes fadeIn {
  6200. from {
  6201. opacity: 0;
  6202. transform: translateY(10px);
  6203. }
  6204. to {
  6205. opacity: 1;
  6206. transform: translateY(0);
  6207. }
  6208. }
  6209. .translator-tools-container {
  6210. animation: fadeIn 0.3s ease;
  6211. }
  6212. .translator-tools-dropdown {
  6213. animation: fadeIn 0.2s ease;
  6214. }
  6215. .translator-tools-container.hidden,
  6216. .translator-notification.hidden,
  6217. .center-translate-status.hidden {
  6218. visibility: hidden !important;
  6219. }
  6220. `);
  6221. if (!document.body.contains(container)) {
  6222. document.body.appendChild(container);
  6223. }
  6224. container.style.zIndex = "2147483647 !important";
  6225. }
  6226. showProcessingStatus(message) {
  6227. this.removeProcessingStatus();
  6228. const status = document.createElement("div");
  6229. status.className = "processing-status";
  6230. status.innerHTML = `
  6231. <div class="processing-spinner" style="color: white"></div>
  6232. <div class="processing-message" style="color: white">${message}</div>
  6233. <div class="processing-progress" style="color: white">0%</div>
  6234. `;
  6235. Object.assign(status.style, {
  6236. position: "fixed",
  6237. top: "50%",
  6238. left: "50%",
  6239. transform: "translate(-50%, -50%)",
  6240. backgroundColor: "rgba(0, 0, 0, 0.8)",
  6241. color: "white",
  6242. padding: "20px",
  6243. borderRadius: "8px",
  6244. zIndex: "2147483647 !important",
  6245. textAlign: "center",
  6246. minWidth: "200px",
  6247. });
  6248. document.body.appendChild(status);
  6249. this.processingStatus = status;
  6250. }
  6251. updateProcessingStatus(message, progress) {
  6252. if (this.processingStatus) {
  6253. const messageEl = this.processingStatus.querySelector(
  6254. ".processing-message"
  6255. );
  6256. const progressEl = this.processingStatus.querySelector(
  6257. ".processing-progress"
  6258. );
  6259. if (messageEl) messageEl.textContent = message;
  6260. if (progressEl) progressEl.textContent = `${progress}%`;
  6261. }
  6262. }
  6263. removeProcessingStatus() {
  6264. if (this.processingStatus) {
  6265. this.processingStatus.remove();
  6266. this.processingStatus = null;
  6267. }
  6268. }
  6269. readFileContent(file) {
  6270. return new Promise((resolve, reject) => {
  6271. const reader = new FileReader();
  6272. reader.onload = (e) => resolve(e.target.result);
  6273. reader.onerror = () => reject(new Error("Không thể đọc file"));
  6274. reader.readAsText(file);
  6275. });
  6276. }
  6277. showLoadingStatus(message) {
  6278. const loading = document.createElement("div");
  6279. loading.id = "pdf-loading-status";
  6280. loading.style.cssText = `
  6281. position: fixed;
  6282. top: 50%;
  6283. left: 50%;
  6284. transform: translate(-50%, -50%);
  6285. background-color: rgba(0, 0, 0, 0.8);
  6286. color: white;
  6287. padding: 20px;
  6288. border-radius: 8px;
  6289. z-index: 2147483647 !important;
  6290. `;
  6291. loading.innerHTML = `
  6292. <div style="text-align: center;">
  6293. <div class="spinner" style="color: white"></div>
  6294. <div style="color: white">${message}</div>
  6295. </div>
  6296. `;
  6297. document.body.appendChild(loading);
  6298. }
  6299. removeLoadingStatus() {
  6300. const loading = document.getElementById("pdf-loading-status");
  6301. if (loading) loading.remove();
  6302. }
  6303. updateProgress(message, percent) {
  6304. const loading = document.getElementById("pdf-loading-status");
  6305. if (loading) {
  6306. loading.innerHTML = `
  6307. <div style="text-align: center;">
  6308. <div class="spinner" style="color: white"></div>
  6309. <div style="color: white">${message}</div>
  6310. <div style="color: white">${percent}%</div>
  6311. </div>
  6312. `;
  6313. }
  6314. }
  6315. startWebImageOCR() {
  6316. console.log("Starting web image OCR");
  6317. const style = document.createElement("style");
  6318. style.textContent = `
  6319. .translator-overlay {
  6320. position: fixed !important;
  6321. top: 0 !important;
  6322. left: 0 !important;
  6323. width: 100% !important;
  6324. height: 100% !important;
  6325. background-color: rgba(0,0,0,0.3) !important;
  6326. z-index: 2147483647 !important;
  6327. pointer-events: none !important;
  6328. }
  6329. .translator-guide {
  6330. position: fixed !important;
  6331. top: 20px !important;
  6332. left: 50% !important;
  6333. transform: translateX(-50%) !important;
  6334. background-color: rgba(0,0,0,0.8) !important;
  6335. color: white !important;
  6336. padding: 10px 20px !important;
  6337. border-radius: 8px !important;
  6338. font-size: 14px !important;
  6339. z-index: 2147483647 !important;
  6340. pointer-events: none !important;
  6341. }
  6342. .translator-cancel {
  6343. position: fixed !important;
  6344. top: 20px !important;
  6345. right: 20px !important;
  6346. background-color: #ff4444 !important;
  6347. color: white !important;
  6348. border: none !important;
  6349. border-radius: 50% !important;
  6350. width: 30px !important;
  6351. height: 30px !important;
  6352. font-size: 16px !important;
  6353. cursor: pointer !important;
  6354. display: flex !important;
  6355. align-items: center !important;
  6356. justify-content: center !important;
  6357. z-index: 2147483647 !important;
  6358. pointer-events: auto !important;
  6359. }
  6360. img {
  6361. pointer-events: auto !important;
  6362. }
  6363. img.translator-image-highlight {
  6364. outline: 3px solid #4a90e2 !important;
  6365. cursor: pointer !important;
  6366. position: relative !important;
  6367. z-index: 2147483647 !important;
  6368. }
  6369. `;
  6370. document.head.appendChild(style);
  6371. const overlay = document.createElement("div");
  6372. overlay.className = "translator-overlay";
  6373. const guide = document.createElement("div");
  6374. guide.className = "translator-guide";
  6375. guide.textContent = "Click vào ảnh để OCR";
  6376. const cancelBtn = document.createElement("button");
  6377. cancelBtn.className = "translator-cancel";
  6378. cancelBtn.textContent = "✕";
  6379. document.body.appendChild(overlay);
  6380. document.body.appendChild(guide);
  6381. document.body.appendChild(cancelBtn);
  6382. const handleHover = (e) => {
  6383. if (e.target.tagName === "IMG") {
  6384. e.target.classList.add("translator-image-highlight");
  6385. }
  6386. };
  6387. const handleLeave = (e) => {
  6388. if (e.target.tagName === "IMG") {
  6389. e.target.classList.remove("translator-image-highlight");
  6390. }
  6391. };
  6392. const handleClick = async (e) => {
  6393. if (e.target.tagName === "IMG") {
  6394. e.preventDefault();
  6395. e.stopPropagation();
  6396. try {
  6397. this.showTranslatingStatus();
  6398. const canvas = document.createElement("canvas");
  6399. canvas.width = e.target.naturalWidth;
  6400. canvas.height = e.target.naturalHeight;
  6401. const ctx = canvas.getContext("2d");
  6402. ctx.drawImage(e.target, 0, 0);
  6403. const blob = await new Promise((resolve) =>
  6404. canvas.toBlob(resolve, "image/png")
  6405. );
  6406. const file = new File([blob], "web-image.png", {
  6407. type: "image/png",
  6408. });
  6409. const showSource = this.translator.userSettings.settings.displayOptions.languageLearning.showSource;
  6410. const result = await this.ocr.processImage(file);
  6411. const translations = result.split("\n");
  6412. let fullTranslation = "";
  6413. let pinyin = "";
  6414. let text = "";
  6415. for (const trans of translations) {
  6416. const parts = trans.split("<|>");
  6417. if (showSource) {
  6418. text += (parts[0]?.trim() || "") + "\n";
  6419. }
  6420. pinyin += (parts[1]?.trim() || "") + "\n";
  6421. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  6422. }
  6423. this.displayPopup(
  6424. fullTranslation.trim(),
  6425. text.trim(),
  6426. "OCR Web Image",
  6427. pinyin.trim()
  6428. );
  6429. this.removeWebImageListeners();
  6430. } catch (error) {
  6431. console.error("OCR error:", error);
  6432. this.showNotification(error.message, "error");
  6433. } finally {
  6434. this.removeTranslatingStatus();
  6435. }
  6436. }
  6437. };
  6438. document.addEventListener("mouseover", handleHover, true);
  6439. document.addEventListener("mouseout", handleLeave, true);
  6440. document.addEventListener("click", handleClick, true);
  6441. cancelBtn.addEventListener("click", () => {
  6442. this.removeWebImageListeners();
  6443. });
  6444. this.webImageListeners = {
  6445. hover: handleHover,
  6446. leave: handleLeave,
  6447. click: handleClick,
  6448. overlay,
  6449. guide,
  6450. cancelBtn,
  6451. style,
  6452. };
  6453. }
  6454. startMangaTranslation() {
  6455. const style = document.createElement("style");
  6456. style.textContent = `
  6457. .translator-overlay {
  6458. position: fixed;
  6459. top: 0;
  6460. left: 0;
  6461. width: 100%;
  6462. height: 100%;
  6463. background-color: rgba(0,0,0,0.3);
  6464. z-index: 2147483647 !important;
  6465. pointer-events: none;
  6466. transition: background 0.3s ease;
  6467. }
  6468. .translator-overlay.translating-done {
  6469. background-color: transparent;
  6470. }
  6471. .translator-guide {
  6472. position: fixed;
  6473. top: 20px;
  6474. left: 50%;
  6475. transform: translateX(-50%);
  6476. background-color: rgba(0,0,0,0.8);
  6477. color: white;
  6478. padding: 10px 20px;
  6479. border-radius: 8px;
  6480. font-size: 14px;
  6481. z-index: 2147483647 !important;
  6482. }
  6483. .translator-cancel {
  6484. position: fixed;
  6485. top: 20px;
  6486. right: 20px;
  6487. background-color: #ff4444;
  6488. color: white;
  6489. border: none;
  6490. border-radius: 50%;
  6491. width: 30px;
  6492. height: 30px;
  6493. font-size: 16px;
  6494. cursor: pointer;
  6495. z-index: 2147483647 !important;
  6496. }
  6497. .manga-translation-container {
  6498. position: absolute;
  6499. top: 0;
  6500. left: 0;
  6501. pointer-events: none;
  6502. z-index: 2147483647 !important;
  6503. }
  6504. .manga-translation-overlay {
  6505. position: absolute;
  6506. background-color: rgba(255, 255, 255, 0.95);
  6507. padding: 4px 8px;
  6508. border-radius: 8px;
  6509. pointer-events: none;
  6510. text-align: center;
  6511. word-break: break-word;
  6512. display: flex;
  6513. align-items: center;
  6514. justify-content: center;
  6515. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  6516. border: 2px solid rgba(74, 144, 226, 0.7);
  6517. }
  6518. img.translator-image-highlight {
  6519. outline: 3px solid #4a90e2;
  6520. cursor: pointer;
  6521. }
  6522. `;
  6523. document.head.appendChild(style);
  6524. const overlayContainer = document.createElement("div");
  6525. overlayContainer.className = "manga-translation-container";
  6526. document.body.appendChild(overlayContainer);
  6527. const overlay = document.createElement("div");
  6528. overlay.className = "translator-overlay";
  6529. const guide = document.createElement("div");
  6530. guide.className = "translator-guide";
  6531. guide.textContent = "Click vào ảnh để dịch";
  6532. const cancelBtn = document.createElement("button");
  6533. cancelBtn.className = "translator-cancel";
  6534. cancelBtn.textContent = "✕";
  6535. document.body.appendChild(overlay);
  6536. document.body.appendChild(guide);
  6537. document.body.appendChild(cancelBtn);
  6538. let existingOverlays = [];
  6539. const handleHover = (e) => {
  6540. if (e.target.tagName === "IMG") {
  6541. e.target.classList.add("translator-image-highlight");
  6542. }
  6543. };
  6544. const handleLeave = (e) => {
  6545. if (e.target.tagName === "IMG") {
  6546. e.target.classList.remove("translator-image-highlight");
  6547. }
  6548. };
  6549. const handleClick = async (e) => {
  6550. if (e.target.tagName === "IMG") {
  6551. e.preventDefault();
  6552. e.stopPropagation();
  6553. try {
  6554. this.showTranslatingStatus();
  6555. const image = e.target;
  6556. const imageUrl = new URL(image.src);
  6557. const referer = window.location.href;
  6558. const canvas = document.createElement("canvas");
  6559. const ctx = canvas.getContext("2d");
  6560. const loadImage = async (url) => {
  6561. return new Promise((resolve, reject) => {
  6562. GM_xmlhttpRequest({
  6563. method: "GET",
  6564. url: url,
  6565. headers: {
  6566. Accept: "image/webp,image/apng,image/*,*/*;q=0.8",
  6567. "Accept-Encoding": "gzip, deflate, br",
  6568. "Accept-Language": "en-US,en;q=0.9",
  6569. "Cache-Control": "no-cache",
  6570. Pragma: "no-cache",
  6571. Referer: referer,
  6572. Origin: imageUrl.origin,
  6573. "Sec-Fetch-Dest": "image",
  6574. "Sec-Fetch-Mode": "no-cors",
  6575. "Sec-Fetch-Site": "cross-site",
  6576. "User-Agent": navigator.userAgent,
  6577. },
  6578. responseType: "blob",
  6579. anonymous: true,
  6580. onload: function(response) {
  6581. if (response.status === 200) {
  6582. const blob = response.response;
  6583. const img = new Image();
  6584. img.onload = () => {
  6585. canvas.width = img.naturalWidth;
  6586. canvas.height = img.naturalHeight;
  6587. ctx.drawImage(img, 0, 0);
  6588. resolve();
  6589. };
  6590. img.onerror = () =>
  6591. reject(new Error("Không thể load ảnh"));
  6592. img.src = URL.createObjectURL(blob);
  6593. } else {
  6594. const img = new Image();
  6595. img.crossOrigin = "anonymous";
  6596. img.onload = () => {
  6597. canvas.width = img.naturalWidth;
  6598. canvas.height = img.naturalHeight;
  6599. ctx.drawImage(img, 0, 0);
  6600. resolve();
  6601. };
  6602. img.onerror = () =>
  6603. reject(new Error("Không thể load ảnh"));
  6604. img.src = url;
  6605. }
  6606. },
  6607. onerror: function() {
  6608. const img = new Image();
  6609. img.crossOrigin = "anonymous";
  6610. img.onload = () => {
  6611. canvas.width = img.naturalWidth;
  6612. canvas.height = img.naturalHeight;
  6613. ctx.drawImage(img, 0, 0);
  6614. resolve();
  6615. };
  6616. img.onerror = () => reject(new Error("Không thể load ảnh"));
  6617. img.src = url;
  6618. },
  6619. });
  6620. });
  6621. };
  6622. await loadImage(image.src);
  6623. const blob = await new Promise((resolve, reject) => {
  6624. try {
  6625. canvas.toBlob((b) => {
  6626. if (b) resolve(b);
  6627. else reject(new Error("Không thể tạo blob"));
  6628. }, "image/png");
  6629. } catch (err) {
  6630. reject(new Error("Lỗi khi tạo blob"));
  6631. }
  6632. });
  6633. const file = new File([blob], "manga.png", { type: "image/png" });
  6634. const result = await this.detectTextPositions(file);
  6635. overlayContainer.innerHTML = "";
  6636. existingOverlays = [];
  6637. if (result?.regions) {
  6638. overlayContainer.innerHTML = "";
  6639. existingOverlays = [];
  6640. overlay.classList.add("translating-done");
  6641. const sortedRegions = result.regions.sort((a, b) => {
  6642. if (Math.abs(a.position.y - b.position.y) < 20) {
  6643. return b.position.x - a.position.x;
  6644. }
  6645. return a.position.y - b.position.y;
  6646. });
  6647. sortedRegions.forEach((region) => {
  6648. const overlay = document.createElement("div");
  6649. overlay.className = "manga-translation-overlay";
  6650. const calculatePosition = () => {
  6651. const imageRect = image.getBoundingClientRect();
  6652. const x =
  6653. (imageRect.width * region.position.x) / 100 +
  6654. imageRect.left;
  6655. const y =
  6656. (imageRect.height * region.position.y) / 100 +
  6657. imageRect.top;
  6658. const width = (imageRect.width * region.position.width) / 100;
  6659. const height =
  6660. (imageRect.height * region.position.height) / 100;
  6661. return { x, y, width, height };
  6662. };
  6663. const pos = calculatePosition();
  6664. const padding = 2;
  6665. const themeMode = this.translator.userSettings.settings.theme;
  6666. const theme = CONFIG.THEME[themeMode];
  6667. Object.assign(overlay.style, {
  6668. position: "fixed",
  6669. left: `${pos.x}px`,
  6670. top: `${pos.y}px`,
  6671. minWidth: `${pos.width - padding * 2}px`,
  6672. width: "auto",
  6673. maxWidth: `${pos.width * 1.4 - padding * 2}px`,
  6674. height: "auto",
  6675. // maxHeight: `${pos.height * 2}px`,
  6676. backgroundColor: `${theme.background}`,
  6677. color: `${theme.text}`,
  6678. padding: `${padding * 2}px ${padding * 4}px`,
  6679. borderRadius: "8px",
  6680. display: "flex",
  6681. alignItems: "center",
  6682. justifyContent: "center",
  6683. textAlign: "center",
  6684. wordBreak: "keep-all",
  6685. wordWrap: "break-word",
  6686. // overflowWrap: "normal",
  6687. lineHeight: "1.2",
  6688. pointerEvents: "none",
  6689. zIndex: "2147483647 !important",
  6690. fontSize:
  6691. this.translator.userSettings.settings.displayOptions
  6692. .webImageTranslation.fontSize || "9px",
  6693. fontWeight: "600",
  6694. margin: "0",
  6695. flexWrap: "wrap",
  6696. whiteSpace: "pre-wrap",
  6697. overflow: "visible",
  6698. boxSizing: "border-box",
  6699. transform: "none",
  6700. transformOrigin: "center center",
  6701. });
  6702. overlay.textContent = region.translation;
  6703. overlayContainer.appendChild(overlay);
  6704. const updatePosition = debounce(() => {
  6705. const newPos = calculatePosition();
  6706. overlay.style.left = `${newPos.x}px`;
  6707. overlay.style.top = `${newPos.y}px`;
  6708. overlay.style.minWidth = `${newPos.width - padding * 2}px`;
  6709. overlay.style.maxWidth = `${newPos.width * 1.4 - padding * 2
  6710. }px`;
  6711. // overlay.style.maxHeight = `${newPos.height * 2}px`;
  6712. }, 16);
  6713. window.addEventListener("scroll", updatePosition, {
  6714. passive: true,
  6715. });
  6716. window.addEventListener("resize", updatePosition, {
  6717. passive: true,
  6718. });
  6719. });
  6720. overlayContainer.style.cssText = `
  6721. position: fixed;
  6722. top: 0;
  6723. left: 0;
  6724. width: 100%;
  6725. height: 100%;
  6726. pointer-events: none;
  6727. z-index: 2147483647 !important;
  6728. `;
  6729. document.head.appendChild(style);
  6730. }
  6731. } catch (error) {
  6732. console.error("Translation error:", error);
  6733. this.showNotification(error.message, "error");
  6734. } finally {
  6735. this.removeTranslatingStatus();
  6736. }
  6737. }
  6738. };
  6739. document.addEventListener("mouseover", handleHover, true);
  6740. document.addEventListener("mouseout", handleLeave, true);
  6741. document.addEventListener("click", handleClick, true);
  6742. cancelBtn.addEventListener("click", () => {
  6743. if (this.updatePosition) {
  6744. window.removeEventListener("scroll", this.updatePosition);
  6745. window.removeEventListener("resize", this.updatePosition);
  6746. this.updatePosition = null;
  6747. }
  6748. overlayContainer.innerHTML = "";
  6749. overlayContainer.remove();
  6750. document.removeEventListener("mouseover", handleHover, true);
  6751. document.removeEventListener("mouseout", handleLeave, true);
  6752. document.removeEventListener("click", handleClick, true);
  6753. overlay.remove();
  6754. guide.remove();
  6755. cancelBtn.remove();
  6756. style.remove();
  6757. document
  6758. .querySelectorAll(".translator-image-highlight")
  6759. .forEach((el) => {
  6760. el.classList.remove("translator-image-highlight");
  6761. });
  6762. document
  6763. .querySelectorAll(".manga-translation-overlay")
  6764. .forEach((el) => el.remove());
  6765. overlay.classList.remove("translating-done");
  6766. this.removeWebImageListeners();
  6767. });
  6768. this.webImageListeners = {
  6769. hover: handleHover,
  6770. leave: handleLeave,
  6771. click: handleClick,
  6772. overlay,
  6773. guide,
  6774. cancelBtn,
  6775. style,
  6776. container: overlayContainer,
  6777. };
  6778. }
  6779. async detectTextPositions(file) {
  6780. try {
  6781. const base64Image = await this.ocr.fileToBase64(file);
  6782. const settings = this.translator.userSettings.settings;
  6783. const selectedModel = this.translator.api.getGeminiModel();
  6784. const targetLanguage = settings.displayOptions.targetLanguage;
  6785. const requestBody = {
  6786. contents: [
  6787. {
  6788. parts: [
  6789. {
  6790. text: `Analyze this image and extract all text regions. For each text region:
  6791. 1. Extract the original text
  6792. 2. Dch sang ngôn ng có mã ngôn ng là '${targetLanguage}' vi yêu cu sau:
  6793. Bn là mt người dch truyn chuyên nghip, chuyên to bn dch chính xác và t nhiên. Bn cn dch mt đon truyn sang ngôn ng có mã ngôn ng là '${targetLanguage}'. Hãy đảm bo rng bn dch ca bn gi nguyên ý nghĩa ca câu gc và phù hp vi văn phong ca ngôn ng đích. Khi dch, hãy chú ý đến ng cnh văn hóa và bi cnh ca câu chuyn để người đọc có th hiu chính xác ni dung. Các quy tc quan trng bn cn tuân th bao gm:
  6794. - Đảm bo nghĩa ca các câu không b thay đổi khi dch.
  6795. - S dng các t lóng hoc cm t thông dng khi cn thiết để bn dch gn gũi vi người đọc.
  6796. - Kim tra chính t và ng pháp trong bn dch.
  6797. - Thêm các tr t ngôn ng có mã ngôn ng là '${targetLanguage}' khi cn để câu văn hoàn chnh hơn.
  6798. - S dng đại t nhân xưng ngôn ng có mã ngôn ng là '${targetLanguage}' t nhiên và hp ng cnh.
  6799. - Tham kho các trang web sau để ly văn phong phù hp cho các đon truyn liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
  6800. Lưu ý:
  6801. - Nhng t tên riêng, địa đim thì hãy dch theo nghĩa Hán Vit ví d như: Dip Trn, Lc Thiếu Du, Long kiếm, Long Sĩ Đầu, Thiên kiếp, ngõ Nê Bình, ... thì gi theo nghĩa Hán Vit s hay hơn là dch hn sang ngôn ng có mã ngôn ng là '${targetLanguage}'.
  6802. - Ch tr v bn dch ngôn ng có mã ngôn ng là '${targetLanguage}', không gii thích thêm.
  6803. 3. Determine PRECISE position and size:
  6804. - x, y: exact percentage position relative to image (0-100)
  6805. - width, height: exact percentage size relative to image (0-100)
  6806. - text_length: character count of original text
  6807. - text_lines: number of text lines
  6808. - bubble_type: speech/thought/narration/sfx
  6809. Return ONLY a JSON object like:
  6810. {
  6811. "regions": [{
  6812. "text": "original text",
  6813. "translation": "translated text",
  6814. "position": {
  6815. "x": 20.5,
  6816. "y": 30.2,
  6817. "width": 15.3,
  6818. "height": 10.1,
  6819. "text_length": 25,
  6820. "text_lines": 2,
  6821. "bubble_type": "speech"
  6822. }
  6823. }]
  6824. }`,
  6825. },
  6826. {
  6827. inline_data: {
  6828. mime_type: file.type,
  6829. data: base64Image,
  6830. },
  6831. },
  6832. ],
  6833. },
  6834. ],
  6835. generationConfig: {
  6836. temperature: settings.ocrOptions.temperature,
  6837. topP: settings.ocrOptions.topP,
  6838. topK: settings.ocrOptions.topK,
  6839. },
  6840. };
  6841. console.log("Sending API request...");
  6842. const responses =
  6843. await this.translator.api.keyManager.executeWithMultipleKeys(
  6844. async (key) => {
  6845. const response = await new Promise((resolve, reject) => {
  6846. GM_xmlhttpRequest({
  6847. method: "POST",
  6848. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`,
  6849. headers: { "Content-Type": "application/json" },
  6850. data: JSON.stringify(requestBody),
  6851. onload: (response) => {
  6852. console.log("API Response:", response);
  6853. if (response.status === 200) {
  6854. try {
  6855. const result = JSON.parse(response.responseText);
  6856. const text =
  6857. result?.candidates?.[0]?.content?.parts?.[0]?.text;
  6858. if (text) {
  6859. const jsonMatch = text.match(/\{[\s\S]*\}/);
  6860. if (jsonMatch) {
  6861. const parsedJson = JSON.parse(jsonMatch[0]);
  6862. resolve(parsedJson);
  6863. } else {
  6864. reject(new Error("No JSON found in response"));
  6865. }
  6866. } else {
  6867. reject(new Error("Invalid response format"));
  6868. }
  6869. } catch (error) {
  6870. console.error("Parse error:", error);
  6871. reject(error);
  6872. }
  6873. } else {
  6874. reject(new Error(`API Error: ${response.status}`));
  6875. }
  6876. },
  6877. onerror: (error) => reject(error),
  6878. });
  6879. });
  6880. return response;
  6881. },
  6882. settings.apiProvider
  6883. );
  6884. console.log("API responses:", responses);
  6885. const response = responses.find((r) => r && r.regions);
  6886. if (!response) {
  6887. throw new Error("No valid response found");
  6888. }
  6889. return response;
  6890. } catch (error) {
  6891. console.error("Text detection error:", error);
  6892. throw error;
  6893. }
  6894. }
  6895. getBrowserContextMenuSize() {
  6896. const browser = navigator.userAgent;
  6897. const sizes = {
  6898. firefox: {
  6899. width: 275,
  6900. height: 340,
  6901. itemHeight: 34,
  6902. },
  6903. chrome: {
  6904. width: 250,
  6905. height: 320,
  6906. itemHeight: 32,
  6907. },
  6908. safari: {
  6909. width: 240,
  6910. height: 300,
  6911. itemHeight: 30,
  6912. },
  6913. edge: {
  6914. width: 260,
  6915. height: 330,
  6916. itemHeight: 33,
  6917. },
  6918. };
  6919. let size;
  6920. if (browser.includes("Firefox")) {
  6921. size = sizes.firefox;
  6922. } else if (browser.includes("Safari") && !browser.includes("Chrome")) {
  6923. size = sizes.safari;
  6924. } else if (browser.includes("Edge")) {
  6925. size = sizes.edge;
  6926. } else {
  6927. size = sizes.chrome;
  6928. }
  6929. const dpi = window.devicePixelRatio || 1;
  6930. return {
  6931. width: Math.round(size.width * dpi),
  6932. height: Math.round(size.height * dpi),
  6933. itemHeight: Math.round(size.itemHeight * dpi),
  6934. };
  6935. }
  6936. setupContextMenu() {
  6937. if (!this.translator.userSettings.settings.contextMenu?.enabled) return;
  6938. document.addEventListener("contextmenu", (e) => {
  6939. const selection = window.getSelection();
  6940. const selectedText = selection.toString().trim();
  6941. if (selectedText) {
  6942. const oldMenus = document.querySelectorAll(
  6943. ".translator-context-menu"
  6944. );
  6945. oldMenus.forEach((menu) => menu.remove());
  6946. const contextMenu = document.createElement("div");
  6947. contextMenu.className = "translator-context-menu";
  6948. const menuItems = [
  6949. { text: "Dịch nhanh", action: "quick" },
  6950. { text: "Dịch popup", action: "popup" },
  6951. { text: "Dịch nâng cao", action: "advanced" },
  6952. ];
  6953. const range = selection.getRangeAt(0).cloneRange();
  6954. menuItems.forEach((item) => {
  6955. const menuItem = document.createElement("div");
  6956. menuItem.className = "translator-context-menu-item";
  6957. menuItem.textContent = item.text;
  6958. menuItem.onclick = (e) => {
  6959. e.preventDefault();
  6960. e.stopPropagation();
  6961. const newSelection = window.getSelection();
  6962. newSelection.removeAllRanges();
  6963. newSelection.addRange(range);
  6964. this.handleTranslateButtonClick(newSelection, item.action);
  6965. contextMenu.remove();
  6966. };
  6967. contextMenu.appendChild(menuItem);
  6968. });
  6969. const viewportWidth = window.innerWidth;
  6970. const viewportHeight = window.innerHeight;
  6971. const menuWidth = 150;
  6972. const menuHeight = (menuItems.length * 40);
  6973. const browserMenu = this.getBrowserContextMenuSize();
  6974. const browserMenuWidth = browserMenu.width;
  6975. const browserMenuHeight = browserMenu.height;
  6976. const spaceWidth = browserMenuWidth + menuWidth;
  6977. const remainingWidth = viewportWidth - e.clientX;
  6978. const rightEdge = viewportWidth - menuWidth;
  6979. const bottomEdge = viewportHeight - menuHeight;
  6980. const browserMenuWidthEdge = viewportWidth - browserMenuWidth;
  6981. const browserMenuHeightEdge = viewportHeight - browserMenuHeight;
  6982. let left, top;
  6983. if (e.clientX < menuWidth && e.clientY < menuHeight) {
  6984. left = e.clientX + browserMenuWidth + 10;
  6985. top = e.clientY;
  6986. } else if (
  6987. e.clientX > browserMenuWidthEdge &&
  6988. e.clientY < browserMenuHeight
  6989. ) {
  6990. left = e.clientX - spaceWidth + remainingWidth;
  6991. top = e.clientY;
  6992. } else if (
  6993. e.clientX > browserMenuWidthEdge &&
  6994. e.clientY > viewportHeight - browserMenuHeight
  6995. ) {
  6996. left = e.clientX - spaceWidth + remainingWidth;
  6997. top = e.clientY - menuHeight;
  6998. } else if (
  6999. e.clientX < menuWidth &&
  7000. e.clientY > viewportHeight - browserMenuHeight
  7001. ) {
  7002. left = e.clientX + browserMenuWidth + 10;
  7003. top = e.clientY - menuHeight;
  7004. } else if (e.clientY < menuHeight) {
  7005. left = e.clientX - menuWidth;
  7006. top = e.clientY;
  7007. } else if (e.clientX > browserMenuWidthEdge) {
  7008. left = e.clientX - spaceWidth + remainingWidth;
  7009. top = e.clientY;
  7010. } else if (e.clientY > browserMenuHeightEdge - menuHeight / 2) {
  7011. left = e.clientX - menuWidth;
  7012. top = e.clientY - menuHeight;
  7013. } else {
  7014. left = e.clientX;
  7015. top = e.clientY - menuHeight;
  7016. }
  7017. left = Math.max(5, Math.min(left, rightEdge - 5));
  7018. top = Math.max(5, Math.min(top, bottomEdge - 5));
  7019. contextMenu.style.left = `${left}px`;
  7020. contextMenu.style.top = `${top}px`;
  7021. document.body.appendChild(contextMenu);
  7022. const closeMenu = (e) => {
  7023. if (!contextMenu.contains(e.target)) {
  7024. contextMenu.remove();
  7025. document.removeEventListener("click", closeMenu);
  7026. }
  7027. };
  7028. document.addEventListener("click", closeMenu);
  7029. const handleScroll = () => {
  7030. contextMenu.remove();
  7031. window.removeEventListener("scroll", handleScroll);
  7032. };
  7033. window.addEventListener("scroll", handleScroll);
  7034. }
  7035. });
  7036. const themeMode = this.translator.userSettings.settings.theme;
  7037. const theme = CONFIG.THEME[themeMode];
  7038. GM_addStyle(`
  7039. .translator-context-menu {
  7040. position: fixed;
  7041. color: ${theme.text};
  7042. background-color: ${theme.background};
  7043. border-radius: 8px;
  7044. padding: 8px 8px 5px 8px;
  7045. min-width: 150px;
  7046. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  7047. z-index: 2147483647 !important;
  7048. font-family: Arial, sans-serif;
  7049. font-size: 14px;
  7050. opacity: 0;
  7051. transform: scale(0.95);
  7052. transition: all 0.1s ease-out;
  7053. animation: menuAppear 0.15s ease-out forwards;
  7054. }
  7055. @keyframes menuAppear {
  7056. from {
  7057. opacity: 0;
  7058. transform: scale(0.95);
  7059. }
  7060. to {
  7061. opacity: 1;
  7062. transform: scale(1);
  7063. }
  7064. }
  7065. .translator-context-menu-item {
  7066. padding: 5px;
  7067. margin-bottom: 3px;
  7068. cursor: pointer;
  7069. color: ${theme.text};
  7070. background-color: ${theme.backgroundShadow};
  7071. border: 1px solid ${theme.border};
  7072. border-radius: 7px;
  7073. transition: all 0.2s ease;
  7074. display: flex;
  7075. align-items: center;
  7076. gap: 8px;
  7077. white-space: nowrap;
  7078. z-index: 2147483647 !important;
  7079. }
  7080. .translator-context-menu-item:hover {
  7081. background-color: ${theme.button.translate.background};
  7082. color: ${theme.button.translate.text};
  7083. }
  7084. .translator-context-menu-item:active {
  7085. transform: scale(0.98);
  7086. }
  7087. `);
  7088. }
  7089. removeWebImageListeners() {
  7090. if (this.webImageListeners) {
  7091. document.removeEventListener(
  7092. "mouseover",
  7093. this.webImageListeners.hover,
  7094. true
  7095. );
  7096. document.removeEventListener(
  7097. "mouseout",
  7098. this.webImageListeners.leave,
  7099. true
  7100. );
  7101. document.removeEventListener(
  7102. "click",
  7103. this.webImageListeners.click,
  7104. true
  7105. );
  7106. this.webImageListeners.overlay?.remove();
  7107. this.webImageListeners.guide?.remove();
  7108. this.webImageListeners.cancelBtn?.remove();
  7109. this.webImageListeners.style?.remove();
  7110. document
  7111. .querySelectorAll(".translator-image-highlight")
  7112. .forEach((el) => {
  7113. el.classList.remove("translator-image-highlight");
  7114. });
  7115. this.webImageListeners = null;
  7116. }
  7117. }
  7118. handleSettingsShortcut(e) {
  7119. if (!this.translator.userSettings.settings.shortcuts?.settingsEnabled)
  7120. return;
  7121. if ((e.altKey || e.metaKey) && e.key === "s") {
  7122. e.preventDefault();
  7123. const settingsUI = this.translator.userSettings.createSettingsUI();
  7124. document.body.appendChild(settingsUI);
  7125. }
  7126. }
  7127. async handleTranslationShortcuts(e) {
  7128. if (!this.translator.userSettings.settings.shortcuts?.enabled) return;
  7129. const shortcuts = this.translator.userSettings.settings.shortcuts;
  7130. if (e.altKey || e.metaKey) {
  7131. let translateType = null;
  7132. if (e.key === shortcuts.pageTranslate.key) {
  7133. e.preventDefault();
  7134. await this.handlePageTranslation();
  7135. return;
  7136. } else if (e.key === shortcuts.inputTranslate.key) {
  7137. e.preventDefault();
  7138. const activeElement = document.activeElement;
  7139. if (this.input.isValidEditor(activeElement)) {
  7140. const text = this.input.getEditorContent(activeElement);
  7141. if (text) {
  7142. await this.input.translateEditor(activeElement, true);
  7143. }
  7144. }
  7145. return;
  7146. }
  7147. const selection = window.getSelection();
  7148. const selectedText = selection?.toString().trim();
  7149. if (!selectedText || this.isTranslating) return;
  7150. const targetElement = selection.anchorNode?.parentElement;
  7151. if (!targetElement) return;
  7152. if (e.key === shortcuts.quickTranslate.key) {
  7153. e.preventDefault();
  7154. translateType = "quick";
  7155. } else if (e.key === shortcuts.popupTranslate.key) {
  7156. e.preventDefault();
  7157. translateType = "popup";
  7158. } else if (e.key === shortcuts.advancedTranslate.key) {
  7159. e.preventDefault();
  7160. translateType = "advanced";
  7161. }
  7162. if (translateType) {
  7163. await this.handleTranslateButtonClick(selection, translateType);
  7164. }
  7165. }
  7166. }
  7167. updateSettingsListener(enabled) {
  7168. if (enabled) {
  7169. document.addEventListener("keydown", this.settingsShortcutListener);
  7170. } else {
  7171. document.removeEventListener("keydown", this.settingsShortcutListener);
  7172. }
  7173. }
  7174. updateSettingsTranslationListeners(enabled) {
  7175. if (enabled) {
  7176. document.addEventListener("keydown", this.translationShortcutListener);
  7177. } else {
  7178. document.removeEventListener(
  7179. "keydown",
  7180. this.translationShortcutListener
  7181. );
  7182. }
  7183. }
  7184. updateSelectionListeners(enabled) {
  7185. if (enabled) this.setupSelectionHandlers();
  7186. }
  7187. updateTapListeners(enabled) {
  7188. if (enabled) this.setupDocumentTapHandler();
  7189. }
  7190. setupEventListeners() {
  7191. const shortcuts = this.translator.userSettings.settings.shortcuts;
  7192. const clickOptions = this.translator.userSettings.settings.clickOptions;
  7193. const touchOptions = this.translator.userSettings.settings.touchOptions;
  7194. if (this.translator.userSettings.settings.contextMenu?.enabled) {
  7195. this.setupContextMenu();
  7196. }
  7197. if (shortcuts?.settingsEnabled) {
  7198. this.updateSettingsListener(true);
  7199. }
  7200. if (shortcuts?.enabled) {
  7201. this.updateSettingsTranslationListeners(true);
  7202. }
  7203. if (clickOptions?.enabled) {
  7204. this.updateSelectionListeners(true);
  7205. this.translationButtonEnabled = true;
  7206. }
  7207. if (touchOptions?.enabled) {
  7208. this.updateTapListeners(true);
  7209. this.translationTapEnabled = true;
  7210. }
  7211. const isEnabled =
  7212. localStorage.getItem("translatorToolsEnabled") === "true";
  7213. if (isEnabled) {
  7214. this.setupTranslatorTools();
  7215. }
  7216. document.addEventListener("settingsChanged", (e) => {
  7217. this.removeToolsContainer();
  7218. const newSettings = e.detail;
  7219. if (newSettings.theme !== this.translator.userSettings.settings.theme) {
  7220. this.updateAllButtonStyles();
  7221. }
  7222. this.updateSettingsListener(newSettings.shortcuts?.settingsEnabled);
  7223. this.updateSettingsTranslationListeners(newSettings.shortcuts?.enabled);
  7224. if (newSettings.clickOptions?.enabled !== undefined) {
  7225. this.translationButtonEnabled = newSettings.clickOptions.enabled;
  7226. this.updateSelectionListeners(newSettings.clickOptions.enabled);
  7227. if (!newSettings.clickOptions.enabled) {
  7228. this.removeTranslateButton();
  7229. }
  7230. }
  7231. if (newSettings.touchOptions?.enabled !== undefined) {
  7232. this.translationTapEnabled = newSettings.touchOptions.enabled;
  7233. this.updateTapListeners(newSettings.touchOptions.enabled);
  7234. if (!newSettings.touchOptions.enabled) {
  7235. this.removeTranslateButton();
  7236. }
  7237. }
  7238. this.cache = new TranslationCache(
  7239. newSettings.cacheOptions.text.maxSize,
  7240. newSettings.cacheOptions.text.expirationTime
  7241. );
  7242. this.cache.clear();
  7243. if (this.ocr?.imageCache) {
  7244. this.ocr.imageCache.clear();
  7245. }
  7246. const apiConfig = {
  7247. providers: CONFIG.API.providers,
  7248. currentProvider: newSettings.apiProvider,
  7249. apiKey: newSettings.apiKey,
  7250. maxRetries: CONFIG.API.maxRetries,
  7251. retryDelay: CONFIG.API.retryDelay,
  7252. };
  7253. this.api = new APIManager(
  7254. apiConfig,
  7255. () => this.translator.userSettings.settings
  7256. );
  7257. const isEnabled =
  7258. localStorage.getItem("translatorToolsEnabled") === "true";
  7259. if (isEnabled) {
  7260. this.setupTranslatorTools();
  7261. }
  7262. });
  7263. }
  7264. showNotification(message, type = "info") {
  7265. const notification = document.createElement("div");
  7266. notification.className = "translator-notification";
  7267. const colors = {
  7268. info: "#4a90e2",
  7269. success: "#28a745",
  7270. warning: "#ffc107",
  7271. error: "#dc3545",
  7272. };
  7273. const backgroundColor = colors[type] || colors.info;
  7274. const textColor = type === "warning" ? "#000" : "#fff";
  7275. Object.assign(notification.style, {
  7276. position: "fixed",
  7277. top: "20px",
  7278. left: "50%",
  7279. transform: "translateX(-50%)",
  7280. backgroundColor,
  7281. color: textColor,
  7282. padding: "10px 20px",
  7283. borderRadius: "8px",
  7284. zIndex: "2147483647 !important",
  7285. animation: "fadeInOut 2s ease",
  7286. fontFamily: "Arial, sans-serif",
  7287. fontSize: "14px",
  7288. boxShadow: "0 2px 10px rgba(0,0,0,0.2)",
  7289. });
  7290. notification.textContent = message;
  7291. document.body.appendChild(notification);
  7292. setTimeout(() => notification.remove(), 2000);
  7293. }
  7294. resetState() {
  7295. if (this.pressTimer) clearTimeout(this.pressTimer);
  7296. if (this.timer) clearTimeout(this.timer);
  7297. this.isLongPress = false;
  7298. this.lastTime = 0;
  7299. this.count = 0;
  7300. this.isDown = false;
  7301. this.isTranslating = false;
  7302. this.ignoreNextSelectionChange = false;
  7303. this.removeTranslateButton();
  7304. this.removeTranslatingStatus();
  7305. }
  7306. removeTranslateButton() {
  7307. if (this.currentTranslateButton) {
  7308. this.currentTranslateButton.remove();
  7309. this.currentTranslateButton = null;
  7310. }
  7311. }
  7312. removeTranslatingStatus() {
  7313. if (this.translatingStatus) {
  7314. this.translatingStatus.remove();
  7315. this.translatingStatus = null;
  7316. }
  7317. }
  7318. }
  7319. class Translator {
  7320. constructor() {
  7321. window.translator = this;
  7322. this.userSettings = new UserSettings(this);
  7323. const apiConfig = {
  7324. ...CONFIG.API,
  7325. currentProvider: this.userSettings.getSetting("apiProvider"),
  7326. apiKey: this.userSettings.getSetting("apiKey"),
  7327. };
  7328. this.api = new APIManager(apiConfig, () => this.userSettings.settings);
  7329. this.ocr = new OCRManager(this);
  7330. this.ui = new UIManager(this);
  7331. this.cache = new TranslationCache(
  7332. this.userSettings.settings.cacheOptions.text.maxSize,
  7333. this.userSettings.settings.cacheOptions.text.expirationTime
  7334. );
  7335. this.page = new PageTranslator(this);
  7336. this.inputTranslator = new InputTranslator(this);
  7337. this.ui.setupEventListeners();
  7338. this.cache.optimizeStorage();
  7339. this.autoCorrectEnabled = true;
  7340. }
  7341. async translate(
  7342. text,
  7343. targetElement,
  7344. isAdvanced = false,
  7345. popup = false,
  7346. targetLang = ""
  7347. ) {
  7348. try {
  7349. if (!text) return null;
  7350. const settings = this.userSettings.settings.displayOptions;
  7351. const targetLanguage = targetLang || settings.targetLanguage;
  7352. const promptType = isAdvanced ? "advanced" : "normal";
  7353. const prompt = this.createPrompt(text, promptType, targetLanguage);
  7354. let translatedText;
  7355. const cacheEnabled =
  7356. this.userSettings.settings.cacheOptions.text.enabled;
  7357. if (cacheEnabled) {
  7358. translatedText = this.cache.get(text, isAdvanced, targetLanguage);
  7359. }
  7360. if (!translatedText) {
  7361. translatedText = await this.api.request(prompt);
  7362. if (cacheEnabled && translatedText) {
  7363. this.cache.set(text, translatedText, isAdvanced, targetLanguage);
  7364. }
  7365. }
  7366. if (
  7367. translatedText &&
  7368. targetElement &&
  7369. !targetElement.isPDFTranslation
  7370. ) {
  7371. if (isAdvanced || popup) {
  7372. const translations = translatedText.split("\n");
  7373. let fullTranslation = "";
  7374. let pinyin = "";
  7375. for (const trans of translations) {
  7376. const parts = trans.split("<|>");
  7377. pinyin += (parts[1]?.trim() || "") + "\n";
  7378. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  7379. }
  7380. this.ui.displayPopup(
  7381. fullTranslation.trim(),
  7382. text,
  7383. "King1x32 <3",
  7384. pinyin.trim()
  7385. );
  7386. } else {
  7387. this.ui.showTranslationBelow(translatedText, targetElement, text);
  7388. }
  7389. }
  7390. return translatedText;
  7391. } catch (error) {
  7392. console.error("Lỗi dịch:", error);
  7393. this.ui.showNotification(error.message, "error");
  7394. }
  7395. }
  7396. async translateLongText(text, maxChunkSize = 1000) {
  7397. const chunks = this.splitIntoChunks(text, maxChunkSize);
  7398. const translations = await Promise.all(
  7399. chunks.map((chunk) => this.translate(chunk))
  7400. );
  7401. return this.smartMerge(translations);
  7402. }
  7403. splitIntoChunks(text, maxChunkSize) {
  7404. const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
  7405. const chunks = [];
  7406. let currentChunk = "";
  7407. for (const sentence of sentences) {
  7408. if ((currentChunk + sentence).length > maxChunkSize && currentChunk) {
  7409. chunks.push(currentChunk.trim());
  7410. currentChunk = "";
  7411. }
  7412. currentChunk += sentence + " ";
  7413. }
  7414. if (currentChunk) {
  7415. chunks.push(currentChunk.trim());
  7416. }
  7417. return chunks;
  7418. }
  7419. smartMerge(translations) {
  7420. return translations.reduce((merged, current, index) => {
  7421. if (index === 0) return current;
  7422. const lastChar = merged.slice(-1);
  7423. if (".!?".includes(lastChar)) {
  7424. return `${merged} ${current}`;
  7425. }
  7426. return merged + current;
  7427. }, "");
  7428. }
  7429. async autoCorrect(translation) {
  7430. const targetLanguage =
  7431. this.userSettings.settings.displayOptions.targetLanguage;
  7432. const prompt = `Vui lòng kim tra và sa cha bt k li ng pháp hoc vn đề v ng cnh trong bn dch sang ngôn ng có mã ngôn ng là '${targetLanguage}' này: "${translation}". Không thêm hay bt ý ca bn gc cũng như không thêm tiêu đề, không gii thích v các thay đổi đã thc hin.`;
  7433. try {
  7434. const corrected = await this.api.request(prompt);
  7435. return corrected.trim();
  7436. } catch (error) {
  7437. console.error("Auto-correction failed:", error);
  7438. return translation;
  7439. }
  7440. }
  7441. createPrompt(text, type = "normal", targetLang = "") {
  7442. const settings = this.userSettings.settings;
  7443. const targetLanguage =
  7444. targetLang || settings.displayOptions.targetLanguage;
  7445. const sourceLanguage = settings.displayOptions.sourceLanguage;
  7446. const isPinyinMode =
  7447. settings.displayOptions.translationMode !== "translation_only"
  7448. if (
  7449. settings.promptSettings?.enabled &&
  7450. settings.promptSettings?.useCustom
  7451. ) {
  7452. const prompts = settings.promptSettings.customPrompts;
  7453. const promptKey = isPinyinMode ? `${type}_chinese` : type;
  7454. let promptTemplate = prompts[promptKey];
  7455. if (promptTemplate) {
  7456. return promptTemplate
  7457. .replace(/\{text\}/g, text)
  7458. .replace(/\{targetLang\}/g, targetLanguage)
  7459. .replace(
  7460. /\{sourceLang\}/g,
  7461. sourceLanguage || this.page.languageCode
  7462. );
  7463. }
  7464. }
  7465. return this.createDefaultPrompt(text, type, isPinyinMode, targetLanguage);
  7466. }
  7467. createDefaultPrompt(
  7468. text,
  7469. type = "normal",
  7470. isPinyinMode = false,
  7471. targetLang = ""
  7472. ) {
  7473. const settings = this.userSettings.settings;
  7474. const targetLanguage =
  7475. targetLang || settings.displayOptions.targetLanguage;
  7476. const share_media = `Bn là mt người dch ph đề phim chuyên nghip, chuyên to file SRT. Bn cn dch mt đon hi thoi phim sang ngôn ng có mã ngôn ng là '${targetLanguage}'. Hãy đảm bo rng bn dch ca bn chính xác và t nhiên, gi nguyên ý nghĩa ca câu gc. Khi dch, hãy chú ý đến ng cnh văn hóa và bi cnh ca b phim để người xem có th hiu chính xác ni dung. Các quy tc quan trng bn cn tuân th bao gm:
  7477. - Đảm bo nghĩa ca các câu không b thay đổi khi dch.
  7478. - S dng các t lóng hoc cm t thông dng khi cn thiết để bn dch gn gũi vi người đọc.
  7479. - Kim tra chính t và ng pháp trong bn dch.
  7480. - Thêm các tr t ngôn ng có mã ngôn ng là '${targetLanguage}' khi cn để hi thoi hoàn chnh hơn.
  7481. - S dng đại t nhân xưng ngôn ng có mã ngôn ng là '${targetLanguage}' t nhiên và hp ng cnh.
  7482. - Tham kho các trang web sau để ly văn phong phù hp cho các đon hi thoi liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
  7483. Lưu ý:
  7484. - Bn dch phi hoàn toàn bng ngôn ng có mã ngôn ng là '${targetLanguage}', nhưng ví d khi dch sang tiếng Vit nếu gp nhng danh t riêng ch địa đim hoc tên riêng, có phm trù trong ngôn ng là t ghép ca 2 ngôn ng gi là t Hán Vit, hãy dch sang nghĩa t Hán Vit như Dip Trn, Lc 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à dch hn sang nghĩa tiếng Vit là Lá Trn, Rng kiếm, Tri kiếp, núi Rng Ngng Đầu,...`;
  7485. const base_normal = `Cho bn văn bn cn x lý: "${text}"
  7486. Hãy dch văn bn cn x lý trên sang ngôn ng có mã ngôn ng là '${targetLanguage}' vi các yêu cu sau:
  7487. - Dch phi tuân th cht ch bi cnh và sc thái ban đầu ca văn bn.
  7488. - Đảm bo s lưu loát và t nhiên như người bn xứ.
  7489. - Không thêm bt k gii thích hay din gii nào ngoài bn dch.
  7490. - Bo toàn các thut ng và danh t riêng vi t l 1:1.
  7491. Nếu bn nhn thy văn bn là truyn thì hãy dch truyn theo yêu cu sau:
  7492. Bn là mt người dch truyn chuyên nghip, chuyên to bn dch chính xác và t nhiên. Bn cn dch mt đon truyn sang ngôn ng có mã ngôn ng là '${targetLanguage}'. Hãy đảm bo rng bn dch ca bn gi nguyên ý nghĩa ca câu gc và phù hp vi văn phong ca ngôn ng đích. Khi dch, hãy chú ý đến ng cnh văn hóa và bi cnh ca câu chuyn để người đọc có th hiu chính xác ni dung. Các quy tc quan trng bn cn tuân th bao gm:
  7493. - Đảm bo nghĩa ca các câu không b thay đổi khi dch.
  7494. - S dng các t lóng hoc cm t thông dng khi cn thiết để bn dch gn gũi vi người đọc.
  7495. - Kim tra chính t và ng pháp trong bn dch.
  7496. - Thêm các tr t ngôn ng có mã ngôn ng là '${targetLanguage}' khi cn để câu văn hoàn chnh hơn.
  7497. - S dng đại t nhân xưng ngôn ng có mã ngôn ng là '${targetLanguage}' t nhiên và hp ng cnh.
  7498. - Tham kho các trang web sau để ly văn phong phù hp cho các đon truyn liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
  7499. Lưu ý:
  7500. - Bn dch phi hoàn toàn bng ngôn ng có mã ngôn ng là '${targetLanguage}', nhưng ví d khi dch sang tiếng Vit nếu gp nhng danh t riêng ch địa đim hoc tên riêng, có phm trù trong ngôn ng là t ghép ca 2 ngôn ng gi là t Hán Vit, hãy dch sang nghĩa t Hán Vit như Dip Trn, Lc 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à dch hn sang nghĩa tiếng Vit là Lá Trn, Rng kiếm, Tri kiếp, núi Rng Ngng Đầu,...
  7501. - Hãy in ra bn dch mà không có du ngoc kép, gi nguyên định dng phông ch ban đầu và không gii thích gì thêm.`;
  7502. const basePrompts = {
  7503. normal: `${base_normal}`,
  7504. advanced: `Dch và phân tích t khóa: "${text}"`,
  7505. ocr: `Bn là mt người dch truyn chuyên nghip, chuyên to bn dch chính xác và t nhiên. Bn cn dch mt đon truyn sang ngôn ng có mã ngôn ng là '${targetLanguage}'. Hãy đảm bo rng bn dch ca bn gi nguyên ý nghĩa ca câu gc và phù hp vi văn phong ca ngôn ng đích. Khi dch, hãy chú ý đến ng cnh văn hóa và bi cnh ca câu chuyn để người đọc có th hiu chính xác ni dung. Các quy tc quan trng bn cn tuân th bao gm:
  7506. - Đảm bo nghĩa ca các câu không b thay đổi khi dch.
  7507. - S dng các t lóng hoc cm t thông dng khi cn thiết để bn dch gn gũi vi người đọc.
  7508. - Kim tra chính t và ng pháp trong bn dch.
  7509. - Thêm các tr t ngôn ng có mã ngôn ng là '${targetLanguage}' khi cn để câu văn hoàn chnh hơn.
  7510. - S dng đại t nhân xưng ngôn ng có mã ngôn ng là '${targetLanguage}' t nhiên và hp ng cnh.
  7511. - Tham kho các trang web sau để ly văn phong phù hp cho các đon truyn liên quan đến sex: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
  7512. Lưu ý:
  7513. - Bn dch phi hoàn toàn bng ngôn ng có mã ngôn ng là '${targetLanguage}', nhưng ví d khi dch sang tiếng Vit nếu gp nhng danh t riêng ch địa đim hoc tên riêng, có phm trù trong ngôn ng là t ghép ca 2 ngôn ng gi là t Hán Vit, hãy dch sang nghĩa t Hán Vit như Dip Trn, Lc 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à dch hn sang nghĩa tiếng Vit là Lá Trn, Rng kiếm, Tri kiếp, núi Rng Ngng Đầu,...
  7514. - Ch tr v bn dch ngôn ng có mã ngôn ng là '${targetLanguage}', không gii thích thêm.`,
  7515. media: `${share_media}
  7516. - Định dng bn dch ca bn theo định dng SRT và đảm bo rng mi đon hi thoi được đánh s th tự, có thi gian bt đầu và kết thúc rõ ràng. Không cn gii thích thêm.`,
  7517. page: `${base_normal}`,
  7518. };
  7519. const pinyinPrompts = {
  7520. normal: `Hãy tr v theo format sau, mi phn cách nhau bng du <|> và không có gii thích thêm:
  7521. Văn bn gc <|> pinyin có s tone (1-4) <|> bn dch sang ngôn ng có mã ngôn ng là '${targetLanguage}'
  7522. Văn bn cn x lý: "${text}"
  7523. Lưu ý:
  7524. - Nếu có t không phi là tiếng Trung, hãy tr v giá tr pinyin ca t đó là phiên âm ca t đó và theo ngôn ng đó (Nếu là tiếng Anh thì hay theo phiên âm ca US). Ví dụ: Hello <|> /heˈloʊ/ <|> Xin chào
  7525. - Bn dch phi hoàn toàn bng ngôn ng có mã ngôn ng là '${targetLanguage}', nhưng ví d khi dch sang tiếng Vit nếu gp nhng danh t riêng ch địa đim hoc tên riêng, có phm trù trong ngôn ng là t ghép ca 2 ngôn ng gi là t Hán Vit, hãy dch sang nghĩa t Hán Vit như Dip Trn, Lc 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à dch hn sang nghĩa tiếng Vit là Lá Trn, Rng kiếm, Tri kiếp, núi Rng Ngng Đầu,...
  7526. - Ch tr v bn dch theo format trên và không gii thích thêm.`,
  7527. advanced: `Dch và phân tích t khóa: "${text}"`,
  7528. ocr: `Hãy tr v theo format sau, mi phn cách nhau bng du <|> và không có gii thích thêm:
  7529. Văn bn gc <|> pinyin có s tone (1-4) <|> bn dch sang ngôn ng có mã ngôn ng là '${targetLanguage}'
  7530. Đọc hiu tht kĩ và x lý toàn b văn bn trong hình nh.
  7531. Lưu ý:
  7532. - Nếu có t không phi là tiếng Trung, hãy tr v giá tr pinyin ca t đó là phiên âm ca t đó và theo ngôn ng đó (Nếu là tiếng Anh thì hay theo phiên âm ca US). Ví dụ: Hello <|> /heˈloʊ/ <|> Xin chào
  7533. - Bn dch phi hoàn toàn bng ngôn ng có mã ngôn ng là '${targetLanguage}', nhưng ví d khi dch sang tiếng Vit nếu gp nhng danh t riêng ch địa đim hoc tên riêng, có phm trù trong ngôn ng là t ghép ca 2 ngôn ng gi là t Hán Vit, hãy dch sang nghĩa t Hán Vit như Dip Trn, Lc 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à dch hn sang nghĩa tiếng Vit là Lá Trn, Rng kiếm, Tri kiếp, núi Rng Ngng Đầu,...
  7534. - Ch tr v bn dch theo format trên, mi 1 cm theo format s 1 dòng và không gii thích thêm.`,
  7535. media: `${share_media}
  7536. - Định dng bn dch ca bn theo định dng SRT phi đảm bo rng mi đon hi thoi được đánh s th tự, có thi gian bt đầu và kết thúc, có dòng văn bn gc và dòng văn bn dch.
  7537. - Không cn gii thích thêm.`,
  7538. page: `Hãy tr v theo format sau, mi phn cách nhau bng du <|> và không có gii thích thêm:
  7539. Văn bn gc <|> pinyin có s tone (1-4) <|> bn dch sang ngôn ng có mã ngôn ng là '${targetLanguage}'
  7540. Dch tht t nhiên, đúng ng cnh, gi nguyên định dng phông ch ban đầu. Nếu có t không phi là tiếng Trung, hãy tr v phiên âm ca t đó theo ngôn ng ca t đó.
  7541. Văn bn cn x lý: "${text}"
  7542. Lưu ý:
  7543. - Nếu có t không phi là tiếng Trung, hãy tr v giá tr pinyin ca t đó là phiên âm ca t đó và theo ngôn ng đó (Nếu là tiếng Anh thì hay theo phiên âm ca US). Ví dụ: Hello <|> /heˈloʊ/ <|> Xin chào
  7544. - Bn dch phi hoàn toàn bng ngôn ng có mã ngôn ng là '${targetLanguage}', nhưng ví d khi dch sang tiếng Vit nếu gp nhng danh t riêng ch địa đim hoc tên riêng, có phm trù trong ngôn ng là t ghép ca 2 ngôn ng gi là t Hán Vit, hãy dch sang nghĩa t Hán Vit như Dip Trn, Lc 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à dch hn sang nghĩa tiếng Vit là Lá Trn, Rng kiếm, Tri kiếp, núi Rng Ngng Đầu,...
  7545. - Ch tr v bn dch theo format trên và không gii thích thêm.`,
  7546. };
  7547. return isPinyinMode ? pinyinPrompts[type] : basePrompts[type];
  7548. }
  7549. showSettingsUI() {
  7550. const settingsUI = this.userSettings.createSettingsUI();
  7551. document.body.appendChild(settingsUI);
  7552. }
  7553. handleError(error, targetElement) {
  7554. console.error("Translation failed:", error);
  7555. const message = error.message.includes("Rate limit")
  7556. ? "Vui lòng chờ giữa các lần dịch"
  7557. : error.message.includes("Gemini API")
  7558. ? "Lỗi Gemini: " + error.message
  7559. : error.message.includes("API Key")
  7560. ? "Lỗi xác thực API"
  7561. : "Lỗi dịch thuật: " + error.message;
  7562. this.ui.showTranslationBelow(targetElement, message);
  7563. }
  7564. }
  7565. function debounce(func, wait) {
  7566. let timeout;
  7567. return function executedFunction(...args) {
  7568. const later = () => {
  7569. clearTimeout(timeout);
  7570. func(...args);
  7571. };
  7572. clearTimeout(timeout);
  7573. timeout = setTimeout(later, wait);
  7574. };
  7575. }
  7576. GM_registerMenuCommand("Cài đặt Translator AI", () => {
  7577. const translator = window.translator;
  7578. if (translator) {
  7579. const settingsUI = translator.userSettings.createSettingsUI();
  7580. document.body.appendChild(settingsUI);
  7581. }
  7582. });
  7583. const translator = new Translator();
  7584. })();