King Translator AI

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