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-05-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name King Translator AI
  3. // @namespace https://kingsmanvn.pages.dev
  4. // @version 4.4
  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/lz-string/1.4.4/lz-string.min.js
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
  21. // @require https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js
  22. // @require https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js
  23. // @homepageURL https://github.com/king1x32/UserScripts
  24. // ==/UserScript==
  25. (function() {
  26. "use strict";
  27. const CONFIG = {
  28. API: {
  29. providers: {
  30. gemini: {
  31. baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
  32. models: {
  33. fast: [
  34. "gemini-2.5-flash-preview-04-17",
  35. "gemini-2.0-flash-live-001",
  36. "gemini-2.0-flash-lite",
  37. "gemini-2.0-flash-exp",
  38. "gemini-2.0-flash",
  39. ],
  40. pro: ["gemini-2.5-pro-exp-03-25", "gemini-2.0-pro-exp-02-05", "gemini-2.0-pro-exp"],
  41. vision: [
  42. "gemini-2.0-flash-thinking-exp-01-21",
  43. "gemini-2.0-flash-thinking-exp",
  44. ],
  45. },
  46. headers: { "Content-Type": "application/json" },
  47. body: (prompt) => ({
  48. contents: [
  49. {
  50. parts: [{ text: prompt }],
  51. },
  52. ],
  53. generationConfig: { temperature: 0.7 },
  54. }),
  55. responseParser: (response) => {
  56. if (typeof response === "string") {
  57. return response;
  58. }
  59. if (response?.candidates?.[0]?.content?.parts?.[0]?.text) {
  60. return response.candidates[0].content.parts[0].text;
  61. }
  62. throw new Error("Không thể đọc kết quả từ API");
  63. },
  64. },
  65. openai: {
  66. url: () => "https://api.groq.com/openai/v1/chat/completions",
  67. headers: (apiKey) => ({
  68. "Content-Type": "application/json",
  69. Authorization: `Bearer ${apiKey}`,
  70. }),
  71. body: (prompt) => ({
  72. model: "llama-3.3-70b-versatile",
  73. messages: [{ role: "user", content: prompt }],
  74. temperature: 0.7,
  75. }),
  76. responseParser: (response) => response.choices?.[0]?.message?.content,
  77. },
  78. },
  79. currentProvider: "gemini",
  80. apiKey: {
  81. gemini: [""],
  82. openai: [""],
  83. },
  84. currentKeyIndex: {
  85. gemini: 0,
  86. openai: 0,
  87. },
  88. maxRetries: 3,
  89. retryDelay: 1000,
  90. },
  91. OCR: {
  92. generation: {
  93. temperature: 0.2,
  94. topP: 0.7,
  95. topK: 20,
  96. },
  97. maxFileSize: 15 * 1024 * 1024, // 15MB
  98. supportedFormats: [
  99. "image/jpeg",
  100. "image/png",
  101. "image/webp",
  102. "image/heic",
  103. "image/heif",
  104. ],
  105. },
  106. MEDIA: {
  107. generation: {
  108. temperature: 0.2,
  109. topP: 0.7,
  110. topK: 20,
  111. },
  112. audio: {
  113. maxSize: 100 * 1024 * 1024, // 100MB
  114. supportedFormats: [
  115. "audio/wav",
  116. "audio/mp3",
  117. "audio/ogg",
  118. "audio/m4a",
  119. "audio/aac",
  120. "audio/flac",
  121. "audio/wma",
  122. "audio/opus",
  123. "audio/amr",
  124. "audio/midi",
  125. "audio/mpa",
  126. ],
  127. },
  128. video: {
  129. maxSize: 200 * 1024 * 1024, // 200MB
  130. supportedFormats: [
  131. "video/mp4",
  132. "video/webm",
  133. "video/ogg",
  134. "video/x-msvideo",
  135. "video/quicktime",
  136. "video/x-ms-wmv",
  137. "video/x-flv",
  138. "video/3gpp",
  139. "video/3gpp2",
  140. "video/x-matroska",
  141. ],
  142. },
  143. },
  144. VIDEO_STREAMING: {
  145. enabled: true,
  146. supportedSites: [
  147. 'youtube.com',
  148. // 'netflix.com',
  149. // 'udemy.com', // Thêm Udemy
  150. // 'coursera.org', // Thêm Coursera
  151. ],
  152. styles: {
  153. subtitleContainer: {
  154. position: 'absolute',
  155. bottom: '2%',
  156. left: '50%',
  157. transform: 'translateX(-50%)',
  158. textAlign: 'center',
  159. zIndex: 2147483647,
  160. padding: '5px 10px',
  161. borderRadius: '5px',
  162. backgroundColor: 'rgba(0,0,0,0.7)',
  163. color: 'white',
  164. fontSize: 'clamp(1rem, 1.5cqw, 2.5rem)',
  165. fontFamily: 'Arial, sans-serif',
  166. textShadow: '2px 2px 2px rgba(0,0,0,0.5)',
  167. maxWidth: '90%'
  168. }
  169. }
  170. },
  171. contextMenu: {
  172. enabled: true,
  173. },
  174. pageTranslation: {
  175. enabled: true,
  176. autoTranslate: false,
  177. showInitialButton: false, // Hiện nút dịch ban đầu
  178. buttonTimeout: 10000, // Thời gian hiển thị nút (10 giây)
  179. useCustomSelectors: false,
  180. customSelectors: [],
  181. defaultSelectors: [
  182. "script",
  183. "code",
  184. "style",
  185. "input",
  186. "button",
  187. "textarea",
  188. ".notranslate",
  189. ".translator-settings-container",
  190. ".translator-tools-container",
  191. ".translation-div",
  192. ".draggable",
  193. ".page-translate-button",
  194. ".translator-tools-dropdown",
  195. ".translator-notification",
  196. ".translator-content",
  197. ".translator-context-menu",
  198. ".translator-overlay",
  199. ".translator-guide",
  200. ".center-translate-status",
  201. ".no-translate",
  202. "[data-notranslate]",
  203. "[translate='no']",
  204. ".html5-player-chrome",
  205. ".html5-video-player",
  206. ],
  207. generation: {
  208. temperature: 0.2,
  209. topP: 0.9,
  210. topK: 40
  211. }
  212. },
  213. promptSettings: {
  214. enabled: true,
  215. customPrompts: {
  216. normal: "",
  217. advanced: "",
  218. chinese: "",
  219. ocr: "",
  220. media: "",
  221. page: "",
  222. },
  223. useCustom: false,
  224. },
  225. CACHE: {
  226. text: {
  227. maxSize: 100, // Tối đa 100 entries cho text
  228. expirationTime: 300000, // 5 phút
  229. },
  230. image: {
  231. maxSize: 25, // Tối đa 25 entries cho ảnh
  232. expirationTime: 1800000, // 30 phút
  233. },
  234. media: {
  235. maxSize: 25, // Số lượng media được cache tối đa
  236. expirationTime: 1800000, // 30 phút
  237. },
  238. },
  239. RATE_LIMIT: {
  240. maxRequests: 5,
  241. perMilliseconds: 10000,
  242. },
  243. THEME: {
  244. mode: "dark",
  245. light: {
  246. background: "#cccccc",
  247. backgroundShadow: "rgba(255, 255, 255, 0.05)",
  248. text: "#333333",
  249. border: "#bbb",
  250. title: "#202020",
  251. content: "#555",
  252. button: {
  253. close: { background: "#ff4444", text: "#ddd" },
  254. translate: { background: "#007BFF", text: "#ddd" },
  255. },
  256. },
  257. dark: {
  258. background: "#333333",
  259. backgroundShadow: "rgba(0, 0, 0, 0.05)",
  260. text: "#cccccc",
  261. border: "#555",
  262. title: "#eeeeee",
  263. content: "#bbb",
  264. button: {
  265. close: { background: "#aa2222", text: "#ddd" },
  266. translate: { background: "#004a99", text: "#ddd" },
  267. },
  268. },
  269. },
  270. STYLES: {
  271. translation: {
  272. marginTop: "10px",
  273. padding: "10px",
  274. backgroundColor: "#f0f0f0",
  275. borderLeft: "3px solid #4CAF50",
  276. borderRadius: "8px",
  277. color: "#333",
  278. position: "relative",
  279. fontFamily: "SF Pro Rounded, sans-serif",
  280. fontSize: "16px",
  281. zIndex: "2147483647",
  282. },
  283. popup: {
  284. position: "fixed",
  285. border: "1px solid",
  286. padding: "20px",
  287. zIndex: "2147483647",
  288. maxWidth: "90vw",
  289. minWidth: "300px",
  290. maxHeight: "80vh",
  291. boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
  292. borderRadius: "15px",
  293. fontFamily: "SF Pro Rounded, Arial, sans-serif",
  294. fontSize: "16px",
  295. top: `${window.innerHeight / 2}px`,
  296. left: `${window.innerWidth / 2}px`,
  297. transform: "translate(-50%, -50%)",
  298. display: "flex",
  299. flexDirection: "column",
  300. overflowY: "auto",
  301. },
  302. button: {
  303. position: "fixed",
  304. border: "none",
  305. borderRadius: "8px",
  306. padding: "5px 10px",
  307. cursor: "pointer",
  308. zIndex: "2147483647",
  309. fontSize: "14px",
  310. },
  311. dragHandle: {
  312. padding: "10px",
  313. borderBottom: "1px solid",
  314. cursor: "move",
  315. userSelect: "none",
  316. display: "flex",
  317. justifyContent: "space-between",
  318. alignItems: "center",
  319. borderTopLeftRadius: "15px",
  320. borderTopRightRadius: "15px",
  321. zIndex: "2147483647",
  322. },
  323. },
  324. };
  325. const DEFAULT_SETTINGS = {
  326. theme: CONFIG.THEME.mode,
  327. apiProvider: CONFIG.API.currentProvider,
  328. apiKey: {
  329. gemini: [""],
  330. openai: [""],
  331. },
  332. currentKeyIndex: {
  333. gemini: 0,
  334. openai: 0,
  335. },
  336. geminiOptions: {
  337. modelType: "fast", // 'fast', 'pro', 'vision', 'custom'
  338. fastModel: "gemini-2.0-flash-lite",
  339. proModel: "gemini-2.0-pro-exp-02-05",
  340. visionModel: "gemini-2.0-flash-thinking-exp-01-21",
  341. customModel: "",
  342. },
  343. contextMenu: {
  344. enabled: true,
  345. },
  346. promptSettings: {
  347. enabled: true,
  348. customPrompts: {
  349. normal: "",
  350. advanced: "",
  351. chinese: "",
  352. ocr: "",
  353. media: "",
  354. page: "",
  355. },
  356. useCustom: false,
  357. },
  358. inputTranslation: {
  359. enabled: true,
  360. excludeSelectors: [],
  361. },
  362. pageTranslation: {
  363. enabled: true,
  364. autoTranslate: false,
  365. showInitialButton: false, // Hiện nút dịch ban đầu
  366. buttonTimeout: 10000, // Thời gian hiển thị nút (10 giây)
  367. useCustomSelectors: false,
  368. customSelectors: [],
  369. defaultSelectors: [
  370. "script",
  371. "code",
  372. "style",
  373. "input",
  374. "button",
  375. "textarea",
  376. ".notranslate",
  377. ".translator-settings-container",
  378. ".translator-tools-container",
  379. ".translation-div",
  380. ".draggable",
  381. ".page-translate-button",
  382. ".translator-tools-dropdown",
  383. ".translator-notification",
  384. ".translator-content",
  385. ".translator-context-menu",
  386. ".translator-overlay",
  387. ".translator-guide",
  388. ".center-translate-status",
  389. ".no-translate",
  390. "[data-notranslate]",
  391. "[translate='no']",
  392. ".html5-player-chrome",
  393. ".html5-video-player",
  394. ],
  395. generation: {
  396. temperature: 0.2,
  397. topP: 0.9,
  398. topK: 40
  399. }
  400. },
  401. ocrOptions: {
  402. enabled: true,
  403. preferredProvider: CONFIG.API.currentProvider,
  404. displayType: "popup",
  405. maxFileSize: CONFIG.OCR.maxFileSize,
  406. temperature: CONFIG.OCR.generation.temperature,
  407. topP: CONFIG.OCR.generation.topP,
  408. topK: CONFIG.OCR.generation.topK,
  409. },
  410. mediaOptions: {
  411. enabled: true,
  412. temperature: CONFIG.MEDIA.generation.temperature,
  413. topP: CONFIG.MEDIA.generation.topP,
  414. topK: CONFIG.MEDIA.generation.topK,
  415. audio: {
  416. processingInterval: 2000, // 2 seconds
  417. bufferSize: 16384,
  418. format: {
  419. sampleRate: 44100,
  420. numChannels: 1,
  421. bitsPerSample: 16,
  422. },
  423. },
  424. },
  425. videoStreamingOptions: {
  426. enabled: true,
  427. fontSize: 'clamp(1rem, 1.5cqw, 2.5rem)',
  428. backgroundColor: 'rgba(0,0,0,0.7)',
  429. textColor: 'white'
  430. },
  431. displayOptions: {
  432. fontSize: "16px",
  433. minPopupWidth: "300px",
  434. maxPopupWidth: "90vw",
  435. webImageTranslation: {
  436. fontSize: "9px",
  437. minFontSize: "8px",
  438. maxFontSize: "16px",
  439. },
  440. translationMode: "translation_only", // 'translation_only', 'parallel' hoặc 'language_learning'
  441. sourceLanguage: "auto", // 'auto' hoặc 'zh','en','vi',...
  442. targetLanguage: "vi", // 'vi', 'en', 'zh', 'ko', 'ja',...
  443. languageLearning: {
  444. showSource: true,
  445. },
  446. },
  447. shortcuts: {
  448. settingsEnabled: true,
  449. enabled: true,
  450. pageTranslate: { key: "f", altKey: true },
  451. inputTranslate: { key: "t", altKey: true },
  452. quickTranslate: { key: "q", altKey: true },
  453. popupTranslate: { key: "e", altKey: true },
  454. advancedTranslate: { key: "a", altKey: true },
  455. },
  456. clickOptions: {
  457. enabled: true,
  458. singleClick: { translateType: "popup" },
  459. doubleClick: { translateType: "quick" },
  460. hold: { translateType: "advanced" },
  461. },
  462. touchOptions: {
  463. enabled: true,
  464. sensitivity: 100,
  465. twoFingers: { translateType: "popup" },
  466. threeFingers: { translateType: "advanced" },
  467. fourFingers: { translateType: "quick" },
  468. },
  469. cacheOptions: {
  470. text: {
  471. enabled: true,
  472. maxSize: CONFIG.CACHE.text.maxSize,
  473. expirationTime: CONFIG.CACHE.text.expirationTime,
  474. },
  475. image: {
  476. enabled: true,
  477. maxSize: CONFIG.CACHE.image.maxSize,
  478. expirationTime: CONFIG.CACHE.image.expirationTime,
  479. },
  480. media: {
  481. enabled: true,
  482. maxSize: CONFIG.CACHE.media.maxSize,
  483. expirationTime: CONFIG.CACHE.media.expirationTime,
  484. },
  485. },
  486. rateLimit: {
  487. maxRequests: CONFIG.RATE_LIMIT.maxRequests,
  488. perMilliseconds: CONFIG.RATE_LIMIT.perMilliseconds,
  489. },
  490. };
  491. class MobileOptimizer {
  492. constructor(ui) {
  493. this.ui = ui;
  494. this.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  495. if (this.isMobile) {
  496. this.optimizeForMobile();
  497. }
  498. }
  499. optimizeForMobile() {
  500. this.reduceDOMOperations();
  501. this.optimizeTouchHandling();
  502. this.adjustUIForMobile();
  503. }
  504. reduceDOMOperations() {
  505. const observer = new MutationObserver((mutations) => {
  506. requestAnimationFrame(() => {
  507. mutations.forEach((mutation) => {
  508. if (mutation.type === "childList") {
  509. this.optimizeAddedNodes(mutation.addedNodes);
  510. }
  511. });
  512. });
  513. });
  514. observer.observe(document.body, {
  515. childList: true,
  516. subtree: true,
  517. });
  518. }
  519. optimizeTouchHandling() {
  520. let touchStartY = 0;
  521. let touchStartX = 0;
  522. document.addEventListener(
  523. "touchstart",
  524. (e) => {
  525. touchStartY = e.touches[0].clientY;
  526. touchStartX = e.touches[0].clientX;
  527. },
  528. { passive: true }
  529. );
  530. document.addEventListener(
  531. "touchmove",
  532. (e) => {
  533. const touchY = e.touches[0].clientY;
  534. const touchX = e.touches[0].clientX;
  535. if (
  536. Math.abs(touchY - touchStartY) > 10 ||
  537. Math.abs(touchX - touchStartX) > 10
  538. ) {
  539. this.ui.removeTranslateButton();
  540. }
  541. },
  542. { passive: true }
  543. );
  544. }
  545. adjustUIForMobile() {
  546. const style = document.createElement("style");
  547. style.textContent = `
  548. .translator-tools-container {
  549. bottom: 25px;
  550. right: 5px;
  551. }
  552. .translator-tools-button {
  553. padding: 8px 15px;
  554. font-size: 14px;
  555. }
  556. .translator-tools-dropdown {
  557. min-width: 205px;
  558. max-height: 60vh;
  559. overflow-y: auto;
  560. }
  561. .translator-tools-item {
  562. padding: 10px;
  563. }
  564. .draggable {
  565. max-width: 95vw;
  566. max-height: 80vh;
  567. }
  568. `;
  569. this.ui.shadowRoot.appendChild(style);
  570. }
  571. optimizeAddedNodes(nodes) {
  572. nodes.forEach((node) => {
  573. if (node.nodeType === Node.ELEMENT_NODE) {
  574. const images = node.getElementsByTagName("img");
  575. Array.from(images).forEach((img) => {
  576. if (!img.loading) img.loading = "lazy";
  577. });
  578. }
  579. });
  580. }
  581. }
  582. // const bypassCSP = () => {
  583. // const style = document.createElement("style");
  584. // style.textContent = `
  585. // .translator-tools-container {
  586. // position: fixed;
  587. // bottom: 40px;
  588. // right: 25px;
  589. // z-index: 2147483647;
  590. // font-family: Arial, sans-serif;
  591. // display: block;
  592. // visibility: visible;
  593. // opacity: 1;
  594. // }
  595. // `;
  596. // this.shadowRoot.appendChild(style);
  597. // };
  598. class UserSettings {
  599. constructor(translator) {
  600. this.translator = translator;
  601. this.settings = this.loadSettings();
  602. this.isSettingsUIOpen = false;
  603. }
  604. createSettingsUI() {
  605. if (this.isSettingsUIOpen) {
  606. return;
  607. }
  608. this.isSettingsUIOpen = true;
  609. const container = document.createElement("div");
  610. const themeMode = this.settings.theme ? this.settings.theme : CONFIG.THEME.mode;
  611. const theme = CONFIG.THEME[themeMode];
  612. const isDark = themeMode === "dark";
  613. const geminiModels = {
  614. fast: CONFIG.API.providers.gemini.models.fast || [],
  615. pro: CONFIG.API.providers.gemini.models.pro || [],
  616. vision: CONFIG.API.providers.gemini.models.vision || [],
  617. };
  618. const resetStyle = `
  619. * {
  620. all: revert;
  621. box-sizing: border-box;
  622. font-family: Arial, sans-serif;
  623. margin: 0;
  624. padding: 0;
  625. }
  626. .settings-grid {
  627. display: grid;
  628. grid-template-columns: 47% 53%;
  629. align-items: center;
  630. gap: 10px;
  631. margin-bottom: 8px;
  632. }
  633. .settings-label {
  634. min-width: 100px;
  635. text-align: left;
  636. padding-right: 10px;
  637. }
  638. .settings-input {
  639. min-width: 100px;
  640. margin-left: 5px;
  641. }
  642. h2 {
  643. flex: 1;
  644. display: flex;
  645. font-family: Arial, sans-serif;
  646. align-items: center;
  647. justify-content: center;
  648. margin-bottom: 15px;
  649. font-weight: bold;
  650. color: ${theme.title};
  651. grid-column: 1 / -1;
  652. }
  653. h3 {
  654. font-family: Arial, sans-serif;
  655. margin-bottom: 15px;
  656. font-weight: bold;
  657. color: ${theme.title};
  658. grid-column: 1 / -1;
  659. }
  660. h4 {
  661. color: ${theme.title};
  662. }
  663. input[type="radio"],
  664. input[type="checkbox"] {
  665. align-items: center;
  666. justify-content: center;
  667. }
  668. button {
  669. font-family: Arial, sans-serif;
  670. font-size: 14px;
  671. background-color: ${isDark ? "#444" : "#ddd"};
  672. color: ${isDark ? "#ddd" : "#000"};
  673. padding: 5px 15px;
  674. border-radius: 8px;
  675. cursor: pointer;
  676. border: none;
  677. margin: 5px;
  678. }
  679. #cancelSettings {
  680. background-color: ${isDark ? "#666" : "#ddd"};
  681. color: ${isDark ? "#ddd" : "#000"};
  682. padding: 5px 15px;
  683. border-radius: 8px;
  684. cursor: pointer;
  685. border: none;
  686. margin: 5px;
  687. }
  688. #cancelSettings:hover {
  689. background-color: ${isDark ? "#888" : "#aaa"};
  690. }
  691. #saveSettings {
  692. background-color: #007BFF;
  693. padding: 5px 15px;
  694. border-radius: 8px;
  695. cursor: pointer;
  696. border: none;
  697. margin: 5px;
  698. }
  699. #saveSettings:hover {
  700. background-color: #009ddd;
  701. }
  702. button {
  703. font-family: Arial, sans-serif;
  704. font-size: 14px;
  705. border: none;
  706. border-radius: 8px;
  707. cursor: pointer;
  708. transition: all 0.2s ease;
  709. font-weight: 500;
  710. letter-spacing: 0.3px;
  711. }
  712. button:hover {
  713. transform: translateY(-2px);
  714. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  715. }
  716. button:active {
  717. transform: translateY(0);
  718. }
  719. #exportSettings:hover {
  720. background-color: #218838;
  721. }
  722. #importSettings:hover {
  723. background-color: #138496;
  724. }
  725. #cancelSettings:hover {
  726. background-color: ${isDark ? "#777" : "#dae0e5"};
  727. }
  728. #saveSettings:hover {
  729. background-color: #0056b3;
  730. }
  731. @keyframes buttonPop {
  732. 0% { transform: scale(1); }
  733. 50% { transform: scale(0.98); }
  734. 100% { transform: scale(1); }
  735. }
  736. button:active {
  737. animation: buttonPop 0.2s ease;
  738. }
  739. .radio-group {
  740. display: flex;
  741. gap: 15px;
  742. }
  743. .radio-group label {
  744. flex: 1;
  745. display: flex;
  746. color: ${isDark ? "#ddd" : "#000"};
  747. align-items: center;
  748. justify-content: center;
  749. padding: 5px;
  750. }
  751. .radio-group input[type="radio"] {
  752. margin-right: 5px;
  753. }
  754. .shortcut-container {
  755. display: flex;
  756. align-items: center;
  757. gap: 8px;
  758. }
  759. .shortcut-prefix {
  760. white-space: nowrap;
  761. color: ${isDark ? "#aaa" : "#555"};
  762. font-size: 14px;
  763. min-width: 45px;
  764. }
  765. .shortcut-input {
  766. flex: 1;
  767. min-width: 60px;
  768. max-width: 100px;
  769. }
  770. .prompt-textarea {
  771. width: 100%;
  772. min-height: 100px;
  773. margin: 5px 0;
  774. padding: 8px;
  775. background-color: ${isDark ? "#444" : "#fff"};
  776. color: ${isDark ? "#fff" : "#000"};
  777. border: 1px solid ${isDark ? "#666" : "#ccc"};
  778. border-radius: 8px;
  779. font-family: monospace;
  780. font-size: 13px;
  781. resize: vertical;
  782. }
  783. `;
  784. const styleElement = document.createElement("style");
  785. styleElement.textContent = resetStyle;
  786. container.appendChild(styleElement);
  787. container.innerHTML += `
  788. <h2 style="position: sticky; top: 0; background-color: ${theme.background}; padding: 20px; margin: 0; z-index: 2147483647; border-bottom: 1px solid ${theme.border}; border-radius: 15px 15px 0 0;">Cài đặt King Translator AI</h2>
  789. <div style="margin-bottom: 15px;">
  790. <h3>GIAO DIN</h3>
  791. <div class="radio-group">
  792. <label>
  793. <input type="radio" name="theme" value="light" ${!isDark ? "checked" : ""
  794. }>
  795. <span class="settings-label">Sáng</span>
  796. </label>
  797. <label>
  798. <input type="radio" name="theme" value="dark" ${isDark ? "checked" : ""}>
  799. <span class="settings-label">Ti</span>
  800. </label>
  801. </div>
  802. </div>
  803. <div style="margin-bottom: 15px;">
  804. <h3>API PROVIDER</h3>
  805. <div class="radio-group">
  806. <label>
  807. <input type="radio" name="apiProvider" value="gemini" ${this.settings.apiProvider === "gemini" ? "checked" : ""
  808. }>
  809. <span class="settings-label">Gemini</span>
  810. </label>
  811. <label>
  812. <input type="radio" name="apiProvider" value="openai" disabled>
  813. <span class="settings-label">OpenAI</span>
  814. </label>
  815. </div>
  816. </div>
  817. <div style="margin-bottom: 15px;">
  818. <h3>API KEYS</h3>
  819. <div id="geminiKeys" style="margin-bottom: 10px;">
  820. <h4 class="settings-label" style="margin-bottom: 5px;">Gemini API Keys</h4>
  821. <div class="api-keys-container">
  822. ${this.settings.apiKey.gemini
  823. .map(
  824. (key) => `
  825. <div class="api-key-entry" style="display: flex; gap: 10px; margin-bottom: 5px;">
  826. <input type="text" class="gemini-key" value="${key}" style="flex: 1; width: 100%; border-radius: 6px; margin-left: 5px;">
  827. <button class="remove-key" data-provider="gemini" data-index="${this.settings.apiKey.gemini.indexOf(
  828. key
  829. )}" style="background-color: #ff4444;">×</button>
  830. </div>
  831. `
  832. )
  833. .join("")}
  834. </div>
  835. <button id="addGeminiKey" class="settings-label" style="background-color: #28a745; margin-top: 5px;">+ Add Gemini Key</button>
  836. </div>
  837. <div id="openaiKeys" style="margin-bottom: 10px;">
  838. <h4 class="settings-label" style="margin-bottom: 5px;">OpenAI API Keys</h4>
  839. <div class="api-keys-container">
  840. ${this.settings.apiKey.openai
  841. .map(
  842. (key) => `
  843. <div class="api-key-entry" style="display: flex; gap: 10px; margin-bottom: 5px;">
  844. <input type="text" class="openai-key" value="${key}" style="flex: 1; width: 100%; border-radius: 6px; margin-left: 5px;">
  845. <button class="remove-key" data-provider="openai" data-index="${this.settings.apiKey.openai.indexOf(
  846. key
  847. )}" style="background-color: #ff4444;">×</button>
  848. </div>
  849. `
  850. )
  851. .join("")}
  852. </div>
  853. <button id="addOpenaiKey" class="settings-label" style="background-color: #28a745; margin-top: 5px;">+ Add OpenAI Key</button>
  854. </div>
  855. </div>
  856. <div style="margin-bottom: 15px;">
  857. <h3>MODEL GEMINI</h3>
  858. <div class="settings-grid">
  859. <span class="settings-label">S dng loi model:</span>
  860. <select id="geminiModelType" class="settings-input">
  861. <option value="fast" ${this.settings.geminiOptions?.modelType === "fast" ? "selected" : ""
  862. }>Nhanh</option>
  863. <option value="pro" ${this.settings.geminiOptions?.modelType === "pro" ? "selected" : ""
  864. }>Pro</option>
  865. <option value="vision" ${this.settings.geminiOptions?.modelType === "vision" ? "selected" : ""
  866. }>Suy lun</option>
  867. <option value="custom" ${this.settings.geminiOptions?.modelType === "custom" ? "selected" : ""
  868. }>Tùy chnh</option>
  869. </select>
  870. </div>
  871. <div id="fastModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "fast"
  872. ? 'style="display: none;"'
  873. : ""
  874. }>
  875. <span class="settings-label">Model Nhanh:</span>
  876. <select id="fastModel" class="settings-input">
  877. ${geminiModels.fast
  878. .map(
  879. (model) => `
  880. <option value="${model}" ${this.settings.geminiOptions?.fastModel === model ? "selected" : ""
  881. }>${model}</option>
  882. `
  883. )
  884. .join("")}
  885. </select>
  886. </div>
  887. <div id="proModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "pro"
  888. ? 'style="display: none;"'
  889. : ""
  890. }>
  891. <span class="settings-label">Model Chuyên nghip:</span>
  892. <select id="proModel" class="settings-input">
  893. ${geminiModels.pro
  894. .map(
  895. (model) => `
  896. <option value="${model}" ${this.settings.geminiOptions?.proModel === model ? "selected" : ""
  897. }>${model}</option>
  898. `
  899. )
  900. .join("")}
  901. </select>
  902. </div>
  903. <div id="visionModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "vision"
  904. ? 'style="display: none;"'
  905. : ""
  906. }>
  907. <span class="settings-label">Model Suy lun:</span>
  908. <select id="visionModel" class="settings-input">
  909. ${geminiModels.vision
  910. .map(
  911. (model) => `
  912. <option value="${model}" ${this.settings.geminiOptions?.visionModel === model ? "selected" : ""
  913. }>${model}</option>
  914. `
  915. )
  916. .join("")}
  917. </select>
  918. </div>
  919. <div id="customModelContainer" class="settings-grid" ${this.settings.geminiOptions?.modelType !== "custom"
  920. ? 'style="display: none;"'
  921. : ""
  922. }>
  923. <span class="settings-label">Model tùy chnh:</span>
  924. <input type="text" id="customModel" class="settings-input" value="${this.settings.geminiOptions?.customModel || ""
  925. }"
  926. placeholder="Nhập tên model">
  927. </div>
  928. </div>
  929. <div style="margin-bottom: 15px;">
  930. <h3>DCH KHI VIT</h3>
  931. <div class="settings-grid">
  932. <span class="settings-label">Bt tính năng:</span>
  933. <input type="checkbox" id="inputTranslationEnabled"
  934. ${this.settings.inputTranslation?.enabled ? "checked" : ""}>
  935. </div>
  936. </div>
  937. <div style="margin-bottom: 15px;">
  938. <h3>TOOLS DCH</h3>
  939. <div class="settings-grid">
  940. <span class="settings-label">Hin th Tools ⚙️</span>
  941. <input type="checkbox" id="showTranslatorTools"
  942. ${localStorage.getItem("translatorToolsEnabled") === "true"
  943. ? "checked"
  944. : ""
  945. }>
  946. </div>
  947. </div>
  948. <div style="margin-bottom: 15px;">
  949. <h3>DCH TOÀN TRANG</h3>
  950. <div class="settings-grid">
  951. <span class="settings-label">Bt tính năng dch trang:</span>
  952. <input type="checkbox" id="pageTranslationEnabled" ${this.settings.pageTranslation?.enabled ? "checked" : ""
  953. }>
  954. </div>
  955. <div class="settings-grid">
  956. <span class="settings-label">Hin nút dch 10s đầu:</span>
  957. <input type="checkbox" id="showInitialButton" ${this.settings.pageTranslation?.showInitialButton ? "checked" : ""
  958. }>
  959. </div>
  960. <div class="settings-grid">
  961. <span class="settings-label">T động dch trang:</span>
  962. <input type="checkbox" id="autoTranslatePage" ${this.settings.pageTranslation?.autoTranslate ? "checked" : ""
  963. }>
  964. </div>
  965. <div class="settings-grid">
  966. <span class="settings-label">Tùy chnh Selectors loi trừ:</span>
  967. <input type="checkbox" id="useCustomSelectors" ${this.settings.pageTranslation?.useCustomSelectors ? "checked" : ""
  968. }>
  969. </div>
  970. <div id="selectorsSettings" style="display: ${this.settings.pageTranslation?.useCustomSelectors ? "block" : "none"
  971. }">
  972. <div class="settings-grid" style="align-items: start;">
  973. <span class="settings-label">Selectors loi trừ:</span>
  974. <div style="flex: 1;">
  975. <textarea id="customSelectors"
  976. style="width: 100%; min-height: 100px; margin: 5px 0; padding: 8px;
  977. background-color: ${isDark ? "#444" : "#fff"};
  978. color: ${isDark ? "#fff" : "#000"};
  979. border: 1px solid ${isDark ? "#666" : "#ccc"};
  980. border-radius: 8px;
  981. font-family: monospace;
  982. font-size: 13px;"
  983. >${this.settings.pageTranslation?.customSelectors?.join("\n") || ""
  984. }</textarea>
  985. <div style="font-size: 12px; color: ${isDark ? "#999" : "#666"
  986. }; margin-top: 4px;">
  987. Hãy nhp mi selector mt dòng!
  988. </div>
  989. </div>
  990. </div>
  991. <div class="settings-grid" style="align-items: start;">
  992. <span class="settings-label">Selectors mc định:</span>
  993. <div style="flex: 1;">
  994. <textarea id="defaultSelectors" readonly
  995. style="width: 100%; min-height: 100px; margin: 5px 0; padding: 8px;
  996. background-color: ${isDark ? "#333" : "#f5f5f5"};
  997. color: ${isDark ? "#999" : "#666"};
  998. border: 1px solid ${isDark ? "#555" : "#ddd"};
  999. border-radius: 8px;
  1000. font-family: monospace;
  1001. font-size: 13px;"
  1002. >${this.settings.pageTranslation?.defaultSelectors?.join("\n") || ""
  1003. }</textarea>
  1004. <div style="font-size: 12px; color: ${isDark ? "#999" : "#666"
  1005. }; margin-top: 4px;">
  1006. Đây là danh sách selectors mc định s được s dng khi tt tùy chnh.
  1007. </div>
  1008. </div>
  1009. </div>
  1010. <div class="settings-grid">
  1011. <span class="settings-label">Kết hp vi mc định:</span>
  1012. <input type="checkbox" id="combineWithDefault" ${this.settings.pageTranslation?.combineWithDefault ? "checked" : ""
  1013. }>
  1014. <div style="font-size: 12px; color: ${isDark ? "#999" : "#666"
  1015. }; margin-top: 4px; grid-column: 2;">
  1016. 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.
  1017. </div>
  1018. </div>
  1019. </div>
  1020. <div class="settings-grid">
  1021. <span class="settings-label">Temperature:</span>
  1022. <input type="number" id="pageTranslationTemperature" class="settings-input"
  1023. value="${this.settings.pageTranslation.generation.temperature}"
  1024. min="0" max="1" step="0.1">
  1025. </div>
  1026. <div class="settings-grid">
  1027. <span class="settings-label">Top P:</span>
  1028. <input type="number" id="pageTranslationTopP" class="settings-input"
  1029. value="${this.settings.pageTranslation.generation.topP}"
  1030. min="0" max="1" step="0.1">
  1031. </div>
  1032. <div class="settings-grid">
  1033. <span class="settings-label">Top K:</span>
  1034. <input type="number" id="pageTranslationTopK" class="settings-input"
  1035. value="${this.settings.pageTranslation.generation.topK}"
  1036. min="1" max="100" step="1">
  1037. </div>
  1038. </div>
  1039. <div style="margin-bottom: 15px;">
  1040. <h3>TÙY CHNH PROMPT</h3>
  1041. <div class="settings-grid">
  1042. <span class="settings-label">S dng prompt tùy chnh:</span>
  1043. <input type="checkbox" id="useCustomPrompt" ${this.settings.promptSettings?.useCustom ? "checked" : ""
  1044. }>
  1045. </div>
  1046. <div id="promptSettings" style="display: ${this.settings.promptSettings?.useCustom ? "block" : "none"
  1047. }">
  1048. <!-- Normal prompts -->
  1049. <div class="settings-grid" style="align-items: start;">
  1050. <span class="settings-label">Prompt dch thường (nhanh + popup):</span>
  1051. <textarea id="normalPrompt" class="prompt-textarea"
  1052. placeholder="Nhập prompt cho dịch thường..."
  1053. >${this.settings.promptSettings?.customPrompts?.normal || ""}</textarea>
  1054. </div>
  1055. <div class="settings-grid" style="align-items: start;">
  1056. <span class="settings-label">Prompt dch thường (nhanh + popup)(Chinese):</span>
  1057. <textarea id="normalPrompt_chinese" class="prompt-textarea"
  1058. placeholder="Nhập prompt cho dịch thường với pinyin..."
  1059. >${this.settings.promptSettings?.customPrompts?.normal_chinese || ""
  1060. }</textarea>
  1061. </div>
  1062. <!-- Advanced prompts -->
  1063. <div class="settings-grid" style="align-items: start;">
  1064. <span class="settings-label">Prompt dch nâng cao:</span>
  1065. <textarea id="advancedPrompt" class="prompt-textarea"
  1066. placeholder="Nhập prompt cho dịch nâng cao..."
  1067. >${this.settings.promptSettings?.customPrompts?.advanced || ""}</textarea>
  1068. </div>
  1069. <div class="settings-grid" style="align-items: start;">
  1070. <span class="settings-label">Prompt dch nâng cao (Chinese):</span>
  1071. <textarea id="advancedPrompt_chinese" class="prompt-textarea"
  1072. placeholder="Nhập prompt cho dịch nâng cao với pinyin..."
  1073. >${this.settings.promptSettings?.customPrompts?.advanced_chinese || ""
  1074. }</textarea>
  1075. </div>
  1076. <!-- OCR prompts -->
  1077. <div class="settings-grid" style="align-items: start;">
  1078. <span class="settings-label">Prompt OCR:</span>
  1079. <textarea id="ocrPrompt" class="prompt-textarea"
  1080. placeholder="Nhập prompt cho OCR..."
  1081. >${this.settings.promptSettings?.customPrompts?.ocr || ""}</textarea>
  1082. </div>
  1083. <div class="settings-grid" style="align-items: start;">
  1084. <span class="settings-label">Prompt OCR (Chinese):</span>
  1085. <textarea id="ocrPrompt_chinese" class="prompt-textarea"
  1086. placeholder="Nhập prompt cho OCR với pinyin..."
  1087. >${this.settings.promptSettings?.customPrompts?.ocr_chinese || ""
  1088. }</textarea>
  1089. </div>
  1090. <!-- Media prompts -->
  1091. <div class="settings-grid" style="align-items: start;">
  1092. <span class="settings-label">Prompt Media:</span>
  1093. <textarea id="mediaPrompt" class="prompt-textarea"
  1094. placeholder="Nhập prompt cho media..."
  1095. >${this.settings.promptSettings?.customPrompts?.media || ""}</textarea>
  1096. </div>
  1097. <div class="settings-grid" style="align-items: start;">
  1098. <span class="settings-label">Prompt Media (Chinese):</span>
  1099. <textarea id="mediaPrompt_chinese" class="prompt-textarea"
  1100. placeholder="Nhập prompt cho media với pinyin..."
  1101. >${this.settings.promptSettings?.customPrompts?.media_chinese || ""
  1102. }</textarea>
  1103. </div>
  1104. <!-- Page prompts -->
  1105. <div class="settings-grid" style="align-items: start;">
  1106. <span class="settings-label">Prompt dch trang:</span>
  1107. <textarea id="pagePrompt" class="prompt-textarea"
  1108. placeholder="Nhập prompt cho dịch trang..."
  1109. >${this.settings.promptSettings?.customPrompts?.page || ""}</textarea>
  1110. </div>
  1111. <div class="settings-grid" style="align-items: start;">
  1112. <span class="settings-label">Prompt dch trang (Chinese):</span>
  1113. <textarea id="pagePrompt_chinese" class="prompt-textarea"
  1114. placeholder="Nhập prompt cho dịch trang với pinyin..."
  1115. >${this.settings.promptSettings?.customPrompts?.page_chinese || ""
  1116. }</textarea>
  1117. </div>
  1118. <div style="margin-top: 10px; font-size: 12px; color: ${isDark ? "#999" : "#666"
  1119. };">
  1120. Các biến có th s dng trong prompt:
  1121. <ul style="margin-left: 20px;">
  1122. <li>{text} - Văn bn cn dch</li>
  1123. <li>{targetLang} - Ngôn ng đích</li>
  1124. <li>{sourceLang} - Ngôn ng ngun (nếu có)</li>
  1125. </ul>
  1126. </div>
  1127. </div>
  1128. </div>
  1129. <div style="margin-bottom: 15px;">
  1130. <h3>DCH VĂN BN TRONG NH</h3>
  1131. <div class="settings-grid">
  1132. <span class="settings-label">Bt OCR dch:</span>
  1133. <input type="checkbox" id="ocrEnabled" ${this.settings.ocrOptions?.enabled ? "checked" : ""
  1134. }>
  1135. </div>
  1136. <div class="settings-grid">
  1137. <span class="settings-label">Temperature:</span>
  1138. <input type="number" id="ocrTemperature" class="settings-input" value="${this.settings.ocrOptions.temperature
  1139. }"
  1140. min="0" max="1" step="0.1">
  1141. </div>
  1142. <div class="settings-grid">
  1143. <span class="settings-label">Top P:</span>
  1144. <input type="number" id="ocrTopP" class="settings-input" value="${this.settings.ocrOptions.topP
  1145. }" min="0" max="1"
  1146. step="0.1">
  1147. </div>
  1148. <div class="settings-grid">
  1149. <span class="settings-label">Top K:</span>
  1150. <input type="number" id="ocrTopK" class="settings-input" value="${this.settings.ocrOptions.topK
  1151. }" min="1"
  1152. max="100" step="1">
  1153. </div>
  1154. </div>
  1155. <div style="margin-bottom: 15px;">
  1156. <h3>DCH MEDIA</h3>
  1157. <div class="settings-grid">
  1158. <span class="settings-label">Bt dch Media:</span>
  1159. <input type="checkbox" id="mediaEnabled" ${this.settings.mediaOptions.enabled ? "checked" : ""
  1160. }>
  1161. </div>
  1162. <div class="settings-grid">
  1163. <span class="settings-label">Temperature:</span>
  1164. <input type="number" id="mediaTemperature" class="settings-input"
  1165. value="${this.settings.mediaOptions.temperature
  1166. }" min="0" max="1" step="0.1">
  1167. </div>
  1168. <div class="settings-grid">
  1169. <span class="settings-label">Top P:</span>
  1170. <input type="number" id="mediaTopP" class="settings-input" value="${this.settings.mediaOptions.topP
  1171. }" min="0"
  1172. max="1" step="0.1">
  1173. </div>
  1174. <div class="settings-grid">
  1175. <span class="settings-label">Top K:</span>
  1176. <input type="number" id="mediaTopK" class="settings-input" value="${this.settings.mediaOptions.topK
  1177. }" min="1"
  1178. max="100" step="1">
  1179. </div>
  1180. </div>
  1181. <div style="margin-bottom: 15px;">
  1182. <h3>DCH PH ĐỀ VIDEO TRC TUYN</h3>
  1183. <div class="settings-grid">
  1184. <span class="settings-label">Bt tính năng:</span>
  1185. <input type="checkbox" id="videoStreamingEnabled"
  1186. ${this.settings.videoStreamingOptions?.enabled ? "checked" : ""}>
  1187. </div>
  1188. <div class="settings-grid">
  1189. <span class="settings-label">C chữ:</span>
  1190. <input type="text" id="videoStreamingFontSize" class="settings-input"
  1191. value="${this.settings.videoStreamingOptions?.fontSize || "20px"}">
  1192. </div>
  1193. <div class="settings-grid">
  1194. <span class="settings-label">Màu nn:</span>
  1195. <input type="text" id="videoStreamingBgColor" class="settings-input"
  1196. value="${this.settings.videoStreamingOptions?.backgroundColor || "rgba(0,0,0,0.7)"}">
  1197. </div>
  1198. <div class="settings-grid">
  1199. <span class="settings-label">Màu chữ:</span>
  1200. <input type="text" id="videoStreamingTextColor" class="settings-input"
  1201. value="${this.settings.videoStreamingOptions?.textColor || "white"}">
  1202. </div>
  1203. </div>
  1204. <div style="margin-bottom: 15px;">
  1205. <h3>HIN THỊ</h3>
  1206. <div class="settings-grid">
  1207. <span class="settings-label">Chế độ hin thị:</span>
  1208. <select id="displayMode" class="settings-input">
  1209. <option value="translation_only" ${this.settings.displayOptions.translationMode === "translation_only"
  1210. ? "selected"
  1211. : ""
  1212. }>Ch hin bn dch</option>
  1213. <option value="parallel" ${this.settings.displayOptions.translationMode === "parallel"
  1214. ? "selected"
  1215. : ""
  1216. }>Song song văn bn gc và bn dch</option>
  1217. <option value="language_learning" ${this.settings.displayOptions.translationMode === "language_learning"
  1218. ? "selected"
  1219. : ""
  1220. }>Chế độ hc ngôn ngữ</option>
  1221. </select>
  1222. </div>
  1223. <div id="languageLearningOptions" style="display: ${this.settings.displayOptions.translationMode === "language_learning"
  1224. ? "block"
  1225. : "none"
  1226. }">
  1227. <div id="sourceOption" class="settings-grid">
  1228. <span class="settings-label">Hin bn gc:</span>
  1229. <input type="checkbox" id="showSource" ${this.settings.displayOptions.languageLearning.showSource
  1230. ? "checked"
  1231. : ""
  1232. }>
  1233. </div>
  1234. </div>
  1235. <div class="settings-grid">
  1236. <span class="settings-label">Ngôn ng ngun:</span>
  1237. <select id="sourceLanguage" class="settings-input">
  1238. <option value="auto" ${this.settings.displayOptions.sourceLanguage === "auto" ? "selected" : ""
  1239. }>T động nhn din</option>
  1240. <option value="en" ${this.settings.displayOptions.sourceLanguage === "en" ? "selected" : ""
  1241. }>Tiếng Anh</option>
  1242. <option value="zh" ${this.settings.displayOptions.sourceLanguage === "zh" ? "selected" : ""
  1243. }>Tiếng Trung</option>
  1244. <option value="ko" ${this.settings.displayOptions.sourceLanguage === "ko" ? "selected" : ""
  1245. }>Tiếng Hàn</option>
  1246. <option value="ja" ${this.settings.displayOptions.sourceLanguage === "ja" ? "selected" : ""
  1247. }>Tiếng Nht</option>
  1248. <option value="fr" ${this.settings.displayOptions.sourceLanguage === "fr" ? "selected" : ""
  1249. }>Tiếng Pháp</option>
  1250. <option value="de" ${this.settings.displayOptions.sourceLanguage === "de" ? "selected" : ""
  1251. }>Tiếng Đức</option>
  1252. <option value="es" ${this.settings.displayOptions.sourceLanguage === "es" ? "selected" : ""
  1253. }>Tiếng Tây Ban Nha</option>
  1254. <option value="it" ${this.settings.displayOptions.sourceLanguage === "it" ? "selected" : ""
  1255. }>Tiếng Ý</option>
  1256. <option value="pt" ${this.settings.displayOptions.sourceLanguage === "pt" ? "selected" : ""
  1257. }>Tiếng B Đào Nha</option>
  1258. <option value="ru" ${this.settings.displayOptions.sourceLanguage === "ru" ? "selected" : ""
  1259. }>Tiếng Nga</option>
  1260. <option value="ar" ${this.settings.displayOptions.sourceLanguage === "ar" ? "selected" : ""
  1261. }>Tiếng Rp</option>
  1262. <option value="hi" ${this.settings.displayOptions.sourceLanguage === "hi" ? "selected" : ""
  1263. }>Tiếng Hindi</option>
  1264. <option value="bn" ${this.settings.displayOptions.sourceLanguage === "bn" ? "selected" : ""
  1265. }>Tiếng Bengal</option>
  1266. <option value="id" ${this.settings.displayOptions.sourceLanguage === "id" ? "selected" : ""
  1267. }>Tiếng Indonesia</option>
  1268. <option value="ms" ${this.settings.displayOptions.sourceLanguage === "ms" ? "selected" : ""
  1269. }>Tiếng Malaysia</option>
  1270. <option value="th" ${this.settings.displayOptions.sourceLanguage === "th" ? "selected" : ""
  1271. }>Tiếng Thái</option>
  1272. <option value="tr" ${this.settings.displayOptions.sourceLanguage === "tr" ? "selected" : ""
  1273. }>Tiếng Th Nhĩ Kỳ</option>
  1274. <option value="nl" ${this.settings.displayOptions.sourceLanguage === "nl" ? "selected" : ""
  1275. }>Tiếng Hà Lan</option>
  1276. <option value="pl" ${this.settings.displayOptions.sourceLanguage === "pl" ? "selected" : ""
  1277. }>Tiếng Ba Lan</option>
  1278. <option value="uk" ${this.settings.displayOptions.sourceLanguage === "uk" ? "selected" : ""
  1279. }>Tiếng Ukraine</option>
  1280. <option value="el" ${this.settings.displayOptions.sourceLanguage === "el" ? "selected" : ""
  1281. }>Tiếng Hy Lp</option>
  1282. <option value="cs" ${this.settings.displayOptions.sourceLanguage === "cs" ? "selected" : ""
  1283. }>Tiếng Séc</option>
  1284. <option value="da" ${this.settings.displayOptions.sourceLanguage === "da" ? "selected" : ""
  1285. }>Tiếng Đan Mch</option>
  1286. <option value="fi" ${this.settings.displayOptions.sourceLanguage === "fi" ? "selected" : ""
  1287. }>Tiếng Phn Lan</option>
  1288. <option value="he" ${this.settings.displayOptions.sourceLanguage === "he" ? "selected" : ""
  1289. }>Tiếng Do Thái</option>
  1290. <option value="hu" ${this.settings.displayOptions.sourceLanguage === "hu" ? "selected" : ""
  1291. }>Tiếng Hungary</option>
  1292. <option value="no" ${this.settings.displayOptions.sourceLanguage === "no" ? "selected" : ""
  1293. }>Tiếng Na Uy</option>
  1294. <option value="ro" ${this.settings.displayOptions.sourceLanguage === "ro" ? "selected" : ""
  1295. }>Tiếng Romania</option>
  1296. <option value="sv" ${this.settings.displayOptions.sourceLanguage === "sv" ? "selected" : ""
  1297. }>Tiếng Thy Đin</option>
  1298. <option value="ur" ${this.settings.displayOptions.sourceLanguage === "ur" ? "selected" : ""
  1299. }>Tiếng Urdu</option>
  1300. <option value="vi" ${this.settings.displayOptions.sourceLanguage === "vi" ? "selected" : ""
  1301. }>Tiếng Vit</option>
  1302. </select>
  1303. </div>
  1304. <div class="settings-grid">
  1305. <span class="settings-label">Ngôn ng đích:</span>
  1306. <select id="targetLanguage" class="settings-input">
  1307. <option value="vi" ${this.settings.displayOptions.targetLanguage === "vi" ? "selected" : ""
  1308. }>Tiếng Vit</option>
  1309. <option value="en" ${this.settings.displayOptions.targetLanguage === "en" ? "selected" : ""
  1310. }>Tiếng Anh</option>
  1311. <option value="zh" ${this.settings.displayOptions.targetLanguage === "zh" ? "selected" : ""
  1312. }>Tiếng Trung</option>
  1313. <option value="ko" ${this.settings.displayOptions.targetLanguage === "ko" ? "selected" : ""
  1314. }>Tiếng Hàn</option>
  1315. <option value="ja" ${this.settings.displayOptions.targetLanguage === "ja" ? "selected" : ""
  1316. }>Tiếng Nht</option>
  1317. <option value="fr" ${this.settings.displayOptions.targetLanguage === "fr" ? "selected" : ""
  1318. }>Tiếng Pháp</option>
  1319. <option value="de" ${this.settings.displayOptions.targetLanguage === "de" ? "selected" : ""
  1320. }>Tiếng Đức</option>
  1321. <option value="es" ${this.settings.displayOptions.targetLanguage === "es" ? "selected" : ""
  1322. }>Tiếng Tây Ban Nha</option>
  1323. <option value="it" ${this.settings.displayOptions.targetLanguage === "it" ? "selected" : ""
  1324. }>Tiếng Ý</option>
  1325. <option value="pt" ${this.settings.displayOptions.targetLanguage === "pt" ? "selected" : ""
  1326. }>Tiếng B Đào Nha</option>
  1327. <option value="ru" ${this.settings.displayOptions.targetLanguage === "ru" ? "selected" : ""
  1328. }>Tiếng Nga</option>
  1329. <option value="ar" ${this.settings.displayOptions.targetLanguage === "ar" ? "selected" : ""
  1330. }>Tiếng Rp</option>
  1331. <option value="hi" ${this.settings.displayOptions.targetLanguage === "hi" ? "selected" : ""
  1332. }>Tiếng Hindi</option>
  1333. <option value="bn" ${this.settings.displayOptions.targetLanguage === "bn" ? "selected" : ""
  1334. }>Tiếng Bengal</option>
  1335. <option value="id" ${this.settings.displayOptions.targetLanguage === "id" ? "selected" : ""
  1336. }>Tiếng Indonesia</option>
  1337. <option value="ms" ${this.settings.displayOptions.targetLanguage === "ms" ? "selected" : ""
  1338. }>Tiếng Malaysia</option>
  1339. <option value="th" ${this.settings.displayOptions.targetLanguage === "th" ? "selected" : ""
  1340. }>Tiếng Thái</option>
  1341. <option value="tr" ${this.settings.displayOptions.targetLanguage === "tr" ? "selected" : ""
  1342. }>Tiếng Th Nhĩ Kỳ</option>
  1343. <option value="nl" ${this.settings.displayOptions.targetLanguage === "nl" ? "selected" : ""
  1344. }>Tiếng Hà Lan</option>
  1345. <option value="pl" ${this.settings.displayOptions.targetLanguage === "pl" ? "selected" : ""
  1346. }>Tiếng Ba Lan</option>
  1347. <option value="uk" ${this.settings.displayOptions.targetLanguage === "uk" ? "selected" : ""
  1348. }>Tiếng Ukraine</option>
  1349. <option value="el" ${this.settings.displayOptions.targetLanguage === "el" ? "selected" : ""
  1350. }>Tiếng Hy Lp</option>
  1351. <option value="cs" ${this.settings.displayOptions.targetLanguage === "cs" ? "selected" : ""
  1352. }>Tiếng Séc</option>
  1353. <option value="da" ${this.settings.displayOptions.targetLanguage === "da" ? "selected" : ""
  1354. }>Tiếng Đan Mch</option>
  1355. <option value="fi" ${this.settings.displayOptions.targetLanguage === "fi" ? "selected" : ""
  1356. }>Tiếng Phn Lan</option>
  1357. <option value="he" ${this.settings.displayOptions.targetLanguage === "he" ? "selected" : ""
  1358. }>Tiếng Do Thái</option>
  1359. <option value="hu" ${this.settings.displayOptions.targetLanguage === "hu" ? "selected" : ""
  1360. }>Tiếng Hungary</option>
  1361. <option value="no" ${this.settings.displayOptions.targetLanguage === "no" ? "selected" : ""
  1362. }>Tiếng Na Uy</option>
  1363. <option value="ro" ${this.settings.displayOptions.targetLanguage === "ro" ? "selected" : ""
  1364. }>Tiếng Romania</option>
  1365. <option value="sv" ${this.settings.displayOptions.targetLanguage === "sv" ? "selected" : ""
  1366. }>Tiếng Thy Đin</option>
  1367. <option value="ur" ${this.settings.displayOptions.targetLanguage === "ur" ? "selected" : ""
  1368. }>Tiếng Urdu</option>
  1369. </select>
  1370. </div>
  1371. <div class="settings-grid">
  1372. <span class="settings-label">C ch dch nh web:</span>
  1373. <select id="webImageFontSize" class="settings-input">
  1374. <option value="8px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "8px"
  1375. ? "selected"
  1376. : ""
  1377. }>Rt nh (8px)</option>
  1378. <option value="9px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "9px"
  1379. ? "selected"
  1380. : ""
  1381. }>Nh (9px)</option>
  1382. <option value="10px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "10px"
  1383. ? "selected"
  1384. : ""
  1385. }>Va (10px)</option>
  1386. <option value="12px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "12px"
  1387. ? "selected"
  1388. : ""
  1389. }>Ln (12px)</option>
  1390. <option value="14px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "14px"
  1391. ? "selected"
  1392. : ""
  1393. }>Rt ln (14px)</option>
  1394. <option value="16px" ${this.settings.displayOptions?.webImageTranslation?.fontSize === "16px"
  1395. ? "selected"
  1396. : ""
  1397. }>Siêu ln (16px)</option>
  1398. </select>
  1399. </div>
  1400. <div class="settings-grid">
  1401. <span class="settings-label">C ch dch popup:</span>
  1402. <select id="fontSize" class="settings-input">
  1403. <option value="12px" ${this.settings.displayOptions?.fontSize === "12px" ? "selected" : ""
  1404. }>Nh (12px)</option>
  1405. <option value="14px" ${this.settings.displayOptions?.fontSize === "14px" ? "selected" : ""
  1406. }>Hơi nh (14px)
  1407. </option>
  1408. <option value="16px" ${this.settings.displayOptions?.fontSize === "16px" ? "selected" : ""
  1409. }>Va (16px)</option>
  1410. <option value="18px" ${this.settings.displayOptions?.fontSize === "18px" ? "selected" : ""
  1411. }>Hơi ln (18px)
  1412. </option>
  1413. <option value="20px" ${this.settings.displayOptions?.fontSize === "20px" ? "selected" : ""
  1414. }>Ln (20px)</option>
  1415. <option value="22px" ${this.settings.displayOptions?.fontSize === "22px" ? "selected" : ""
  1416. }>Cc ln (22px)
  1417. </option>
  1418. <option value="24px" ${this.settings.displayOptions?.fontSize === "24px" ? "selected" : ""
  1419. }>Siêu ln (24px)
  1420. </option>
  1421. </select>
  1422. </div>
  1423. <div class="settings-grid">
  1424. <span class="settings-label">Độ rng ti thiu (popup):</span>
  1425. <select id="minPopupWidth" class="settings-input">
  1426. <option value="100px" ${this.settings.displayOptions?.minPopupWidth === "100px"
  1427. ? "selected"
  1428. : ""
  1429. }>Rt nh
  1430. (100px)</option>
  1431. <option value="200px" ${this.settings.displayOptions?.minPopupWidth === "200px"
  1432. ? "selected"
  1433. : ""
  1434. }>Hơi nh
  1435. (200px)</option>
  1436. <option value="300px" ${this.settings.displayOptions?.minPopupWidth === "300px"
  1437. ? "selected"
  1438. : ""
  1439. }>Nh (300px)
  1440. </option>
  1441. <option value="400px" ${this.settings.displayOptions?.minPopupWidth === "400px"
  1442. ? "selected"
  1443. : ""
  1444. }>Va (400px)
  1445. </option>
  1446. <option value="500px" ${this.settings.displayOptions?.minPopupWidth === "500px"
  1447. ? "selected"
  1448. : ""
  1449. }>Hơi ln
  1450. (500px)</option>
  1451. <option value="600px" ${this.settings.displayOptions?.minPopupWidth === "600px"
  1452. ? "selected"
  1453. : ""
  1454. }>Ln (600px)
  1455. </option>
  1456. <option value="700px" ${this.settings.displayOptions?.minPopupWidth === "700px"
  1457. ? "selected"
  1458. : ""
  1459. }>Cc ln
  1460. (700px)</option>
  1461. <option value="800px" ${this.settings.displayOptions?.minPopupWidth === "800px"
  1462. ? "selected"
  1463. : ""
  1464. }>Siêu ln
  1465. (800px)</option>
  1466. </select>
  1467. </div>
  1468. <div class="settings-grid">
  1469. <span class="settings-label">Độ rng ti đa (popup):</span>
  1470. <select id="maxPopupWidth" class="settings-input">
  1471. <option value="30vw" ${this.settings.displayOptions?.maxPopupWidth === "30vw" ? "selected" : ""
  1472. }>30% màn hình
  1473. </option>
  1474. <option value="40vw" ${this.settings.displayOptions?.maxPopupWidth === "40vw" ? "selected" : ""
  1475. }>40% màn hình
  1476. </option>
  1477. <option value="50vw" ${this.settings.displayOptions?.maxPopupWidth === "50vw" ? "selected" : ""
  1478. }>50% màn hình
  1479. </option>
  1480. <option value="60vw" ${this.settings.displayOptions?.maxPopupWidth === "60vw" ? "selected" : ""
  1481. }>60% màn hình
  1482. </option>
  1483. <option value="70vw" ${this.settings.displayOptions?.maxPopupWidth === "70vw" ? "selected" : ""
  1484. }>70% màn hình
  1485. </option>
  1486. <option value="80vw" ${this.settings.displayOptions?.maxPopupWidth === "80vw" ? "selected" : ""
  1487. }>80% màn hình
  1488. </option>
  1489. <option value="90vw" ${this.settings.displayOptions?.maxPopupWidth === "90vw" ? "selected" : ""
  1490. }>90% màn hình
  1491. </option>
  1492. </select>
  1493. </div>
  1494. </div>
  1495. <div style="margin-bottom: 15px;">
  1496. <h3>CONTEXT MENU</h3>
  1497. <div class="settings-grid">
  1498. <span class="settings-label">Bt Context Menu:</span>
  1499. <input type="checkbox" id="contextMenuEnabled" ${this.settings.contextMenu?.enabled ? "checked" : ""
  1500. }>
  1501. </div>
  1502. </div>
  1503. <div style="margin-bottom: 15px;">
  1504. <h3>PHÍM TT</h3>
  1505. <div class="settings-grid">
  1506. <span class="settings-label">Bt phím tt m cài đặt:</span>
  1507. <input type="checkbox" id="settingsShortcutEnabled" ${this.settings.shortcuts?.settingsEnabled ? "checked" : ""
  1508. }>
  1509. </div>
  1510. <div class="settings-grid">
  1511. <span class="settings-label">Bt phím tt dch:</span>
  1512. <input type="checkbox" id="shortcutsEnabled" ${this.settings.shortcuts?.enabled ? "checked" : ""
  1513. }>
  1514. </div>
  1515. <div class="settings-grid">
  1516. <span class="settings-label">Dch trang:</span>
  1517. <div class="shortcut-container">
  1518. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1519. <input type="text" id="pageTranslateKey" class="shortcut-input settings-input"
  1520. value="${this.settings.shortcuts.pageTranslate.key}">
  1521. </div>
  1522. </div>
  1523. <div class="settings-grid">
  1524. <span class="settings-label">Dch text trong hp nhp:</span>
  1525. <div class="shortcut-container">
  1526. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1527. <input type="text" id="inputTranslationKey" class="shortcut-input settings-input"
  1528. value="${this.settings.shortcuts.inputTranslate.key}">
  1529. </div>
  1530. </div>
  1531. <div class="settings-grid">
  1532. <span class="settings-label">Dch nhanh:</span>
  1533. <div class="shortcut-container">
  1534. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1535. <input type="text" id="quickKey" class="shortcut-input settings-input"
  1536. value="${this.settings.shortcuts.quickTranslate.key}">
  1537. </div>
  1538. </div>
  1539. <div class="settings-grid">
  1540. <span class="settings-label">Dch popup:</span>
  1541. <div class="shortcut-container">
  1542. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1543. <input type="text" id="popupKey" class="shortcut-input settings-input"
  1544. value="${this.settings.shortcuts.popupTranslate.key}">
  1545. </div>
  1546. </div>
  1547. <div class="settings-grid">
  1548. <span class="settings-label">Dch nâng cao:</span>
  1549. <div class="shortcut-container">
  1550. <span class="shortcut-prefix">Cmd/Alt &nbsp+</span>
  1551. <input type="text" id="advancedKey" class="shortcut-input settings-input" value="${this.settings.shortcuts.advancedTranslate.key
  1552. }">
  1553. </div>
  1554. </div>
  1555. </div>
  1556. <div style="margin-bottom: 15px;">
  1557. <h3>NÚT DCH</h3>
  1558. <div class="settings-grid">
  1559. <span class="settings-label">Bt nút dch:</span>
  1560. <input type="checkbox" id="translationButtonEnabled" ${this.settings.clickOptions?.enabled ? "checked" : ""
  1561. }>
  1562. </div>
  1563. <div class="settings-grid">
  1564. <span class="settings-label">Nhp đơn:</span>
  1565. <select id="singleClickSelect" class="settings-input">
  1566. <option value="quick" ${this.settings.clickOptions.singleClick.translateType === "quick"
  1567. ? "selected"
  1568. : ""
  1569. }>Dch
  1570. nhanh</option>
  1571. <option value="popup" ${this.settings.clickOptions.singleClick.translateType === "popup"
  1572. ? "selected"
  1573. : ""
  1574. }>Dch
  1575. popup</option>
  1576. <option value="advanced" ${this.settings.clickOptions.singleClick.translateType === "advanced"
  1577. ? "selected"
  1578. : ""
  1579. }>Dch nâng cao</option>
  1580. </select>
  1581. </div>
  1582. <div class="settings-grid">
  1583. <span class="settings-label">Nhp đúp:</span>
  1584. <select id="doubleClickSelect" class="settings-input">
  1585. <option value="quick" ${this.settings.clickOptions.doubleClick.translateType === "quick"
  1586. ? "selected"
  1587. : ""
  1588. }>Dch
  1589. nhanh</option>
  1590. <option value="popup" ${this.settings.clickOptions.doubleClick.translateType === "popup"
  1591. ? "selected"
  1592. : ""
  1593. }>Dch
  1594. popup</option>
  1595. <option value="advanced" ${this.settings.clickOptions.doubleClick.translateType === "advanced"
  1596. ? "selected"
  1597. : ""
  1598. }>Dch nâng cao</option>
  1599. </select>
  1600. </div>
  1601. <div class="settings-grid">
  1602. <span class="settings-label">Gi nút:</span>
  1603. <select id="holdSelect" class="settings-input">
  1604. <option value="quick" ${this.settings.clickOptions.hold.translateType === "quick"
  1605. ? "selected"
  1606. : ""
  1607. }>Dch nhanh
  1608. </option>
  1609. <option value="popup" ${this.settings.clickOptions.hold.translateType === "popup"
  1610. ? "selected"
  1611. : ""
  1612. }>Dch popup
  1613. </option>
  1614. <option value="advanced" ${this.settings.clickOptions.hold.translateType === "advanced"
  1615. ? "selected"
  1616. : ""
  1617. }>Dch
  1618. nâng cao</option>
  1619. </select>
  1620. </div>
  1621. </div>
  1622. <div style="margin-bottom: 15px;">
  1623. <h3>CM NG ĐA ĐIM</h3>
  1624. <div class="settings-grid">
  1625. <span class="settings-label">Bt cm ng:</span>
  1626. <input type="checkbox" id="touchEnabled" ${this.settings.touchOptions?.enabled ? "checked" : ""
  1627. }>
  1628. </div>
  1629. <div class="settings-grid">
  1630. <span class="settings-label">Hai ngón tay:</span>
  1631. <select id="twoFingersSelect" class="settings-input">
  1632. <option value="quick" ${this.settings.touchOptions?.twoFingers?.translateType === "quick"
  1633. ? "selected"
  1634. : ""
  1635. }>
  1636. Dch nhanh</option>
  1637. <option value="popup" ${this.settings.touchOptions?.twoFingers?.translateType === "popup"
  1638. ? "selected"
  1639. : ""
  1640. }>
  1641. Dch popup</option>
  1642. <option value="advanced" ${this.settings.touchOptions?.twoFingers?.translateType === "advanced"
  1643. ? "selected"
  1644. : ""
  1645. }>Dch nâng cao</option>
  1646. </select>
  1647. </div>
  1648. <div class="settings-grid">
  1649. <span class="settings-label">Ba ngón tay:</span>
  1650. <select id="threeFingersSelect" class="settings-input">
  1651. <option value="quick" ${this.settings.touchOptions?.threeFingers?.translateType === "quick"
  1652. ? "selected"
  1653. : ""
  1654. }>
  1655. Dch nhanh</option>
  1656. <option value="popup" ${this.settings.touchOptions?.threeFingers?.translateType === "popup"
  1657. ? "selected"
  1658. : ""
  1659. }>
  1660. Dch popup</option>
  1661. <option value="advanced" ${this.settings.touchOptions?.threeFingers?.translateType === "advanced"
  1662. ? "selected"
  1663. : ""
  1664. }>Dch nâng cao</option>
  1665. </select>
  1666. </div>
  1667. <div class="settings-grid">
  1668. <span class="settings-label">Độ nhy (ms):</span>
  1669. <input type="number" id="touchSensitivity" class="settings-input"
  1670. value="${this.settings.touchOptions?.sensitivity || 100
  1671. }" min="50" max="350" step="50">
  1672. </div>
  1673. </div>
  1674. <div style="margin-bottom: 15px;">
  1675. <h3>RATE LIMIT</h3>
  1676. <div class="settings-grid">
  1677. <span class="settings-label">S yêu cu ti đa:</span>
  1678. <input type="number" id="maxRequests" class="settings-input" value="${this.settings.rateLimit?.maxRequests || CONFIG.RATE_LIMIT.maxRequests
  1679. }" min="1" max="50" step="1">
  1680. </div>
  1681. <div class="settings-grid">
  1682. <span class="settings-label">Thi gian ch (ms):</span>
  1683. <input type="number" id="perMilliseconds" class="settings-input" value="${this.settings.rateLimit?.perMilliseconds ||
  1684. CONFIG.RATE_LIMIT.perMilliseconds
  1685. }" min="1000" step="1000">
  1686. </div>
  1687. </div>
  1688. <div style="margin-bottom: 15px;">
  1689. <h3>CACHE</h3>
  1690. <div style="margin-bottom: 10px;">
  1691. <h4 style="color: ${isDark ? "#678" : "#333"
  1692. }; margin-bottom: 8px;">Text Cache</h4>
  1693. <div class="settings-grid">
  1694. <span class="settings-label">Bt cache text:</span>
  1695. <input type="checkbox" id="textCacheEnabled" ${this.settings.cacheOptions?.text?.enabled ? "checked" : ""
  1696. }>
  1697. </div>
  1698. <div class="settings-grid">
  1699. <span class="settings-label">Kích thước cache text:</span>
  1700. <input type="number" id="textCacheMaxSize" class="settings-input" value="${this.settings.cacheOptions?.text?.maxSize || CONFIG.CACHE.text.maxSize
  1701. }" min="10" max="1000">
  1702. </div>
  1703. <div class="settings-grid">
  1704. <span class="settings-label">Thi gian cache text (ms):</span>
  1705. <input type="number" id="textCacheExpiration" class="settings-input" value="${this.settings.cacheOptions?.text?.expirationTime ||
  1706. CONFIG.CACHE.text.expirationTime
  1707. }" min="60000" step="60000">
  1708. </div>
  1709. <div style="margin-bottom: 10px;">
  1710. <h4 style="color: ${isDark ? "#678" : "#333"
  1711. }; margin-bottom: 8px;">Image Cache</h4>
  1712. <div class="settings-grid">
  1713. <span class="settings-label">Bt cache nh:</span>
  1714. <input type="checkbox" id="imageCacheEnabled" ${this.settings.cacheOptions?.image?.enabled ? "checked" : ""
  1715. }>
  1716. </div>
  1717. <div class="settings-grid">
  1718. <span class="settings-label">Kích thước cache nh:</span>
  1719. <input type="number" id="imageCacheMaxSize" class="settings-input" value="${this.settings.cacheOptions?.image?.maxSize ||
  1720. CONFIG.CACHE.image.maxSize
  1721. }" min="10" max="100">
  1722. </div>
  1723. <div class="settings-grid">
  1724. <span class="settings-label">Thi gian cache nh (ms):</span>
  1725. <input type="number" id="imageCacheExpiration" class="settings-input" value="${this.settings.cacheOptions?.image?.expirationTime ||
  1726. CONFIG.CACHE.image.expirationTime
  1727. }" min="60000" step="60000">
  1728. </div>
  1729. </div>
  1730. <div style="margin-bottom: 10px;">
  1731. <h4 style="color: ${isDark ? "#678" : "#333"
  1732. }; margin-bottom: 8px;">Media Cache</h4>
  1733. <div class="settings-grid">
  1734. <span class="settings-label">Bt cache media:</span>
  1735. <input type="checkbox" id="mediaCacheEnabled" ${this.settings.cacheOptions.media?.enabled ? "checked" : ""
  1736. }>
  1737. </div>
  1738. <div class="settings-grid">
  1739. <span class="settings-label">Media cache entries:</span>
  1740. <input type="number" id="mediaCacheMaxSize" class="settings-input" value="${this.settings.cacheOptions.media?.maxSize ||
  1741. CONFIG.CACHE.media.maxSize
  1742. }" min="5" max="100">
  1743. </div>
  1744. <div class="settings-grid">
  1745. <span class="settings-label">Thi gian expire (giây):</span>
  1746. <input type="number" id="mediaCacheExpirationTime" class="settings-input" value="${this.settings.cacheOptions.media?.expirationTime / 1000 ||
  1747. CONFIG.CACHE.media.expirationTime / 1000
  1748. }" min="60000" step="60000">
  1749. </div>
  1750. </div>
  1751. </div>
  1752. </div>
  1753. <div style="border-top: 1px solid ${isDark ? "#444" : "#ddd"
  1754. }; margin-top: 20px; padding-top: 20px;">
  1755. <h3>SAO LƯU CÀI ĐẶT</h3>
  1756. <div style="display: flex; gap: 10px; margin-bottom: 15px;">
  1757. <button id="exportSettings" style="flex: 1; background-color: #28a745; min-width: 140px; height: 36px; display: flex; align-items: center; justify-content: center; gap: 8px;">
  1758. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1759. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  1760. <polyline points="7 10 12 15 17 10"/>
  1761. <line x1="12" y1="15" x2="12" y2="3"/>
  1762. </svg>
  1763. Xut cài đặt
  1764. </button>
  1765. <input type="file" id="importInput" accept=".json" style="display: none;">
  1766. <button id="importSettings" style="flex: 1; background-color: #17a2b8; min-width: 140px; height: 36px; display: flex; align-items: center; justify-content: center; gap: 8px;">
  1767. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1768. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  1769. <polyline points="17 8 12 3 7 8"/>
  1770. <line x1="12" y1="3" x2="12" y2="15"/>
  1771. </svg>
  1772. Nhp cài đặt
  1773. </button>
  1774. </div>
  1775. </div>
  1776. <div style="position: sticky; bottom: 0; background-color: ${theme.background}; padding: 20px; margin-top: 20px; border-top: 1px solid ${theme.border}; z-index: 2147483647; border-radius: 0 0 15px 15px;">
  1777. <div style="display: flex; gap: 10px; justify-content: flex-end;">
  1778. <button id="cancelSettings" style="min-width: 100px; height: 36px; background-color: ${isDark ? "#666" : "#e9ecef"
  1779. }; color: ${isDark ? "#fff" : "#333"};">
  1780. Hy
  1781. </button>
  1782. <button id="saveSettings" style="min-width: 100px; height: 36px; background-color: #007bff; color: white;">
  1783. Lưu
  1784. </button>
  1785. </div>
  1786. </div>
  1787. `;
  1788. container.className = "translator-settings-container";
  1789. const addGeminiKey = container.querySelector("#addGeminiKey");
  1790. const addOpenaiKey = container.querySelector("#addOpenaiKey");
  1791. const geminiContainer = container.querySelector(
  1792. "#geminiKeys .api-keys-container"
  1793. );
  1794. const openaiContainer = container.querySelector(
  1795. "#openaiKeys .api-keys-container"
  1796. );
  1797. addGeminiKey.addEventListener("click", () => {
  1798. const newEntry = document.createElement("div");
  1799. newEntry.className = "api-key-entry";
  1800. newEntry.style.cssText =
  1801. "display: flex; gap: 10px; margin-bottom: 5px;";
  1802. const currentKeysCount = geminiContainer.children.length;
  1803. newEntry.innerHTML = `
  1804. <input type="text" class="gemini-key" value="" style="flex: 1; width: 100%; border-radius: 6px; margin-left: 5px;">
  1805. <button class="remove-key" data-provider="gemini" data-index="${currentKeysCount}" style="background-color: #ff4444;">×</button>
  1806. `;
  1807. geminiContainer.appendChild(newEntry);
  1808. });
  1809. addOpenaiKey.addEventListener("click", () => {
  1810. const newEntry = document.createElement("div");
  1811. newEntry.className = "api-key-entry";
  1812. newEntry.style.cssText =
  1813. "display: flex; gap: 10px; margin-bottom: 5px;";
  1814. const currentKeysCount = openaiContainer.children.length;
  1815. newEntry.innerHTML = `
  1816. <input type="text" class="openai-key" value="" style="flex: 1; width: 100%; border-radius: 6px; margin-left: 5px;">
  1817. <button class="remove-key" data-provider="openai" data-index="${currentKeysCount}" style="background-color: #ff4444;">×</button>
  1818. `;
  1819. openaiContainer.appendChild(newEntry);
  1820. });
  1821. container.addEventListener("click", (e) => {
  1822. if (e.target.classList.contains("remove-key")) {
  1823. const provider = e.target.dataset.provider;
  1824. e.target.parentElement.remove();
  1825. const container = this.$(
  1826. `#${provider}Keys .api-keys-container`
  1827. );
  1828. Array.from(container.querySelectorAll(".remove-key")).forEach(
  1829. (btn, i) => {
  1830. btn.dataset.index = i;
  1831. }
  1832. );
  1833. }
  1834. });
  1835. const modelTypeSelect = container.querySelector("#geminiModelType");
  1836. const fastContainer = container.querySelector("#fastModelContainer");
  1837. const proContainer = container.querySelector("#proModelContainer");
  1838. const visionContainer = container.querySelector("#visionModelContainer");
  1839. const customContainer = container.querySelector("#customModelContainer");
  1840. modelTypeSelect.addEventListener("change", (e) => {
  1841. const selectedType = e.target.value;
  1842. fastContainer.style.display = selectedType === "fast" ? "" : "none";
  1843. proContainer.style.display = selectedType === "pro" ? "" : "none";
  1844. visionContainer.style.display = selectedType === "vision" ? "" : "none";
  1845. customContainer.style.display = selectedType === "custom" ? "" : "none";
  1846. });
  1847. const useCustomSelectors = container.querySelector("#useCustomSelectors");
  1848. const selectorsSettings = container.querySelector("#selectorsSettings");
  1849. useCustomSelectors.addEventListener("change", (e) => {
  1850. selectorsSettings.style.display = e.target.checked ? "block" : "none";
  1851. });
  1852. const useCustomPrompt = container.querySelector("#useCustomPrompt");
  1853. const promptSettings = container.querySelector("#promptSettings");
  1854. useCustomPrompt.addEventListener("change", (e) => {
  1855. promptSettings.style.display = e.target.checked ? "block" : "none";
  1856. });
  1857. const displayModeSelect = container.querySelector("#displayMode");
  1858. displayModeSelect.addEventListener("change", (e) => {
  1859. const languageLearningOptions = container.querySelector(
  1860. "#languageLearningOptions"
  1861. );
  1862. languageLearningOptions.style.display =
  1863. e.target.value === "language_learning" ? "block" : "none";
  1864. });
  1865. const handleEscape = (e) => {
  1866. if (e.key === "Escape") {
  1867. document.removeEventListener("keydown", handleEscape);
  1868. if (container && container.parentNode) {
  1869. container.parentNode.removeChild(container);
  1870. }
  1871. }
  1872. };
  1873. document.addEventListener("keydown", handleEscape);
  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: `${window.innerWidth / 2}px`,
  1981. transform: "translateX(-50%)",
  1982. backgroundColor,
  1983. color: textColor,
  1984. padding: "10px 20px",
  1985. borderRadius: "8px",
  1986. zIndex: "2147483647",
  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(), 3000);
  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. mediaOptions: {
  2045. ...DEFAULT_SETTINGS.mediaOptions,
  2046. ...(savedSettings?.mediaOptions || {}),
  2047. },
  2048. videoStreamingOptions: {
  2049. ...DEFAULT_SETTINGS.videoStreamingOptions,
  2050. ...(savedSettings?.videoStreamingOptions || {}),
  2051. },
  2052. displayOptions: {
  2053. ...DEFAULT_SETTINGS.displayOptions,
  2054. ...(savedSettings?.displayOptions || {}),
  2055. },
  2056. shortcuts: {
  2057. ...DEFAULT_SETTINGS.shortcuts,
  2058. ...(savedSettings?.shortcuts || {}),
  2059. },
  2060. clickOptions: {
  2061. ...DEFAULT_SETTINGS.clickOptions,
  2062. ...(savedSettings?.clickOptions || {}),
  2063. },
  2064. touchOptions: {
  2065. ...DEFAULT_SETTINGS.touchOptions,
  2066. ...(savedSettings?.touchOptions || {}),
  2067. },
  2068. cacheOptions: {
  2069. text: {
  2070. ...DEFAULT_SETTINGS.cacheOptions.text,
  2071. ...(savedSettings?.cacheOptions?.text || {}),
  2072. },
  2073. image: {
  2074. ...DEFAULT_SETTINGS.cacheOptions.image,
  2075. ...(savedSettings?.cacheOptions?.image || {}),
  2076. },
  2077. media: {
  2078. ...DEFAULT_SETTINGS.cacheOptions.media,
  2079. ...(savedSettings?.cacheOptions?.media || {}),
  2080. },
  2081. page: {
  2082. ...DEFAULT_SETTINGS.cacheOptions.page,
  2083. ...(savedSettings?.cacheOptions?.page || {}),
  2084. },
  2085. },
  2086. rateLimit: {
  2087. ...DEFAULT_SETTINGS.rateLimit,
  2088. ...(savedSettings?.rateLimit || {}),
  2089. },
  2090. };
  2091. }
  2092. saveSettings(settingsUI) {
  2093. const geminiKeys = Array.from(settingsUI.querySelectorAll(".gemini-key"))
  2094. .map((input) => input.value.trim())
  2095. .filter((key) => key !== "");
  2096. const openaiKeys = Array.from(settingsUI.querySelectorAll(".openai-key"))
  2097. .map((input) => input.value.trim())
  2098. .filter((key) => key !== "");
  2099. const useCustomSelectors = settingsUI.querySelector(
  2100. "#useCustomSelectors"
  2101. ).checked;
  2102. const customSelectors = settingsUI
  2103. .querySelector("#customSelectors")
  2104. .value.split("\n")
  2105. .map((s) => s.trim())
  2106. .filter((s) => s && s.length > 0);
  2107. const combineWithDefault = settingsUI.querySelector(
  2108. "#combineWithDefault"
  2109. ).checked;
  2110. const maxWidthVw = settingsUI.querySelector("#maxPopupWidth").value;
  2111. const maxWidthPx = (window.innerWidth * parseInt(maxWidthVw)) / 100;
  2112. const minWidthPx = parseInt(
  2113. settingsUI.querySelector("#minPopupWidth").value
  2114. );
  2115. const finalMinWidth =
  2116. minWidthPx > maxWidthPx
  2117. ? maxWidthVw
  2118. : settingsUI.querySelector("#minPopupWidth").value;
  2119. const newSettings = {
  2120. theme: settingsUI.querySelector('input[name="theme"]:checked').value,
  2121. apiProvider: settingsUI.querySelector(
  2122. 'input[name="apiProvider"]:checked'
  2123. ).value,
  2124. apiKey: {
  2125. gemini:
  2126. geminiKeys.length > 0
  2127. ? geminiKeys
  2128. : [DEFAULT_SETTINGS.apiKey.gemini[0]],
  2129. openai:
  2130. openaiKeys.length > 0
  2131. ? openaiKeys
  2132. : [DEFAULT_SETTINGS.apiKey.openai[0]],
  2133. },
  2134. currentKeyIndex: {
  2135. gemini: 0,
  2136. openai: 0,
  2137. },
  2138. geminiOptions: {
  2139. modelType: settingsUI.querySelector("#geminiModelType").value,
  2140. fastModel: settingsUI.querySelector("#fastModel").value,
  2141. proModel: settingsUI.querySelector("#proModel").value,
  2142. visionModel: settingsUI.querySelector("#visionModel").value,
  2143. customModel: settingsUI.querySelector("#customModel").value,
  2144. },
  2145. contextMenu: {
  2146. enabled: settingsUI.querySelector("#contextMenuEnabled").checked,
  2147. },
  2148. inputTranslation: {
  2149. enabled: settingsUI.querySelector("#inputTranslationEnabled").checked,
  2150. },
  2151. promptSettings: {
  2152. enabled: true,
  2153. useCustom: settingsUI.querySelector("#useCustomPrompt").checked,
  2154. customPrompts: {
  2155. normal: settingsUI.querySelector("#normalPrompt").value.trim(),
  2156. normal_chinese: settingsUI
  2157. .querySelector("#normalPrompt_chinese")
  2158. .value.trim(),
  2159. advanced: settingsUI.querySelector("#advancedPrompt").value.trim(),
  2160. advanced_chinese: settingsUI
  2161. .querySelector("#advancedPrompt_chinese")
  2162. .value.trim(),
  2163. ocr: settingsUI.querySelector("#ocrPrompt").value.trim(),
  2164. ocr_chinese: settingsUI
  2165. .querySelector("#ocrPrompt_chinese")
  2166. .value.trim(),
  2167. media: settingsUI.querySelector("#mediaPrompt").value.trim(),
  2168. media_chinese: settingsUI
  2169. .querySelector("#mediaPrompt_chinese")
  2170. .value.trim(),
  2171. page: settingsUI.querySelector("#pagePrompt").value.trim(),
  2172. page_chinese: settingsUI
  2173. .querySelector("#pagePrompt_chinese")
  2174. .value.trim(),
  2175. },
  2176. },
  2177. pageTranslation: {
  2178. enabled: settingsUI.querySelector("#pageTranslationEnabled").checked,
  2179. autoTranslate: settingsUI.querySelector("#autoTranslatePage").checked,
  2180. showInitialButton:
  2181. settingsUI.querySelector("#showInitialButton").checked,
  2182. buttonTimeout: DEFAULT_SETTINGS.pageTranslation.buttonTimeout,
  2183. useCustomSelectors,
  2184. customSelectors,
  2185. combineWithDefault,
  2186. defaultSelectors: DEFAULT_SETTINGS.pageTranslation.defaultSelectors,
  2187. excludeSelectors: useCustomSelectors
  2188. ? combineWithDefault
  2189. ? [
  2190. ...new Set([
  2191. ...DEFAULT_SETTINGS.pageTranslation.defaultSelectors,
  2192. ...customSelectors,
  2193. ]),
  2194. ]
  2195. : customSelectors
  2196. : DEFAULT_SETTINGS.pageTranslation.defaultSelectors,
  2197. generation: {
  2198. temperature: parseFloat(settingsUI.querySelector("#pageTranslationTemperature").value),
  2199. topP: parseFloat(settingsUI.querySelector("#pageTranslationTopP").value),
  2200. topK: parseInt(settingsUI.querySelector("#pageTranslationTopK").value)
  2201. }
  2202. },
  2203. ocrOptions: {
  2204. enabled: settingsUI.querySelector("#ocrEnabled").checked,
  2205. preferredProvider: settingsUI.querySelector(
  2206. 'input[name="apiProvider"]:checked'
  2207. ).value,
  2208. displayType: "popup",
  2209. maxFileSize: CONFIG.OCR.maxFileSize,
  2210. temperature: parseFloat(
  2211. settingsUI.querySelector("#ocrTemperature").value
  2212. ),
  2213. topP: parseFloat(settingsUI.querySelector("#ocrTopP").value),
  2214. topK: parseInt(settingsUI.querySelector("#ocrTopK").value),
  2215. },
  2216. mediaOptions: {
  2217. enabled: settingsUI.querySelector("#mediaEnabled").checked,
  2218. temperature: parseFloat(
  2219. settingsUI.querySelector("#mediaTemperature").value
  2220. ),
  2221. topP: parseFloat(settingsUI.querySelector("#mediaTopP").value),
  2222. topK: parseInt(settingsUI.querySelector("#mediaTopK").value),
  2223. },
  2224. videoStreamingOptions: {
  2225. enabled: settingsUI.querySelector("#videoStreamingEnabled").checked,
  2226. fontSize: settingsUI.querySelector("#videoStreamingFontSize").value,
  2227. backgroundColor: settingsUI.querySelector("#videoStreamingBgColor").value,
  2228. textColor: settingsUI.querySelector("#videoStreamingTextColor").value
  2229. },
  2230. displayOptions: {
  2231. fontSize: settingsUI.querySelector("#fontSize").value,
  2232. minPopupWidth: finalMinWidth,
  2233. maxPopupWidth: maxWidthVw,
  2234. webImageTranslation: {
  2235. fontSize: settingsUI.querySelector("#webImageFontSize").value,
  2236. },
  2237. translationMode: settingsUI.querySelector("#displayMode").value,
  2238. targetLanguage: settingsUI.querySelector("#targetLanguage").value,
  2239. sourceLanguage: settingsUI.querySelector("#sourceLanguage").value,
  2240. languageLearning: {
  2241. enabled:
  2242. settingsUI.querySelector("#displayMode").value ===
  2243. "language_learning",
  2244. showSource: settingsUI.querySelector("#showSource").checked,
  2245. },
  2246. },
  2247. shortcuts: {
  2248. settingsEnabled: settingsUI.querySelector("#settingsShortcutEnabled")
  2249. .checked,
  2250. enabled: settingsUI.querySelector("#shortcutsEnabled").checked,
  2251. pageTranslate: {
  2252. key: settingsUI.querySelector("#pageTranslateKey").value,
  2253. altKey: true,
  2254. },
  2255. inputTranslate: {
  2256. key: settingsUI.querySelector("#inputTranslationKey").value,
  2257. altKey: true,
  2258. },
  2259. quickTranslate: {
  2260. key: settingsUI.querySelector("#quickKey").value,
  2261. altKey: true,
  2262. },
  2263. popupTranslate: {
  2264. key: settingsUI.querySelector("#popupKey").value,
  2265. altKey: true,
  2266. },
  2267. advancedTranslate: {
  2268. key: settingsUI.querySelector("#advancedKey").value,
  2269. altKey: true,
  2270. },
  2271. },
  2272. clickOptions: {
  2273. enabled: settingsUI.querySelector("#translationButtonEnabled")
  2274. .checked,
  2275. singleClick: {
  2276. translateType: settingsUI.querySelector("#singleClickSelect").value,
  2277. },
  2278. doubleClick: {
  2279. translateType: settingsUI.querySelector("#doubleClickSelect").value,
  2280. },
  2281. hold: {
  2282. translateType: settingsUI.querySelector("#holdSelect").value,
  2283. },
  2284. },
  2285. touchOptions: {
  2286. enabled: settingsUI.querySelector("#touchEnabled").checked,
  2287. sensitivity: parseInt(
  2288. settingsUI.querySelector("#touchSensitivity").value
  2289. ),
  2290. twoFingers: {
  2291. translateType: settingsUI.querySelector("#twoFingersSelect").value,
  2292. },
  2293. threeFingers: {
  2294. translateType: settingsUI.querySelector("#threeFingersSelect")
  2295. .value,
  2296. },
  2297. },
  2298. cacheOptions: {
  2299. text: {
  2300. enabled: settingsUI.querySelector("#textCacheEnabled").checked,
  2301. maxSize: parseInt(
  2302. settingsUI.querySelector("#textCacheMaxSize").value
  2303. ),
  2304. expirationTime: parseInt(
  2305. settingsUI.querySelector("#textCacheExpiration").value
  2306. ),
  2307. },
  2308. image: {
  2309. enabled: settingsUI.querySelector("#imageCacheEnabled").checked,
  2310. maxSize: parseInt(
  2311. settingsUI.querySelector("#imageCacheMaxSize").value
  2312. ),
  2313. expirationTime: parseInt(
  2314. settingsUI.querySelector("#imageCacheExpiration").value
  2315. ),
  2316. },
  2317. media: {
  2318. enabled: settingsUI.querySelector("#mediaCacheEnabled").checked,
  2319. maxSize: parseInt(
  2320. settingsUI.querySelector("#mediaCacheMaxSize").value
  2321. ),
  2322. expirationTime:
  2323. parseInt(
  2324. settingsUI.querySelector("#mediaCacheExpirationTime").value
  2325. ) * 1000,
  2326. },
  2327. },
  2328. rateLimit: {
  2329. maxRequests: parseInt(settingsUI.querySelector("#maxRequests").value),
  2330. perMilliseconds: parseInt(
  2331. settingsUI.querySelector("#perMilliseconds").value
  2332. ),
  2333. },
  2334. };
  2335. const isToolsEnabled = settingsUI.querySelector(
  2336. "#showTranslatorTools"
  2337. ).checked;
  2338. const currentState =
  2339. localStorage.getItem("translatorToolsEnabled") === "true";
  2340. if (isToolsEnabled !== currentState) {
  2341. localStorage.setItem(
  2342. "translatorToolsEnabled",
  2343. isToolsEnabled.toString()
  2344. );
  2345. this.translator.ui.removeToolsContainer();
  2346. this.translator.ui.resetState();
  2347. const overlays = this.$$(".translator-overlay");
  2348. overlays.forEach((overlay) => overlay.remove());
  2349. if (isToolsEnabled) {
  2350. this.translator.ui.setupTranslatorTools();
  2351. }
  2352. }
  2353. const mergedSettings = this.mergeWithDefaults(newSettings);
  2354. GM_setValue("translatorSettings", JSON.stringify(mergedSettings));
  2355. this.settings = mergedSettings;
  2356. const event = new CustomEvent("settingsChanged", {
  2357. detail: mergedSettings,
  2358. });
  2359. document.dispatchEvent(event);
  2360. return mergedSettings;
  2361. }
  2362. getSetting(path) {
  2363. return path.split(".").reduce((obj, key) => obj?.[key], this.settings);
  2364. }
  2365. }
  2366. class APIKeyManager {
  2367. constructor(settings) {
  2368. this.settings = settings;
  2369. this.failedKeys = new Map();
  2370. this.activeKeys = new Map();
  2371. this.keyStats = new Map();
  2372. this.rateLimitedKeys = new Map();
  2373. this.keyRotationInterval = 10000; // 10s
  2374. this.maxConcurrentRequests = 5;
  2375. this.retryDelays = [1000, 2000, 4000];
  2376. this.successRateThreshold = 0.7;
  2377. this.setupKeyRotation();
  2378. }
  2379. markKeyAsRateLimited(key) {
  2380. const now = Date.now();
  2381. this.rateLimitedKeys.set(key, {
  2382. timestamp: now,
  2383. retryAfter: now + this.settings.rateLimit.perMilliseconds
  2384. });
  2385. }
  2386. getAvailableKeys(provider) {
  2387. const allKeys = this.settings.apiKey[provider];
  2388. if (!allKeys || allKeys.length === 0) {
  2389. throw new Error("Không có API key nào được cấu hình");
  2390. }
  2391. const now = Date.now();
  2392. return allKeys.filter(key => {
  2393. if (!key) return false;
  2394. const failedInfo = this.failedKeys.get(key);
  2395. const activeInfo = this.activeKeys.get(key);
  2396. const rateLimitInfo = this.rateLimitedKeys.get(key);
  2397. const stats = this.keyStats.get(key);
  2398. const isFailed = failedInfo && (now - failedInfo.timestamp < 60000);
  2399. const isBusy = activeInfo && (activeInfo.requests >= this.maxConcurrentRequests);
  2400. const isRateLimited = rateLimitInfo && (now < rateLimitInfo.retryAfter);
  2401. const hasLowSuccessRate = stats &&
  2402. stats.total > 10 &&
  2403. (stats.success / stats.total) < this.successRateThreshold;
  2404. return !isFailed && !isBusy && !isRateLimited && !hasLowSuccessRate;
  2405. });
  2406. }
  2407. async executeWithMultipleKeys(promiseGenerator, provider, maxConcurrent = 3) {
  2408. const availableKeys = this.getAvailableKeys(provider);
  2409. if (!availableKeys || availableKeys.length === 0) {
  2410. throw new Error("Không có API key khả dụng. Vui lòng kiểm tra lại API key trong cài đặt.");
  2411. }
  2412. const errors = [];
  2413. const promises = [];
  2414. let currentKeyIndex = 0;
  2415. const processRequest = async () => {
  2416. if (currentKeyIndex >= availableKeys.length) return null;
  2417. const key = availableKeys[currentKeyIndex++];
  2418. try {
  2419. const result = await this.useKey(key, () => promiseGenerator(key));
  2420. if (result) {
  2421. this.updateKeyStats(key, true);
  2422. return { status: "fulfilled", value: result };
  2423. }
  2424. } catch (error) {
  2425. this.updateKeyStats(key, false);
  2426. if (error.message.includes("API key not valid")) {
  2427. this.markKeyAsFailed(key);
  2428. errors.push(`API key ${key.slice(0, 8)}... không hp lệ`);
  2429. } else if (error.message.includes("rate limit")) {
  2430. this.markKeyAsRateLimited(key);
  2431. errors.push(`API key ${key.slice(0, 8)}... đã vượt quá gii hn`);
  2432. } else {
  2433. errors.push(`Li vi API key ${key.slice(0, 8)}...: ${error.message}`);
  2434. }
  2435. if (currentKeyIndex < availableKeys.length) {
  2436. return processRequest();
  2437. }
  2438. return { status: "rejected", reason: error };
  2439. }
  2440. };
  2441. const maxParallel = Math.min(maxConcurrent, availableKeys.length);
  2442. for (let i = 0; i < maxParallel; i++) {
  2443. promises.push(processRequest());
  2444. }
  2445. const results = await Promise.all(promises);
  2446. const successResults = results
  2447. .filter(r => r && r.status === "fulfilled")
  2448. .map(r => r.value);
  2449. if (successResults.length > 0) {
  2450. return successResults;
  2451. }
  2452. const errorGroups = {
  2453. invalid: errors.filter(e => e.includes("không hợp lệ")),
  2454. rateLimit: errors.filter(e => e.includes("vượt quá giới hạn")),
  2455. other: errors.filter(e => !e.includes("không hợp lệ") && !e.includes("vượt quá giới hạn"))
  2456. };
  2457. let errorMessage = "Tất cả API key đều thất bại:\n";
  2458. if (errorGroups.invalid.length > 0) {
  2459. errorMessage += "\nAPI key không hợp lệ:\n" + errorGroups.invalid.join("\n");
  2460. }
  2461. if (errorGroups.rateLimit.length > 0) {
  2462. errorMessage += "\nAPI key bị giới hạn:\n" + errorGroups.rateLimit.join("\n");
  2463. }
  2464. if (errorGroups.other.length > 0) {
  2465. errorMessage += "\nLỗi khác:\n" + errorGroups.other.join("\n");
  2466. }
  2467. throw new Error(errorMessage);
  2468. }
  2469. async useKey(key, action) {
  2470. let activeInfo = this.activeKeys.get(key) || {
  2471. requests: 0,
  2472. timestamp: Date.now()
  2473. };
  2474. const rateLimitInfo = this.rateLimitedKeys.get(key);
  2475. if (rateLimitInfo && Date.now() < rateLimitInfo.retryAfter) {
  2476. throw new Error(`API key ${key.slice(0, 8)}... đang b gii hn. Th li sau ${Math.ceil((rateLimitInfo.retryAfter - Date.now()) / 1000)}s`);
  2477. }
  2478. if (activeInfo.requests >= this.maxConcurrentRequests) {
  2479. throw new Error(`API key ${key.slice(0, 8)}... đang x lý quá nhiu yêu cu`);
  2480. }
  2481. activeInfo.requests++;
  2482. this.activeKeys.set(key, activeInfo);
  2483. try {
  2484. return await action();
  2485. } catch (error) {
  2486. if (error.status === 429 || error.message.includes("rate limit")) {
  2487. const retryAfter = Date.now() + (parseInt(error.headers?.['retry-after']) * 1000 || 60000);
  2488. this.rateLimitedKeys.set(key, { retryAfter });
  2489. throw new Error(`rate limit: ${error.message}`);
  2490. }
  2491. throw error;
  2492. } finally {
  2493. activeInfo = this.activeKeys.get(key);
  2494. if (activeInfo) {
  2495. activeInfo.requests--;
  2496. if (activeInfo.requests <= 0) {
  2497. this.activeKeys.delete(key);
  2498. } else {
  2499. this.activeKeys.set(key, activeInfo);
  2500. }
  2501. }
  2502. }
  2503. }
  2504. markKeyAsFailed(key) {
  2505. if (!key) return;
  2506. const failInfo = this.failedKeys.get(key) || { failures: 0 };
  2507. failInfo.failures++;
  2508. failInfo.timestamp = Date.now();
  2509. this.failedKeys.set(key, failInfo);
  2510. if (this.activeKeys.has(key)) {
  2511. this.activeKeys.delete(key);
  2512. }
  2513. this.updateKeyStats(key, false);
  2514. console.log(`Marked key as failed: ${key.slice(0, 8)}... (${failInfo.failures} failures)`);
  2515. }
  2516. updateKeyStats(key, success) {
  2517. const stats = this.keyStats.get(key) || {
  2518. success: 0,
  2519. fails: 0,
  2520. total: 0,
  2521. lastUsed: 0,
  2522. avgResponseTime: 0
  2523. };
  2524. stats.total++;
  2525. if (success) {
  2526. stats.success++;
  2527. } else {
  2528. stats.fails++;
  2529. }
  2530. stats.lastUsed = Date.now();
  2531. this.keyStats.set(key, stats);
  2532. }
  2533. setupKeyRotation() {
  2534. setInterval(() => {
  2535. const now = Date.now();
  2536. for (const [key, info] of this.rateLimitedKeys.entries()) {
  2537. if (now >= info.retryAfter) {
  2538. this.rateLimitedKeys.delete(key);
  2539. }
  2540. }
  2541. for (const [key, info] of this.failedKeys.entries()) {
  2542. if (now - info.timestamp >= 60000) {
  2543. this.failedKeys.delete(key);
  2544. }
  2545. }
  2546. for (const [key, info] of this.activeKeys.entries()) {
  2547. if (now - info.timestamp >= 30000) {
  2548. info.requests = 0;
  2549. this.activeKeys.set(key, info);
  2550. }
  2551. }
  2552. for (const [key, stats] of this.keyStats.entries()) {
  2553. if (now - stats.lastUsed > 3600000) {
  2554. stats.success = Math.floor(stats.success * 0.9);
  2555. stats.total = Math.floor(stats.total * 0.9);
  2556. this.keyStats.set(key, stats);
  2557. }
  2558. }
  2559. }, this.keyRotationInterval);
  2560. }
  2561. }
  2562. class APIManager {
  2563. constructor(config, getSettings) {
  2564. this.config = config;
  2565. this.getSettings = getSettings;
  2566. this.keyManager = new APIKeyManager(getSettings());
  2567. this.currentProvider = getSettings().apiProvider;
  2568. this.keyRateLimits = new Map();
  2569. }
  2570. getGenerationConfig(useCase) {
  2571. const settings = this.getSettings();
  2572. switch (useCase) {
  2573. case 'ocr':
  2574. return {
  2575. temperature: settings.ocrOptions.temperature,
  2576. topP: settings.ocrOptions.topP,
  2577. topK: settings.ocrOptions.topK
  2578. };
  2579. case 'media':
  2580. return {
  2581. temperature: settings.mediaOptions.temperature,
  2582. topP: settings.mediaOptions.topP,
  2583. topK: settings.mediaOptions.topK
  2584. };
  2585. case 'page':
  2586. return {
  2587. temperature: settings.pageTranslation.generation.temperature,
  2588. topP: settings.pageTranslation.generation.topP,
  2589. topK: settings.pageTranslation.generation.topK
  2590. };
  2591. default:
  2592. return {
  2593. temperature: settings.pageTranslation.generation.temperature,
  2594. topP: settings.pageTranslation.generation.topP,
  2595. topK: settings.pageTranslation.generation.topK
  2596. };
  2597. }
  2598. }
  2599. async checkRateLimit(apiKey) {
  2600. const now = Date.now();
  2601. const settings = this.getSettings();
  2602. const { maxRequests, perMilliseconds } = settings.rateLimit;
  2603. if (!this.keyRateLimits.has(apiKey)) {
  2604. this.keyRateLimits.set(apiKey, {
  2605. queue: [],
  2606. lastRequestTime: 0
  2607. });
  2608. }
  2609. const rateLimitInfo = this.keyRateLimits.get(apiKey);
  2610. rateLimitInfo.queue = rateLimitInfo.queue.filter(
  2611. time => now - time < perMilliseconds
  2612. );
  2613. if (rateLimitInfo.queue.length >= maxRequests) {
  2614. const oldestRequest = rateLimitInfo.queue[0];
  2615. const waitTime = perMilliseconds - (now - oldestRequest);
  2616. if (waitTime > 0) {
  2617. return false;
  2618. }
  2619. rateLimitInfo.queue.shift();
  2620. }
  2621. rateLimitInfo.queue.push(now);
  2622. rateLimitInfo.lastRequestTime = now;
  2623. return true;
  2624. }
  2625. async request(prompt, useCase = 'normal') {
  2626. const provider = this.config.providers[this.currentProvider];
  2627. if (!provider) {
  2628. throw new Error(`Provider ${this.currentProvider} not found`);
  2629. }
  2630. try {
  2631. const responses = await this.keyManager.executeWithMultipleKeys(
  2632. async (key) => {
  2633. const canUseKey = await this.checkRateLimit(key);
  2634. if (!canUseKey) {
  2635. this.keyManager.markKeyAsRateLimited(key);
  2636. throw new Error("Rate limit exceeded for this key");
  2637. }
  2638. const selectedModel = this.getGeminiModel();
  2639. const generationConfig = this.getGenerationConfig(useCase);
  2640. return await this.makeApiRequest(key, selectedModel, prompt, generationConfig);
  2641. },
  2642. this.currentProvider
  2643. );
  2644. if (responses && responses.length > 0) {
  2645. return provider.responseParser(responses[0]);
  2646. }
  2647. throw new Error("Failed to get translation after all retries");
  2648. } catch (error) {
  2649. console.error("Request failed:", error);
  2650. throw error;
  2651. }
  2652. }
  2653. async makeApiRequest(key, model, prompt, generationConfig) {
  2654. return new Promise((resolve, reject) => {
  2655. GM_xmlhttpRequest({
  2656. method: "POST",
  2657. url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`,
  2658. headers: { "Content-Type": "application/json" },
  2659. data: JSON.stringify({
  2660. contents: [{
  2661. parts: [{ text: prompt }]
  2662. }],
  2663. generationConfig
  2664. }),
  2665. onload: (response) => {
  2666. if (response.status === 200) {
  2667. try {
  2668. const result = JSON.parse(response.responseText);
  2669. if (result?.candidates?.[0]?.content?.parts?.[0]?.text) {
  2670. resolve(result.candidates[0].content.parts[0].text);
  2671. } else {
  2672. reject(new Error("Invalid response format"));
  2673. }
  2674. } catch (error) {
  2675. reject(new Error("Failed to parse response"));
  2676. }
  2677. } else {
  2678. if (response.status === 429 || response.status === 403) {
  2679. this.keyManager.markKeyAsFailed(key);
  2680. reject(new Error("API key rate limit exceeded"));
  2681. } else {
  2682. reject(new Error(`API Error: ${response.status}`));
  2683. }
  2684. }
  2685. },
  2686. onerror: (error) => reject(error)
  2687. });
  2688. });
  2689. }
  2690. getGeminiModel() {
  2691. const settings = this.getSettings();
  2692. const geminiOptions = settings.geminiOptions;
  2693. switch (geminiOptions.modelType) {
  2694. case 'fast':
  2695. return geminiOptions.fastModel;
  2696. case 'pro':
  2697. return geminiOptions.proModel;
  2698. case 'vision':
  2699. return geminiOptions.visionModel;
  2700. case 'custom':
  2701. return geminiOptions.customModel || "gemini-2.0-flash-lite";
  2702. default:
  2703. return "gemini-2.0-flash-lite";
  2704. }
  2705. }
  2706. }
  2707. class InputTranslator {
  2708. constructor(translator) {
  2709. this.translator = translator;
  2710. this.isSelectOpen = false;
  2711. this.isTranslating = false;
  2712. this.activeButtons = new Map();
  2713. this.page = this.translator.page;
  2714. this.ui = new UIManager(translator);
  2715. this.setupObservers();
  2716. this.setupEventListeners();
  2717. this.initializeExistingEditors();
  2718. }
  2719. setupObservers() {
  2720. const settings = this.translator.userSettings.settings;
  2721. if (!settings.inputTranslation?.enabled) return;
  2722. this.mutationObserver = new MutationObserver((mutations) => {
  2723. mutations.forEach((mutation) => {
  2724. mutation.addedNodes.forEach((node) => {
  2725. if (node.nodeType === 1) {
  2726. this.handleNewNode(node);
  2727. }
  2728. });
  2729. mutation.removedNodes.forEach((node) => {
  2730. if (node.nodeType === 1) {
  2731. this.handleRemovedNode(node);
  2732. }
  2733. });
  2734. });
  2735. });
  2736. this.resizeObserver = new ResizeObserver(
  2737. debounce((entries) => {
  2738. entries.forEach((entry) => {
  2739. const editor = this.findParentEditor(entry.target);
  2740. if (editor) {
  2741. this.updateButtonPosition(editor);
  2742. }
  2743. });
  2744. }, 100)
  2745. );
  2746. this.mutationObserver.observe(document.body, {
  2747. childList: true,
  2748. subtree: true,
  2749. });
  2750. }
  2751. getEditorSelectors() {
  2752. return [
  2753. ".fr-element.fr-view",
  2754. ".message-editable",
  2755. ".js-editor",
  2756. ".xenForm textarea",
  2757. '[contenteditable="true"]',
  2758. '[role="textbox"]',
  2759. "textarea",
  2760. 'input[type="text"]',
  2761. ].join(",");
  2762. }
  2763. isValidEditor(element) {
  2764. const settings = this.translator.userSettings.settings;
  2765. if (!settings.inputTranslation?.enabled && !settings.shortcuts?.enabled) return;
  2766. if (!element) return false;
  2767. const style = window.getComputedStyle(element);
  2768. if (style.display === "none" || style.visibility === "hidden") {
  2769. return false;
  2770. }
  2771. const rect = element.getBoundingClientRect();
  2772. if (rect.width === 0 || rect.height === 0) {
  2773. return false;
  2774. }
  2775. return element.matches(this.getEditorSelectors());
  2776. }
  2777. findParentEditor(element) {
  2778. while (element && element !== document.body) {
  2779. if (this.isValidEditor(element)) {
  2780. return element;
  2781. }
  2782. if (element.tagName === "IFRAME") {
  2783. try {
  2784. const iframeDoc = element.contentDocument;
  2785. if (iframeDoc && this.isValidEditor(iframeDoc.body)) {
  2786. return iframeDoc.body;
  2787. }
  2788. } catch (e) {
  2789. }
  2790. }
  2791. element = element.parentElement;
  2792. }
  2793. return null;
  2794. }
  2795. setupEventListeners() {
  2796. const settings = this.translator.userSettings.settings;
  2797. if (!settings.inputTranslation?.enabled) return;
  2798. document.addEventListener("focusin", (e) => {
  2799. const editor = this.findParentEditor(e.target);
  2800. if (editor) {
  2801. this.addTranslateButton(editor);
  2802. this.updateButtonVisibility(editor);
  2803. }
  2804. });
  2805. document.addEventListener("focusout", (e) => {
  2806. const editor = this.findParentEditor(e.target);
  2807. if (editor) {
  2808. setTimeout(() => {
  2809. if (this.isSelectOpen) {
  2810. return;
  2811. }
  2812. const activeElement = document.activeElement;
  2813. const container = this.activeButtons.get(editor);
  2814. const isContainerFocused = container && (
  2815. container === activeElement ||
  2816. container.contains(activeElement)
  2817. );
  2818. const isEditorFocused = editor === activeElement ||
  2819. editor.contains(activeElement);
  2820. if (!isContainerFocused && !isEditorFocused) {
  2821. this.removeTranslateButton(editor);
  2822. }
  2823. }, 100);
  2824. }
  2825. });
  2826. document.addEventListener("input", (e) => {
  2827. const editor = this.findParentEditor(e.target);
  2828. if (editor) {
  2829. if (!this.activeButtons.has(editor)) {
  2830. this.addTranslateButton(editor);
  2831. }
  2832. this.updateButtonVisibility(editor);
  2833. }
  2834. });
  2835. }
  2836. updateButtonVisibility(editor) {
  2837. const container = this.activeButtons.get(editor);
  2838. if (container) {
  2839. const hasContent = this.getEditorContent(editor);
  2840. container.style.display = hasContent ? "" : "none";
  2841. }
  2842. }
  2843. getEditorContent(editor) {
  2844. const settings = this.translator.userSettings.settings;
  2845. if (!settings.inputTranslation?.enabled && !settings.shortcuts?.enabled) return;
  2846. let content = "";
  2847. if (editor.value !== undefined) {
  2848. content = editor.value;
  2849. } else if (editor.textContent !== undefined) {
  2850. content = editor.textContent;
  2851. } else if (editor.innerText !== undefined) {
  2852. content = editor.innerText;
  2853. }
  2854. return content.trim();
  2855. }
  2856. setEditorContent(editor, content) {
  2857. if (editor.matches(".fr-element.fr-view")) {
  2858. editor.innerHTML = content;
  2859. } else if (editor.value !== undefined) {
  2860. editor.value = content;
  2861. } else {
  2862. editor.innerHTML = content;
  2863. }
  2864. editor.dispatchEvent(new Event("input", { bubbles: true }));
  2865. editor.dispatchEvent(new Event("change", { bubbles: true }));
  2866. }
  2867. createButton(icon, title) {
  2868. const button = document.createElement("button");
  2869. button.className = "input-translate-button";
  2870. button.innerHTML = icon;
  2871. button.title = title;
  2872. const theme = this.getCurrentTheme();
  2873. button.style.cssText = `
  2874. background-color: ${theme.backgroundColor};
  2875. color: ${theme.text};
  2876. border: none;
  2877. border-radius: 8px;
  2878. padding: 4px;
  2879. font-size: 16px;
  2880. cursor: pointer;
  2881. display: flex;
  2882. align-items: center;
  2883. justify-content: center;
  2884. min-width: 28px;
  2885. height: 28px;
  2886. transition: all 0.15s ease;
  2887. margin: 0;
  2888. outline: none;
  2889. `;
  2890. button.onmouseover = () => {
  2891. button.style.background = theme.hoverBg;
  2892. button.style.color = theme.hoverText;
  2893. };
  2894. button.onmouseout = () => {
  2895. button.style.background = "transparent";
  2896. button.style.color = theme.text;
  2897. };
  2898. return button;
  2899. }
  2900. createButtonContainer() {
  2901. const container = document.createElement("div");
  2902. container.className = "input-translate-button-container";
  2903. const theme = this.getCurrentTheme();
  2904. container.style.cssText = `
  2905. position: absolute;
  2906. display: flex;
  2907. flex-direction: column;
  2908. gap: 5px;
  2909. z-index: 2147483647;
  2910. pointer-events: auto;
  2911. background-color: rgba(0,74,153,0.5);
  2912. border-radius: 8px;
  2913. padding: 5px;
  2914. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  2915. border: 1px solid ${theme.border};
  2916. `;
  2917. return container;
  2918. }
  2919. addTranslateButton(editor) {
  2920. if (this.activeButtons.has(editor)) {
  2921. this.updateButtonVisibility(editor);
  2922. return;
  2923. }
  2924. const container = this.createButtonContainer();
  2925. const settings = this.translator.userSettings.settings.displayOptions;
  2926. const sourceRow = document.createElement("div");
  2927. sourceRow.style.cssText = `
  2928. display: flex;
  2929. align-items: center;
  2930. gap: 5px;
  2931. `;
  2932. const sourceButton = this.createButton("🌐", "Dịch sang ngôn ngữ nguồn");
  2933. const sourceSelect = document.createElement("select");
  2934. const theme = this.getCurrentTheme();
  2935. sourceSelect.style.cssText = `
  2936. background-color: ${theme.backgroundColor};
  2937. color: ${theme.text};
  2938. transition: all 0.15s ease;
  2939. padding: 4px;
  2940. border-radius: 6px;
  2941. border: none;
  2942. margin-left: 5px;
  2943. font-size: 14px;
  2944. max-height: 32px;
  2945. width: auto;
  2946. min-width: 75px;
  2947. max-width: 100px;
  2948. `;
  2949. const languages = {
  2950. auto: "Tự động",
  2951. vi: "Tiếng Việt",
  2952. en: "Tiếng Anh",
  2953. zh: "Tiếng Trung",
  2954. ko: "Tiếng Hàn",
  2955. ja: "Tiếng Nhật",
  2956. fr: "Tiếng Pháp",
  2957. de: "Tiếng Đức",
  2958. es: "Tiếng Tây Ban Nha",
  2959. it: "Tiếng Ý",
  2960. pt: "Tiếng Bồ Đào Nha",
  2961. ru: "Tiếng Nga",
  2962. ar: "Tiếng Ả Rập",
  2963. hi: "Tiếng Hindi",
  2964. bn: "Tiếng Bengal",
  2965. id: "Tiếng Indonesia",
  2966. ms: "Tiếng Malaysia",
  2967. th: "Tiếng Thái",
  2968. tr: "Tiếng Thổ Nhĩ Kỳ",
  2969. nl: "Tiếng Hà Lan",
  2970. pl: "Tiếng Ba Lan",
  2971. uk: "Tiếng Ukraine",
  2972. el: "Tiếng Hy Lạp",
  2973. cs: "Tiếng Séc",
  2974. da: "Tiếng Đan Mạch",
  2975. fi: "Tiếng Phần Lan",
  2976. he: "Tiếng Do Thái",
  2977. hu: "Tiếng Hungary",
  2978. no: "Tiếng Na Uy",
  2979. ro: "Tiếng Romania",
  2980. sv: "Tiếng Thụy Điển",
  2981. ur: "Tiếng Urdu"
  2982. };
  2983. for (const [code, name] of Object.entries(languages)) {
  2984. const option = document.createElement("option");
  2985. option.value = code;
  2986. option.text = name;
  2987. option.selected = code === settings.sourceLanguage;
  2988. sourceSelect.appendChild(option);
  2989. }
  2990. sourceSelect.addEventListener('mousedown', () => {
  2991. this.isSelectOpen = true;
  2992. });
  2993. sourceSelect.addEventListener('blur', () => {
  2994. setTimeout(() => {
  2995. this.isSelectOpen = false;
  2996. }, 200);
  2997. });
  2998. sourceSelect.addEventListener('change', () => {
  2999. setTimeout(() => {
  3000. editor.focus();
  3001. this.isSelectOpen = false;
  3002. }, 200);
  3003. });
  3004. sourceButton.onclick = async (e) => {
  3005. e.preventDefault();
  3006. e.stopPropagation();
  3007. const sourceLang = sourceSelect.value;
  3008. await this.translateEditor(editor, true, sourceLang);
  3009. };
  3010. sourceRow.appendChild(sourceButton);
  3011. sourceRow.appendChild(sourceSelect);
  3012. const targetRow = document.createElement("div");
  3013. targetRow.style.cssText = `
  3014. display: flex;
  3015. align-items: center;
  3016. gap: 5px;
  3017. margin-top: 5px;
  3018. `;
  3019. const targetButton = this.createButton("🔄", "Dịch sang ngôn ngữ đích");
  3020. const targetSelect = document.createElement("select");
  3021. targetSelect.style.cssText = sourceSelect.style.cssText;
  3022. for (const [code, name] of Object.entries(languages)) {
  3023. if (code !== 'auto') {
  3024. const option = document.createElement("option");
  3025. option.value = code;
  3026. option.text = name;
  3027. option.selected = code === settings.targetLanguage;
  3028. targetSelect.appendChild(option);
  3029. }
  3030. }
  3031. targetSelect.addEventListener('mousedown', () => {
  3032. this.isSelectOpen = true;
  3033. });
  3034. targetSelect.addEventListener('blur', () => {
  3035. setTimeout(() => {
  3036. this.isSelectOpen = false;
  3037. }, 200);
  3038. });
  3039. targetSelect.addEventListener('change', () => {
  3040. setTimeout(() => {
  3041. editor.focus();
  3042. this.isSelectOpen = false;
  3043. }, 200);
  3044. });
  3045. targetButton.onclick = async (e) => {
  3046. e.preventDefault();
  3047. e.stopPropagation();
  3048. const targetLang = targetSelect.value;
  3049. await this.translateEditor(editor, false, targetLang);
  3050. };
  3051. targetRow.appendChild(targetButton);
  3052. targetRow.appendChild(targetSelect);
  3053. container.appendChild(sourceRow);
  3054. container.appendChild(targetRow);
  3055. this.positionButtonContainer(container, editor);
  3056. this.ui.shadowRoot.appendChild(container);
  3057. this.activeButtons.set(editor, container);
  3058. container.addEventListener("mousedown", (e) => {
  3059. if (e.target.tagName !== 'SELECT') {
  3060. e.preventDefault();
  3061. }
  3062. });
  3063. this.updateButtonVisibility(editor);
  3064. this.resizeObserver.observe(editor);
  3065. }
  3066. async translateEditor(editor, isSource, selectedLang) {
  3067. if (this.isTranslating) return;
  3068. this.isTranslating = true;
  3069. const container = this.activeButtons.get(editor);
  3070. const button = isSource ?
  3071. container.querySelector('button:first-of-type') :
  3072. container.querySelector('button:last-of-type');
  3073. const originalIcon = button.innerHTML;
  3074. try {
  3075. const text = this.getEditorContent(editor);
  3076. if (!text) return;
  3077. button.innerHTML = "⌛";
  3078. button.style.opacity = "0.7";
  3079. const sourceLang = isSource && selectedLang === "auto" ?
  3080. this.page.languageCode : selectedLang;
  3081. const result = await this.translator.translate(
  3082. text,
  3083. null,
  3084. false,
  3085. false,
  3086. sourceLang
  3087. );
  3088. const translations = result.split("\n");
  3089. let fullTranslation = "";
  3090. for (const trans of translations) {
  3091. const parts = trans.split("<|>");
  3092. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  3093. }
  3094. this.setEditorContent(editor, fullTranslation.trim());
  3095. } catch (error) {
  3096. console.error("Translation error:", error);
  3097. this.translator.ui.showNotification("Lỗi dịch: " + error.message, "error");
  3098. } finally {
  3099. this.isTranslating = false;
  3100. if (button) {
  3101. button.innerHTML = originalIcon;
  3102. button.style.opacity = "1";
  3103. }
  3104. }
  3105. }
  3106. positionButtonContainer(container, editor) {
  3107. const rect = editor.getBoundingClientRect();
  3108. const toolbar = this.findEditorToolbar(editor);
  3109. if (toolbar) {
  3110. const toolbarRect = toolbar.getBoundingClientRect();
  3111. container.style.top = `${toolbarRect.top + window.scrollY}px`;
  3112. container.style.left = `${toolbarRect.right + 5}px`;
  3113. } else {
  3114. container.style.top = `${rect.top + window.scrollY}px`;
  3115. container.style.left = `${rect.right + 5}px`;
  3116. }
  3117. }
  3118. findEditorToolbar(editor) {
  3119. return (
  3120. editor.closest(".fr-box")?.querySelector(".fr-toolbar") ||
  3121. editor.closest(".xenForm")?.querySelector(".buttonGroup")
  3122. );
  3123. }
  3124. updateButtonPosition(editor) {
  3125. const container = this.activeButtons.get(editor);
  3126. if (container) {
  3127. this.positionButtonContainer(container, editor);
  3128. }
  3129. }
  3130. getCurrentTheme() {
  3131. const themeMode = this.translator.userSettings.settings.theme;
  3132. const theme = CONFIG.THEME[themeMode];
  3133. const isDark = themeMode === 'dark';
  3134. return {
  3135. backgroundColor: isDark ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.5)",
  3136. text: isDark ? "#fff" : "#000",
  3137. border: theme.border,
  3138. hoverBg: isDark ? "#555" : "#eee",
  3139. hoverText: isDark ? "#eee" : "#555",
  3140. };
  3141. }
  3142. updateAllButtonStyles() {
  3143. const theme = this.getCurrentTheme();
  3144. this.activeButtons.forEach((container) => {
  3145. container.style.background = theme.background;
  3146. container.style.borderColor = theme.border;
  3147. container
  3148. .querySelectorAll(".input-translate-button")
  3149. .forEach((button) => {
  3150. button.style.color = theme.text;
  3151. });
  3152. });
  3153. }
  3154. handleNewNode(node) {
  3155. if (this.isValidEditor(node)) {
  3156. this.addTranslateButton(node);
  3157. }
  3158. node.querySelectorAll(this.getEditorSelectors()).forEach((editor) => {
  3159. if (this.isValidEditor(editor)) {
  3160. this.addTranslateButton(editor);
  3161. }
  3162. });
  3163. }
  3164. handleRemovedNode(node) {
  3165. if (this.activeButtons.has(node)) {
  3166. this.removeTranslateButton(node);
  3167. }
  3168. node.querySelectorAll(this.getEditorSelectors()).forEach((editor) => {
  3169. if (this.activeButtons.has(editor)) {
  3170. this.removeTranslateButton(editor);
  3171. }
  3172. });
  3173. }
  3174. handleEditorFocus(editor) {
  3175. if (this.getEditorContent(editor)) {
  3176. this.addTranslateButton(editor);
  3177. }
  3178. }
  3179. handleEditorClick(editor) {
  3180. if (this.getEditorContent(editor)) {
  3181. this.addTranslateButton(editor);
  3182. }
  3183. }
  3184. removeTranslateButton(editor) {
  3185. const container = this.activeButtons.get(editor);
  3186. if (container) {
  3187. container.remove();
  3188. this.activeButtons.delete(editor);
  3189. this.resizeObserver.unobserve(editor);
  3190. }
  3191. }
  3192. initializeExistingEditors() {
  3193. const settings = this.translator.userSettings.settings;
  3194. if (!settings.inputTranslation?.enabled) return;
  3195. document.querySelectorAll(this.getEditorSelectors()).forEach((editor) => {
  3196. if (this.isValidEditor(editor) && this.getEditorContent(editor)) {
  3197. this.addTranslateButton(editor);
  3198. }
  3199. });
  3200. }
  3201. cleanup() {
  3202. this.mutationObserver.disconnect();
  3203. this.resizeObserver.disconnect();
  3204. this.activeButtons.forEach((_container, editor) => {
  3205. this.removeTranslateButton(editor);
  3206. });
  3207. }
  3208. }
  3209. class OCRManager {
  3210. constructor(translator) {
  3211. if (!translator) {
  3212. throw new Error("Translator instance is required for OCRManager");
  3213. }
  3214. this.translator = translator;
  3215. this.isProcessing = false;
  3216. this.imageCache = new FileCache(
  3217. CONFIG.CACHE.image.maxSize,
  3218. CONFIG.CACHE.image.expirationTime
  3219. );
  3220. }
  3221. async captureScreen() {
  3222. try {
  3223. const elements = this.translator.ui.$$(".translator-tools-container, .translator-notification, .center-translate-status");
  3224. elements.forEach(el => {
  3225. if (el) el.style.visibility = "hidden";
  3226. });
  3227. const style = document.createElement('style');
  3228. style.textContent = `
  3229. .screenshot-overlay {
  3230. position: fixed;
  3231. top: 0;
  3232. left: 0;
  3233. width: 100%;
  3234. height: 100%;
  3235. background: rgba(0,0,0,0.3);
  3236. cursor: crosshair;
  3237. z-index: 2147483647;
  3238. touch-action: none;
  3239. }
  3240. .screenshot-guide {
  3241. position: fixed;
  3242. top: 20px;
  3243. left: 50%;
  3244. transform: translateX(-50%);
  3245. background: rgba(0,0,0,0.8);
  3246. color: white;
  3247. padding: 10px 20px;
  3248. border-radius: 8px;
  3249. z-index: 2147483648;
  3250. font-family: Arial, sans-serif;
  3251. font-size: 14px;
  3252. text-align: center;
  3253. white-space: nowrap;
  3254. }
  3255. .screenshot-cancel {
  3256. position: fixed;
  3257. top: 20px;
  3258. right: 20px;
  3259. background-color: #ff4444;
  3260. color: white;
  3261. border: none;
  3262. border-radius: 50%;
  3263. width: 30px;
  3264. height: 30px;
  3265. font-size: 16px;
  3266. cursor: pointer;
  3267. display: flex;
  3268. align-items: center;
  3269. justify-content: center;
  3270. z-index: 2147483647;
  3271. pointer-events: auto;
  3272. }
  3273. .screenshot-selection {
  3274. position: fixed;
  3275. border: 2px solid #4a90e2;
  3276. background: rgba(74,144,226,0.1);
  3277. z-index: 2147483647;
  3278. }
  3279. `;
  3280. this.translator.ui.shadowRoot.appendChild(style);
  3281. const overlay = document.createElement('div');
  3282. overlay.className = 'screenshot-overlay';
  3283. const guide = document.createElement('div');
  3284. guide.className = 'screenshot-guide';
  3285. guide.textContent = "Chạm và kéo để chọn vùng cần dịch";
  3286. const cancelBtn = document.createElement("button");
  3287. cancelBtn.className = "screenshot-cancel";
  3288. cancelBtn.textContent = "✕";
  3289. this.translator.ui.shadowRoot.appendChild(overlay);
  3290. this.translator.ui.shadowRoot.appendChild(guide);
  3291. this.translator.ui.shadowRoot.appendChild(cancelBtn);
  3292. return new Promise((resolve, reject) => {
  3293. let startX, startY;
  3294. let selection = null;
  3295. let isSelecting = false;
  3296. const getCoordinates = (event) => {
  3297. if (event.touches) {
  3298. return {
  3299. x: event.touches[0].clientX,
  3300. y: event.touches[0].clientY
  3301. };
  3302. }
  3303. return {
  3304. x: event.clientX,
  3305. y: event.clientY
  3306. };
  3307. };
  3308. const captureElement = async (element, rect) => {
  3309. if (element.tagName === 'IMG' || element.tagName === 'CANVAS') {
  3310. const canvas = document.createElement('canvas');
  3311. const ctx = canvas.getContext('2d', { willReadFrequently: true });
  3312. canvas.width = rect.width;
  3313. canvas.height = rect.height;
  3314. try {
  3315. if (element.tagName === 'IMG') {
  3316. const originalCrossOrigin = element.crossOrigin;
  3317. element.crossOrigin = 'anonymous';
  3318. await new Promise((resolve, reject) => {
  3319. const loadHandler = () => {
  3320. element.removeEventListener('load', loadHandler);
  3321. element.removeEventListener('error', errorHandler);
  3322. resolve();
  3323. };
  3324. const errorHandler = () => {
  3325. element.removeEventListener('load', loadHandler);
  3326. element.removeEventListener('error', errorHandler);
  3327. element.crossOrigin = originalCrossOrigin;
  3328. reject(new Error('Không thể load ảnh'));
  3329. };
  3330. if (element.complete) {
  3331. resolve();
  3332. } else {
  3333. element.addEventListener('load', loadHandler);
  3334. element.addEventListener('error', errorHandler);
  3335. }
  3336. });
  3337. const elementRect = element.getBoundingClientRect();
  3338. const sourceX = rect.left - elementRect.left;
  3339. const sourceY = rect.top - elementRect.top;
  3340. const scaleX = element.naturalWidth / elementRect.width;
  3341. const scaleY = element.naturalHeight / elementRect.height;
  3342. ctx.drawImage(
  3343. element,
  3344. sourceX * scaleX,
  3345. sourceY * scaleY,
  3346. rect.width * scaleX,
  3347. rect.height * scaleY,
  3348. 0,
  3349. 0,
  3350. rect.width,
  3351. rect.height
  3352. );
  3353. element.crossOrigin = originalCrossOrigin;
  3354. } else if (element.tagName === 'CANVAS') {
  3355. const sourceCtx = element.getContext('2d', { willReadFrequently: true });
  3356. const elementRect = element.getBoundingClientRect();
  3357. const sourceX = rect.left - elementRect.left;
  3358. const sourceY = rect.top - elementRect.top;
  3359. const scaleX = element.width / elementRect.width;
  3360. const scaleY = element.height / elementRect.height;
  3361. try {
  3362. const imageData = sourceCtx.getImageData(
  3363. sourceX * scaleX,
  3364. sourceY * scaleY,
  3365. rect.width * scaleX,
  3366. rect.height * scaleY
  3367. );
  3368. canvas.width = imageData.width;
  3369. canvas.height = imageData.height;
  3370. ctx.putImageData(imageData, 0, 0);
  3371. } catch (error) {
  3372. if (error.name === 'SecurityError') {
  3373. throw new Error('Canvas chứa nội dung từ domain khác không thể được truy cập');
  3374. }
  3375. throw error;
  3376. }
  3377. }
  3378. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  3379. const hasContent = imageData.data.some(pixel => pixel !== 0);
  3380. if (!hasContent) {
  3381. throw new Error('Không thể capture nội dung từ element');
  3382. }
  3383. return new Promise((resolve, reject) => {
  3384. canvas.toBlob(blob => {
  3385. if (!blob || blob.size < 100) {
  3386. reject(new Error("Không thể tạo ảnh hợp lệ"));
  3387. return;
  3388. }
  3389. resolve(new File([blob], "screenshot.png", { type: "image/png" }));
  3390. }, 'image/png', 1.0);
  3391. });
  3392. } catch (error) {
  3393. throw new Error(`Li x lý ${element.tagName}: ${error.message}`);
  3394. }
  3395. } else {
  3396. try {
  3397. const screenshotCanvas = await html2canvas(element, {
  3398. width: rect.width,
  3399. height: rect.height,
  3400. x: rect.left - element.getBoundingClientRect().left,
  3401. y: rect.top - element.getBoundingClientRect().top,
  3402. scale: 2,
  3403. logging: false,
  3404. useCORS: true,
  3405. allowTaint: true,
  3406. backgroundColor: '#ffffff',
  3407. foreignObjectRendering: true,
  3408. removeContainer: true,
  3409. ignoreElements: (element) => {
  3410. const classList = element.classList ? Array.from(element.classList) : [];
  3411. const id = element.id || '';
  3412. return id.includes('translator') ||
  3413. classList.some(c => c.includes('translator')) ||
  3414. classList.some(c => c.includes('screenshot'));
  3415. },
  3416. onclone: (clonedDoc) => {
  3417. const elements = clonedDoc.querySelectorAll('[id*="translator"], [class*="translator"], [class*="screenshot"]');
  3418. elements.forEach(el => el.remove());
  3419. }
  3420. });
  3421. return new Promise((resolve, reject) => {
  3422. screenshotCanvas.toBlob(blob => {
  3423. if (!blob || blob.size < 100) {
  3424. reject(new Error("Ảnh chụp không hợp lệ"));
  3425. return;
  3426. }
  3427. resolve(new File([blob], "screenshot.png", { type: "image/png" }));
  3428. }, 'image/png', 1.0);
  3429. });
  3430. } catch (error) {
  3431. throw new Error(`Li html2canvas: ${error.message}`);
  3432. }
  3433. }
  3434. };
  3435. const startSelection = (e) => {
  3436. e.preventDefault();
  3437. const coords = getCoordinates(e);
  3438. startX = coords.x;
  3439. startY = coords.y;
  3440. isSelecting = true;
  3441. if (selection) selection.remove();
  3442. selection = document.createElement('div');
  3443. selection.className = 'screenshot-selection';
  3444. this.translator.ui.shadowRoot.appendChild(selection);
  3445. };
  3446. const updateSelection = debounce((e) => {
  3447. if (!isSelecting || !selection) return;
  3448. e.preventDefault();
  3449. const coords = getCoordinates(e);
  3450. const currentX = coords.x;
  3451. const currentY = coords.y;
  3452. const left = Math.min(startX, currentX);
  3453. const top = Math.min(startY, currentY);
  3454. const width = Math.abs(currentX - startX);
  3455. const height = Math.abs(currentY - startY);
  3456. if (width < 10 || height < 10) return;
  3457. requestAnimationFrame(() => {
  3458. selection.style.left = left + 'px';
  3459. selection.style.top = top + 'px';
  3460. selection.style.width = width + 'px';
  3461. selection.style.height = height + 'px';
  3462. });
  3463. }, 16);
  3464. const endSelection = debounce(async (e) => {
  3465. if (!isSelecting || !selection) return;
  3466. e.preventDefault();
  3467. isSelecting = false;
  3468. try {
  3469. this.translator.ui.showProcessingStatus("Đang chuẩn bị chụp màn hình...");
  3470. const rect = selection.getBoundingClientRect();
  3471. if (rect.width < 10 || rect.height < 10) {
  3472. selection.remove();
  3473. return;
  3474. }
  3475. const elements = document.elementsFromPoint(
  3476. rect.left + rect.width / 2,
  3477. rect.top + rect.height / 2
  3478. );
  3479. const targetElement = elements.find(el => {
  3480. const classList = el.classList ? Array.from(el.classList) : [];
  3481. const id = el.id || '';
  3482. return !id.includes('translator') &&
  3483. !classList.some(c => c.includes('translator')) &&
  3484. !classList.some(c => c.includes('screenshot'));
  3485. });
  3486. if (!targetElement) {
  3487. throw new Error("Không thể xác định vùng chọn");
  3488. }
  3489. const file = await captureElement(targetElement, rect);
  3490. resolve(file);
  3491. } catch (error) {
  3492. console.error("Screenshot error:", error);
  3493. reject(new Error("Không thể chụp màn hình: " + error.message));
  3494. } finally {
  3495. cleanup();
  3496. }
  3497. }, 100);
  3498. const cleanup = () => {
  3499. setTimeout(() => this.translator.ui.removeProcessingStatus(), 1000);
  3500. overlay.remove();
  3501. guide.remove();
  3502. cancelBtn.remove();
  3503. if (selection) selection.remove();
  3504. style.remove();
  3505. elements.forEach(el => {
  3506. if (el) el.style.visibility = "";
  3507. });
  3508. overlay.removeEventListener('mousedown', startSelection);
  3509. document.removeEventListener('mousemove', updateSelection);
  3510. document.removeEventListener('mouseup', endSelection);
  3511. overlay.removeEventListener('touchstart', startSelection);
  3512. document.removeEventListener('touchmove', updateSelection);
  3513. document.removeEventListener('touchend', endSelection);
  3514. document.removeEventListener('touchcancel', cleanup);
  3515. };
  3516. overlay.addEventListener('mousedown', startSelection);
  3517. document.addEventListener('mousemove', updateSelection);
  3518. document.addEventListener('mouseup', endSelection);
  3519. overlay.addEventListener('touchstart', startSelection, { passive: false });
  3520. document.addEventListener('touchmove', updateSelection, { passive: false });
  3521. document.addEventListener('touchend', endSelection);
  3522. document.addEventListener('touchcancel', cleanup);
  3523. cancelBtn.addEventListener("click", () => {
  3524. cleanup();
  3525. reject(new Error('Đã hủy chọn vùng'));
  3526. });
  3527. document.addEventListener('keydown', (e) => {
  3528. if (e.key === 'Escape') {
  3529. cleanup();
  3530. reject(new Error('Đã hủy chọn vùng'));
  3531. }
  3532. });
  3533. });
  3534. } catch (error) {
  3535. console.error("Screen capture error:", error);
  3536. const elements = this.translator.ui.$$(".translator-tools-container, .translator-notification, .center-translate-status");
  3537. elements.forEach(el => {
  3538. if (el) el.style.visibility = "";
  3539. });
  3540. throw new Error(`Không th chp màn hình: ${error.message}`);
  3541. }
  3542. }
  3543. async processImage(file) {
  3544. try {
  3545. this.isProcessing = true;
  3546. this.translator.ui.showProcessingStatus("Đang xử lý ảnh...");
  3547. const optimizedFile = await this.optimizeImage(file);
  3548. const base64Image = await this.fileToBase64(optimizedFile);
  3549. this.translator.ui.updateProcessingStatus("Đang kiểm tra cache...", 20);
  3550. if (this.imageCache && this.translator.userSettings.settings.cacheOptions.image.enabled) {
  3551. const cachedResult = await this.imageCache.get(base64Image);
  3552. if (cachedResult) {
  3553. this.translator.ui.updateProcessingStatus("Đã tìm thấy trong cache", 100);
  3554. return cachedResult;
  3555. }
  3556. }
  3557. this.translator.ui.updateProcessingStatus("Đang nhận diện text...", 40);
  3558. const result = await this.performOCR(optimizedFile);
  3559. if (this.imageCache && this.translator.userSettings.settings.cacheOptions.image.enabled) {
  3560. await this.imageCache.set(base64Image, result);
  3561. }
  3562. this.translator.ui.updateProcessingStatus("Hoàn thành", 100);
  3563. return result;
  3564. } catch (error) {
  3565. console.error("OCR processing error:", error);
  3566. throw error;
  3567. } finally {
  3568. this.isProcessing = false;
  3569. setTimeout(() => this.translator.ui.removeProcessingStatus(), 1000);
  3570. }
  3571. }
  3572. async optimizeImage(file) {
  3573. const img = await createImageBitmap(file);
  3574. let newWidth = img.width;
  3575. let newHeight = img.height;
  3576. const maxDimension = 2000;
  3577. if (img.width > maxDimension || img.height > maxDimension) {
  3578. if (img.width > img.height) {
  3579. newWidth = maxDimension;
  3580. newHeight = Math.floor(img.height * (maxDimension / img.width));
  3581. } else {
  3582. newHeight = maxDimension;
  3583. newWidth = Math.floor(img.width * (maxDimension / img.height));
  3584. }
  3585. }
  3586. const canvas = document.createElement('canvas');
  3587. const ctx = canvas.getContext('2d', { willReadFrequently: true });
  3588. canvas.width = newWidth;
  3589. canvas.height = newHeight;
  3590. ctx.drawImage(img, 0, 0, newWidth, newHeight);
  3591. const blob = await new Promise(resolve => {
  3592. canvas.toBlob(resolve, 'image/jpeg', 0.9);
  3593. });
  3594. return new File([blob], file.name, { type: 'image/jpeg' });
  3595. }
  3596. async performOCR(file) {
  3597. const settings = this.translator.userSettings.settings;
  3598. const selectedModel = this.translator.api.getGeminiModel();
  3599. const prompt = this.translator.createPrompt("ocr", "ocr");
  3600. const base64Image = await this.fileToBase64(file);
  3601. const requestBody = {
  3602. contents: [{
  3603. parts: [
  3604. { text: prompt },
  3605. {
  3606. inline_data: {
  3607. mime_type: file.type,
  3608. data: base64Image
  3609. }
  3610. }
  3611. ]
  3612. }],
  3613. generationConfig: {
  3614. temperature: settings.ocrOptions.temperature,
  3615. topP: settings.ocrOptions.topP,
  3616. topK: settings.ocrOptions.topK,
  3617. }
  3618. };
  3619. const maxRetries = 3;
  3620. let lastError = null;
  3621. for (let attempt = 0; attempt < maxRetries; attempt++) {
  3622. try {
  3623. const response = await new Promise((resolve, reject) => {
  3624. GM_xmlhttpRequest({
  3625. method: "POST",
  3626. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${settings.apiKey[settings.apiProvider][0]}`,
  3627. headers: { "Content-Type": "application/json" },
  3628. data: JSON.stringify(requestBody),
  3629. onload: (response) => {
  3630. if (response.status === 200) {
  3631. try {
  3632. const result = JSON.parse(response.responseText);
  3633. if (result?.candidates?.[0]?.content?.parts?.[0]?.text) {
  3634. resolve(result.candidates[0].content.parts[0].text);
  3635. } else {
  3636. reject(new Error("Invalid response format"));
  3637. }
  3638. } catch (error) {
  3639. reject(new Error("Failed to parse response"));
  3640. }
  3641. } else if (response.status === 413) {
  3642. reject(new Error("Image size too large"));
  3643. } else if (response.status === 429) {
  3644. reject(new Error("Rate limit exceeded"));
  3645. } else {
  3646. reject(new Error(`API Error: ${response.status}`));
  3647. }
  3648. },
  3649. onerror: (error) => reject(new Error(`Connection error: ${error}`))
  3650. });
  3651. });
  3652. return response;
  3653. } catch (error) {
  3654. lastError = error;
  3655. if (error.message === "Image size too large" && attempt < maxRetries - 1) {
  3656. file = await this.reduceImageSize(file);
  3657. base64Image = await this.fileToBase64(file);
  3658. requestBody.contents[0].parts[1].data = base64Image;
  3659. continue;
  3660. }
  3661. if (error.message === "Rate limit exceeded") {
  3662. await new Promise(resolve => setTimeout(resolve, 2000 * (attempt + 1)));
  3663. continue;
  3664. }
  3665. if (attempt < maxRetries - 1) {
  3666. await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
  3667. continue;
  3668. }
  3669. }
  3670. }
  3671. throw lastError || new Error("Failed to perform OCR");
  3672. }
  3673. async reduceImageSize(file) {
  3674. const img = await createImageBitmap(file);
  3675. const canvas = document.createElement('canvas');
  3676. const ctx = canvas.getContext('2d', { willReadFrequently: true });
  3677. canvas.width = Math.floor(img.width * 0.75);
  3678. canvas.height = Math.floor(img.height * 0.75);
  3679. ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  3680. const blob = await new Promise(resolve => {
  3681. canvas.toBlob(resolve, 'image/jpeg', 0.85);
  3682. });
  3683. return new File([blob], file.name, { type: 'image/jpeg' });
  3684. }
  3685. fileToBase64(file) {
  3686. return new Promise((resolve, reject) => {
  3687. const reader = new FileReader();
  3688. reader.onload = () => resolve(reader.result.split(",")[1]);
  3689. reader.onerror = () => reject(new Error("Không thể đọc file"));
  3690. reader.readAsDataURL(file);
  3691. });
  3692. }
  3693. }
  3694. class MediaManager {
  3695. constructor(translator) {
  3696. this.translator = translator;
  3697. this.isProcessing = false;
  3698. this.mediaCache = new FileCache(
  3699. CONFIG.CACHE.media.maxSize,
  3700. CONFIG.CACHE.media.expirationTime
  3701. );
  3702. }
  3703. async processMediaFile(file) {
  3704. try {
  3705. if (!this.isValidFormat(file)) {
  3706. throw new Error("Định dạng file không được hỗ trợ");
  3707. }
  3708. if (!this.isValidSize(file)) {
  3709. throw new Error(
  3710. `File quá ln. Kích thước ti đa: ${this.getMaxSizeInMB(file)}MB`
  3711. );
  3712. }
  3713. this.isProcessing = true;
  3714. this.translator.ui.showProcessingStatus("Đang xử lý media...");
  3715. const base64Media = await this.fileToBase64(file);
  3716. this.translator.ui.updateProcessingStatus("Đang kiểm tra cache...", 20);
  3717. const cacheEnabled =
  3718. this.translator.userSettings.settings.cacheOptions.media?.enabled;
  3719. if (cacheEnabled && this.mediaCache) {
  3720. const cachedResult = await this.mediaCache.get(base64Media);
  3721. if (cachedResult) {
  3722. this.translator.ui.updateProcessingStatus(
  3723. "Đã tìm thấy trong cache",
  3724. 100
  3725. );
  3726. this.translator.ui.displayPopup(cachedResult, null, "Bản dịch");
  3727. return;
  3728. }
  3729. }
  3730. this.translator.ui.updateProcessingStatus(
  3731. "Đang xử lý audio/video...",
  3732. 40
  3733. );
  3734. const settings = this.translator.userSettings.settings;
  3735. const mediaSettings = settings.mediaOptions;
  3736. const selectedModel = this.translator.api.getGeminiModel();
  3737. const prompt = this.translator.createPrompt("media", "media");
  3738. const requestBody = {
  3739. contents: [
  3740. {
  3741. parts: [
  3742. {
  3743. text: prompt,
  3744. },
  3745. {
  3746. inline_data: {
  3747. mime_type: file.type,
  3748. data: base64Media,
  3749. },
  3750. },
  3751. ],
  3752. },
  3753. ],
  3754. generationConfig: {
  3755. temperature: mediaSettings.temperature,
  3756. topP: mediaSettings.topP,
  3757. topK: mediaSettings.topK,
  3758. },
  3759. };
  3760. this.translator.ui.updateProcessingStatus("Đang dịch...", 60);
  3761. const results = await this.translator.api.keyManager.executeWithMultipleKeys(
  3762. async (key) => {
  3763. const response = await new Promise((resolve, reject) => {
  3764. GM_xmlhttpRequest({
  3765. method: "POST",
  3766. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`,
  3767. headers: { "Content-Type": "application/json" },
  3768. data: JSON.stringify(requestBody),
  3769. onload: (response) => {
  3770. if (response.status === 200) {
  3771. try {
  3772. const result = JSON.parse(response.responseText);
  3773. if (result?.candidates?.[0]?.content?.parts?.[0]?.text) {
  3774. resolve(result.candidates[0].content.parts[0].text);
  3775. } else {
  3776. reject(new Error("Invalid response format"));
  3777. }
  3778. } catch (error) {
  3779. reject(new Error("Failed to parse response"));
  3780. }
  3781. } else {
  3782. if (response.status === 429 || response.status === 403) {
  3783. reject(new Error("API key rate limit exceeded"));
  3784. } else {
  3785. reject(new Error(`API Error: ${response.status}`));
  3786. }
  3787. }
  3788. },
  3789. onerror: (error) => reject(new Error(`Connection error: ${error}`))
  3790. });
  3791. });
  3792. return response;
  3793. },
  3794. settings.apiProvider
  3795. );
  3796. this.translator.ui.updateProcessingStatus("Đang hoàn thiện...", 80);
  3797. if (!results || results.length === 0) {
  3798. throw new Error("Không thể xử lý media");
  3799. }
  3800. const finalResult = results[0];
  3801. if (cacheEnabled && this.mediaCache) {
  3802. await this.mediaCache.set(base64Media, finalResult);
  3803. }
  3804. this.translator.ui.updateProcessingStatus("Hoàn thành", 100);
  3805. this.translator.ui.displayPopup(finalResult, null, "Bản dịch");
  3806. } catch (error) {
  3807. console.error("Media processing error:", error);
  3808. throw new Error(`Không th x lý file: ${error.message}`);
  3809. } finally {
  3810. this.isProcessing = false;
  3811. setTimeout(() => this.translator.ui.removeProcessingStatus(), 1000);
  3812. }
  3813. }
  3814. isValidFormat(file) {
  3815. const extension = file.name.split(".").pop().toLowerCase();
  3816. const mimeMapping = {
  3817. mp3: "audio/mp3",
  3818. wav: "audio/wav",
  3819. ogg: "audio/ogg",
  3820. m4a: "audio/m4a",
  3821. aac: "audio/aac",
  3822. flac: "audio/flac",
  3823. wma: "audio/wma",
  3824. opus: "audio/opus",
  3825. amr: "audio/amr",
  3826. midi: "audio/midi",
  3827. mid: "audio/midi",
  3828. mp4: "video/mp4",
  3829. webm: "video/webm",
  3830. ogv: "video/ogg",
  3831. avi: "video/x-msvideo",
  3832. mov: "video/quicktime",
  3833. wmv: "video/x-ms-wmv",
  3834. flv: "video/x-flv",
  3835. "3gp": "video/3gpp",
  3836. "3g2": "video/3gpp2",
  3837. mkv: "video/x-matroska",
  3838. };
  3839. const mimeType = mimeMapping[extension];
  3840. if (mimeType?.startsWith("audio/")) {
  3841. return CONFIG.MEDIA.audio.supportedFormats.includes(mimeType);
  3842. } else if (mimeType?.startsWith("video/")) {
  3843. return CONFIG.MEDIA.video.supportedFormats.includes(mimeType);
  3844. }
  3845. return false;
  3846. }
  3847. isValidSize(file) {
  3848. const maxSize = file.type.startsWith("audio/")
  3849. ? CONFIG.MEDIA.audio.maxSize
  3850. : CONFIG.MEDIA.video.maxSize;
  3851. return file.size <= maxSize;
  3852. }
  3853. getMaxSizeInMB(file) {
  3854. const maxSize = file.type.startsWith("audio/")
  3855. ? CONFIG.MEDIA.audio.maxSize
  3856. : CONFIG.MEDIA.video.maxSize;
  3857. return Math.floor(maxSize / (1024 * 1024));
  3858. }
  3859. fileToBase64(file) {
  3860. return new Promise((resolve, reject) => {
  3861. const reader = new FileReader();
  3862. reader.onload = () => resolve(reader.result.split(",")[1]);
  3863. reader.onerror = () => reject(new Error("Không thể đọc file"));
  3864. reader.readAsDataURL(file);
  3865. });
  3866. }
  3867. cleanup() {
  3868. try {
  3869. if (this.audioCtx) {
  3870. this.audioCtx.close();
  3871. this.audioCtx = null;
  3872. }
  3873. if (this.processor) {
  3874. this.processor.disconnect();
  3875. this.processor = null;
  3876. }
  3877. if (this.container) {
  3878. this.container.remove();
  3879. this.container = null;
  3880. }
  3881. this.mediaElement = null;
  3882. this.audioBuffer = null;
  3883. } catch (error) {
  3884. console.error("Error during cleanup:", error);
  3885. }
  3886. }
  3887. }
  3888. class VideoStreamingTranslator {
  3889. constructor(translator) {
  3890. this.translator = translator;
  3891. this.isEnabled = false;
  3892. this.isPlaying = false;
  3893. this.hasCaptions = false;
  3894. this.initialized = false;
  3895. this.activeVideoId = null;
  3896. this.videoCheckInterval = null;
  3897. this.playingCheckTimeout = null;
  3898. this.currentVideo = null;
  3899. this.subtitleContainer = null;
  3900. this.lastCaption = '';
  3901. this.subtitleCache = new Map();
  3902. this.keyIndex = null;
  3903. this.rateLimitedKeys = new Map();
  3904. this.retryDelay = 500;
  3905. this.captionObserver = null;
  3906. this.settings = translator.userSettings.settings;
  3907. this.platformInfo = this.detectPlatform();
  3908. if (this.settings.videoStreamingOptions?.enabled && this.platformInfo) {
  3909. GM_addStyle(`
  3910. .caption-window {
  3911. position: absolute !important;
  3912. opacity: 0 !important;
  3913. visibility: hidden !important;
  3914. pointer-events: none !important;
  3915. width: 1px !important;
  3916. height: 1px !important;
  3917. overflow: hidden !important;
  3918. clip: rect(0 0 0 0) !important;
  3919. margin: -1px !important;
  3920. padding: 0 !important;
  3921. border: 0 !important;
  3922. }`);
  3923. this.start();
  3924. this.setupVideoListeners();
  3925. this.setupMutationObserver();
  3926. }
  3927. }
  3928. detectPlatform() {
  3929. this.platformConfigs = {
  3930. youtube: {
  3931. videoSelector: [
  3932. '.html5-video-player',
  3933. '.ytp-player-content',
  3934. '.html5-main-video',
  3935. '.video-stream',
  3936. '.video-stream html5-main-video',
  3937. '.html5-video-container video',
  3938. '#movie_player',
  3939. '#player-api_VORAPI_ELEMENT_ID',
  3940. ],
  3941. captionSelectorAll: [
  3942. '.ytp-caption-window-container',
  3943. '.caption-window',
  3944. '.caption-window-transform',
  3945. '.ytp-caption-segment',
  3946. '.captions-text',
  3947. ],
  3948. captionSelector: ['.captions-text'],
  3949. captionButton: {
  3950. desktop: '.ytp-subtitles-button',
  3951. mobile: '.ytmClosedCaptioningButtonButton'
  3952. }
  3953. },
  3954. // netflix: {
  3955. // videoSelector: [
  3956. // '.watch-video',
  3957. // '.VideoContainer',
  3958. // '.nf-player-container'
  3959. // ],
  3960. // captionSelectorAll: [
  3961. // '.captions-text > span',
  3962. // '.player-timedtext',
  3963. // '.player-timedtext-text-container > *',
  3964. // ],
  3965. // captionSelector: '.player-timedtext-text-container span',
  3966. // captionButton: '[data-uia="control-subtitle"]'
  3967. // },
  3968. // udemy: {
  3969. // videoSelector: [
  3970. // '.video-player--video-player',
  3971. // '.video-viewer--container--3yIje',
  3972. // '.video-player--container--1-zwz',
  3973. // '.video-player--video-wrapper--3qqR6',
  3974. // '.vjs-fluid'
  3975. // ],
  3976. // captionSelectorAll: [
  3977. // '.vjs-text-track-display > div > div',
  3978. // '.captions-display--captions-cue-text--TQ0DQ',
  3979. // '.captions-display--vjs-ud-captions-cue-text--38tMf',
  3980. // ],
  3981. // captionSelector: '.vjs-text-track-display > div > div > div',
  3982. // captionButton: '.vjs-subs-caps-button'
  3983. // },
  3984. // coursera: {
  3985. // videoSelector: [
  3986. // '.rc-VideoMiniPlayer',
  3987. // '.video-main-player-container',
  3988. // '.c-video-player',
  3989. // 'video.vjs-tech'
  3990. // ],
  3991. // captionSelectorAll: [
  3992. // '.rc-VideoCaption',
  3993. // '.video-caption-container',
  3994. // '.video-subtitle > span',
  3995. // '.video-caption > span',
  3996. // ],
  3997. // captionSelector: '.rc-VideoCaption > div',
  3998. // captionButton: '.rc-VideoCaption__button'
  3999. // }
  4000. };
  4001. const hostname = window.location.hostname;
  4002. for (const [platform, config] of Object.entries(this.platformConfigs)) {
  4003. if (hostname.includes(platform)) {
  4004. return { platform, config };
  4005. }
  4006. }
  4007. return null;
  4008. }
  4009. setupMutationObserver() {
  4010. const observer = new MutationObserver((mutations) => {
  4011. mutations.forEach((mutation) => {
  4012. mutation.addedNodes.forEach((node) => {
  4013. if (node.nodeName === 'VIDEO') {
  4014. this.handleNewVideo(node);
  4015. }
  4016. if (node.nodeType === Node.ELEMENT_NODE) {
  4017. this.checkForCaptionContainer(node);
  4018. }
  4019. });
  4020. });
  4021. });
  4022. observer.observe(document.body, {
  4023. childList: true,
  4024. subtree: true
  4025. });
  4026. }
  4027. checkForCaptionContainer(node) {
  4028. const { config } = this.platformInfo;
  4029. const CaptionSelectors = config.captionSelectorAll;
  4030. if (CaptionSelectors.some(selector => node.matches?.(selector))) {
  4031. if (this.isEnabled) {
  4032. this.setupCaptionObserver(node);
  4033. }
  4034. }
  4035. node.querySelectorAll(CaptionSelectors.join(',')).forEach(container => {
  4036. if (this.isEnabled) {
  4037. this.setupCaptionObserver(container);
  4038. }
  4039. });
  4040. }
  4041. setupCaptionObserver(container) {
  4042. if (!this.captionObserver) {
  4043. this.captionObserver = new MutationObserver(() => {
  4044. if (this.isEnabled) {
  4045. this.processVideoFrame();
  4046. }
  4047. });
  4048. }
  4049. this.captionObserver.observe(container, {
  4050. childList: true,
  4051. subtree: true,
  4052. characterData: true
  4053. });
  4054. }
  4055. handleNewVideo(videoElement) {
  4056. if (this.isEnabled && !this.currentVideo && this.platformInfo) {
  4057. this.currentVideo = videoElement;
  4058. this.start();
  4059. this.setupVideoListeners();
  4060. }
  4061. }
  4062. setupVideoListeners() {
  4063. if (!this.currentVideo || this.initialized) return;
  4064. this.activeVideoId = Math.random().toString(36).substring(7);
  4065. this.currentVideo.dataset.translatorVideoId = this.activeVideoId;
  4066. const videoEvents = ['play', 'playing', 'pause', 'ended'];
  4067. videoEvents.forEach(eventName => {
  4068. const handler = async () => {
  4069. if (this.currentVideo?.dataset.translatorVideoId !== this.activeVideoId) return;
  4070. switch (eventName) {
  4071. case 'play':
  4072. case 'playing':
  4073. this.isPlaying = true;
  4074. await this.enableCaptions();
  4075. if (!this.hasCaptions) {
  4076. this.startWatchingForCaptions();
  4077. }
  4078. break;
  4079. case 'pause':
  4080. this.isPlaying = false;
  4081. break;
  4082. case 'ended':
  4083. this.isPlaying = false;
  4084. this.cleanupVideo();
  4085. break;
  4086. }
  4087. };
  4088. this.currentVideo.addEventListener(eventName, handler);
  4089. this.currentVideo[`${eventName}Handler`] = handler;
  4090. });
  4091. if (this.currentVideo) {
  4092. let previousWidth = 0;
  4093. let translatedCaption = null;
  4094. let originalText = null;
  4095. let translatedText = null;
  4096. const updateStyles = debounce((width) => {
  4097. if (width === previousWidth) return;
  4098. previousWidth = width;
  4099. if (!translatedCaption) translatedCaption = document.querySelector('.translated-caption');
  4100. if (!originalText) originalText = document.querySelector('.original-text') || null;
  4101. if (!translatedText) translatedText = document.querySelector('.translated-text');
  4102. if (translatedText) {
  4103. let tranWidth, origSize, transSize;
  4104. if (width <= 480) {
  4105. tranWidth = '98%';
  4106. origSize = '0.65em';
  4107. transSize = '0.7em';
  4108. } else if (width <= 962) {
  4109. tranWidth = '95%';
  4110. origSize = '0.75em';
  4111. transSize = '0.8em';
  4112. } else if (width <= 1366) {
  4113. tranWidth = '90%';
  4114. origSize = '0.85em';
  4115. transSize = '0.9em';
  4116. } else {
  4117. tranWidth = '90%';
  4118. origSize = '0.95em';
  4119. transSize = '1em';
  4120. }
  4121. translatedCaption.style.maxWidth = tranWidth;
  4122. if (originalText) originalText.style.fontSize = origSize;
  4123. translatedText.style.fontSize = transSize;
  4124. }
  4125. }, 100);
  4126. const adjustContainer = debounce(() => {
  4127. if (!this.subtitleContainer || !this.currentVideo) return;
  4128. const videoRect = this.currentVideo.getBoundingClientRect();
  4129. const containerRect = this.subtitleContainer.getBoundingClientRect();
  4130. if (containerRect.bottom > videoRect.bottom) {
  4131. this.subtitleContainer.style.bottom = '5%';
  4132. }
  4133. if (containerRect.width > videoRect.width * 0.9) {
  4134. this.subtitleContainer.style.maxWidth = '90%';
  4135. }
  4136. }, 100);
  4137. const resizeObserver = new ResizeObserver(entries => {
  4138. const width = entries[0].contentRect.width;
  4139. updateStyles(width);
  4140. adjustContainer();
  4141. });
  4142. resizeObserver.observe(this.currentVideo);
  4143. }
  4144. this.initialized = true;
  4145. }
  4146. async enableCaptions() {
  4147. try {
  4148. const { platform, config } = this.platformInfo;
  4149. if (platform === 'youtube') {
  4150. await this.enableYouTubeCaptions();
  4151. return;
  4152. }
  4153. const button = document.querySelector(config.captionButton);
  4154. if (!button) return;
  4155. const isEnabled = this.isCaptionEnabled(platform, button);
  4156. if (!isEnabled) {
  4157. button.click();
  4158. await new Promise(resolve => setTimeout(resolve, this.retryDelay));
  4159. }
  4160. this.observeCaptionButton(button, platform);
  4161. } catch (error) {
  4162. console.error('Lỗi khi bật caption:', error);
  4163. }
  4164. }
  4165. isCaptionEnabled(platform, button) {
  4166. switch (platform) {
  4167. case 'netflix':
  4168. return button.getAttribute('data-uia-state') === 'active';
  4169. case 'udemy':
  4170. return button.classList.contains('vjs-selected');
  4171. case 'coursera':
  4172. return button.classList.contains('active');
  4173. default:
  4174. return false;
  4175. }
  4176. }
  4177. async enableYouTubeCaptions() {
  4178. try {
  4179. const waitForPlayer = async () => {
  4180. for (let i = 0; i < 10; i++) {
  4181. const player = document.querySelector('.html5-video-player') ||
  4182. document.querySelector('#player-api_VORAPI_ELEMENT_ID') ||
  4183. document.querySelector('.ytmClosedCaptioningButtonButton')?.parentElement;
  4184. if (player) return player;
  4185. await new Promise(resolve => setTimeout(resolve, this.retryDelay));
  4186. }
  4187. return null;
  4188. };
  4189. const player = await waitForPlayer();
  4190. if (!player) {
  4191. console.log('Không tìm thấy YouTube player');
  4192. return;
  4193. }
  4194. const subtitleButton = player.querySelector('.ytp-subtitles-button') ||
  4195. document.querySelector('.ytmClosedCaptioningButtonButton');
  4196. if (!subtitleButton) {
  4197. console.log('Không tìm thấy nút caption');
  4198. return;
  4199. }
  4200. const isCaptionEnabled = subtitleButton.getAttribute('aria-pressed') === 'true';
  4201. if (!isCaptionEnabled) {
  4202. subtitleButton.click();
  4203. console.log('Đã bật caption YouTube');
  4204. await new Promise(resolve => setTimeout(resolve, this.retryDelay));
  4205. if (subtitleButton.getAttribute('aria-pressed') !== 'true') {
  4206. subtitleButton.click();
  4207. }
  4208. }
  4209. const observer = new MutationObserver((mutations) => {
  4210. mutations.forEach((mutation) => {
  4211. if (mutation.type === 'attributes' && mutation.attributeName === 'aria-pressed') {
  4212. const isEnabled = subtitleButton.getAttribute('aria-pressed') === 'true';
  4213. if (!isEnabled && this.isEnabled) {
  4214. subtitleButton.click();
  4215. }
  4216. }
  4217. });
  4218. });
  4219. observer.observe(subtitleButton, {
  4220. attributes: true,
  4221. attributeFilter: ['aria-pressed']
  4222. });
  4223. } catch (error) {
  4224. console.error('Lỗi khi bật caption YouTube:', error);
  4225. }
  4226. }
  4227. startWatchingForCaptions() {
  4228. if (!this.isEnabled) return;
  4229. this.clearCaptionWatch();
  4230. this.watchForCaptions();
  4231. this.videoCheckInterval = setInterval(() => {
  4232. if (this.isEnabled && this.isPlaying) {
  4233. this.watchForCaptions();
  4234. }
  4235. }, 1000);
  4236. }
  4237. watchForCaptions() {
  4238. if (!this.isPlaying || !this.isEnabled) return;
  4239. const currentCaption = this.getCurrentCaption();
  4240. if (currentCaption) {
  4241. if (!this.hasCaptions) {
  4242. console.log("Phát hiện caption lần đầu");
  4243. this.hasCaptions = true;
  4244. }
  4245. if (currentCaption !== this.lastCaption) {
  4246. this.processCaption(currentCaption);
  4247. this.lastCaption = currentCaption;
  4248. }
  4249. }
  4250. }
  4251. getCurrentCaption() {
  4252. if (!this.currentVideo || !this.isPlaying) return '';
  4253. this.captureVideoCaption();
  4254. }
  4255. async processCaption(caption) {
  4256. if (!caption || !this.isEnabled || !this.isPlaying) return;
  4257. try {
  4258. if (this.subtitleCache.has(caption)) {
  4259. this.updateSubtitles(this.subtitleCache.get(caption));
  4260. return;
  4261. }
  4262. const translationResult = await this.translateWithRetry(caption);
  4263. if (translationResult) {
  4264. this.subtitleCache.set(caption, {
  4265. original: caption,
  4266. translation: translationResult.translation
  4267. });
  4268. this.updateSubtitles({
  4269. original: caption,
  4270. translation: translationResult.translation
  4271. });
  4272. }
  4273. } catch (error) {
  4274. console.error('Lỗi xử lý caption:', error);
  4275. }
  4276. }
  4277. clearCaptionWatch() {
  4278. if (this.videoCheckInterval) {
  4279. clearInterval(this.videoCheckInterval);
  4280. this.videoCheckInterval = null;
  4281. }
  4282. }
  4283. clearAllWatchers() {
  4284. this.clearCaptionWatch();
  4285. if (this.captionObserver) {
  4286. this.captionObserver.disconnect();
  4287. this.captionObserver = null;
  4288. }
  4289. if (this.playingCheckTimeout) {
  4290. clearTimeout(this.playingCheckTimeout);
  4291. this.playingCheckTimeout = null;
  4292. }
  4293. }
  4294. async captureVideoCaption() {
  4295. if (!this.currentVideo) return '';
  4296. try {
  4297. const { config } = this.platformInfo;
  4298. const containers = document.querySelectorAll(config.captionSelector);
  4299. if (!containers || containers.length === 0) {
  4300. if (this.currentVideo.textTracks) {
  4301. const tracks = Array.from(this.currentVideo.textTracks);
  4302. for (const track of tracks) {
  4303. if (track.mode === 'showing' && track.activeCues?.length > 0) {
  4304. return Array.from(track.activeCues)
  4305. .map(cue => cue.text.trim())
  4306. .filter(text => text.length > 0)
  4307. .join(' ');
  4308. }
  4309. }
  4310. }
  4311. return '';
  4312. }
  4313. return Array.from(containers)
  4314. .map(container => container.textContent.trim())
  4315. .filter(text => text.length > 0)
  4316. .join(' ');
  4317. } catch (error) {
  4318. console.error('Lỗi khi lấy caption:', error);
  4319. return '';
  4320. }
  4321. }
  4322. async processVideoFrame() {
  4323. const isVideoActive = this.currentVideo &&
  4324. this.isEnabled &&
  4325. (this.isPlaying || this.currentVideo.playing);
  4326. if (!isVideoActive) return;
  4327. try {
  4328. const currentCaption = await this.captureVideoCaption();
  4329. if (currentCaption && currentCaption !== this.lastCaption) {
  4330. }
  4331. if (!currentCaption?.trim()) {
  4332. if (this.subtitleContainer) {
  4333. this.subtitleContainer.textContent = '';
  4334. }
  4335. return;
  4336. }
  4337. if (currentCaption?.trim() === this.lastCaption?.trim()) {
  4338. console.log("Caption không thay đổi, bỏ qua");
  4339. return;
  4340. }
  4341. this.lastCaption = currentCaption;
  4342. if (this.subtitleCache.has(currentCaption)) {
  4343. console.log("Sử dụng bản dịch từ cache");
  4344. this.updateSubtitles(this.subtitleCache.get(currentCaption));
  4345. return;
  4346. }
  4347. let retryCount = 0;
  4348. let translationResult = null;
  4349. while (retryCount < 3 && !translationResult) {
  4350. try {
  4351. translationResult = await this.translateWithRetry(currentCaption);
  4352. } catch (error) {
  4353. console.error(`Li dch ln ${retryCount + 1}:`, error);
  4354. retryCount++;
  4355. if (retryCount < 3) {
  4356. await new Promise(resolve => setTimeout(resolve, this.retryDelay));
  4357. }
  4358. }
  4359. }
  4360. if (!translationResult) {
  4361. throw new Error("Không thể dịch caption sau nhiều lần thử");
  4362. }
  4363. this.subtitleCache.set(currentCaption, {
  4364. original: currentCaption,
  4365. translation: translationResult.translation
  4366. });
  4367. this.updateSubtitles({
  4368. original: currentCaption,
  4369. translation: translationResult.translation
  4370. });
  4371. } catch (error) {
  4372. console.error('Lỗi xử lý frame:', error);
  4373. if (this.subtitleContainer) {
  4374. this.subtitleContainer.textContent = 'Đang khởi động lại dịch...';
  4375. setTimeout(() => this.start(), this.retryDelay);
  4376. }
  4377. }
  4378. }
  4379. async translateWithRetry(text, retryCount = 0) {
  4380. const apiKeys = this.settings.apiKey[this.settings.apiProvider];
  4381. try {
  4382. if (!this.keyIndex) this.keyIndex = Math.floor(Math.random() * apiKeys.length);
  4383. const currentKey = apiKeys[this.keyIndex];
  4384. const rateLimitInfo = this.rateLimitedKeys.get(currentKey);
  4385. if (rateLimitInfo && Date.now() < rateLimitInfo.resetTime) {
  4386. this.rotateToNextKey();
  4387. return this.translateWithRetry(text, retryCount);
  4388. }
  4389. const translation = await this.makeTranslationRequest(text, currentKey);
  4390. return translation;
  4391. } catch (error) {
  4392. if (error.status === 429 || error.status === 403) {
  4393. let resetAfter = parseInt(error.headers?.['retry-after']) * 1000 || 60000;
  4394. this.rateLimitedKeys.set(apiKeys[this.keyIndex], {
  4395. resetTime: Date.now() + resetAfter,
  4396. status: error.status,
  4397. message: error.message
  4398. });
  4399. this.rotateToNextKey();
  4400. await new Promise(resolve => setTimeout(resolve, this.retryDelay));
  4401. return this.translateWithRetry(text, retryCount + 1);
  4402. }
  4403. throw error;
  4404. }
  4405. }
  4406. createLiveCaptionPrompt(text) {
  4407. const targetLang = this.settings.displayOptions.targetLanguage;
  4408. return `Dch ph đề video này sang "${targetLang}". Ch tr v bn dch, không gii thích:
  4409. "${text}"`;
  4410. }
  4411. async makeTranslationRequest(text, apiKey) {
  4412. const mediaSettings = this.settings.mediaOptions;
  4413. const generationConfig = {
  4414. temperature: mediaSettings.temperature,
  4415. topP: mediaSettings.topP,
  4416. topK: mediaSettings.topK
  4417. };
  4418. const selectedModel = this.translator.api.getGeminiModel();
  4419. const prompt = this.createLiveCaptionPrompt(text);
  4420. return new Promise((resolve, reject) => {
  4421. GM_xmlhttpRequest({
  4422. method: "POST",
  4423. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${apiKey}`,
  4424. headers: { "Content-Type": "application/json" },
  4425. data: JSON.stringify({
  4426. contents: [{
  4427. parts: [{ text: prompt }]
  4428. }],
  4429. generationConfig: generationConfig
  4430. }),
  4431. onload: (response) => {
  4432. if (response.status === 200) {
  4433. try {
  4434. const result = JSON.parse(response.responseText);
  4435. if (result?.candidates?.[0]?.content?.parts?.[0]?.text) {
  4436. resolve({
  4437. original: text,
  4438. translation: result.candidates[0].content.parts[0].text.trim()
  4439. });
  4440. } else {
  4441. reject(new Error("Invalid response format"));
  4442. }
  4443. } catch (error) {
  4444. reject(new Error("Failed to parse response"));
  4445. }
  4446. } else if (response.status === 429 || response.status === 403) {
  4447. const error = new Error("Rate limit exceeded");
  4448. error.status = response.status;
  4449. error.headers = response.headers;
  4450. reject(error);
  4451. } else {
  4452. reject(new Error(`API Error: ${response.status}`));
  4453. }
  4454. },
  4455. onerror: (error) => reject(error)
  4456. });
  4457. });
  4458. }
  4459. rotateToNextKey() {
  4460. const apiKeys = this.settings.apiKey[this.settings.apiProvider];
  4461. this.keyIndex = (this.keyIndex + 1) % apiKeys.length;
  4462. }
  4463. async start() {
  4464. if (this.initialized) return;
  4465. this.isEnabled = true;
  4466. if (!this.currentVideo) {
  4467. const videoSelectors = [
  4468. 'video',
  4469. '[data-video-player]',
  4470. '.video-player video',
  4471. '.player video',
  4472. '.html5-video-player',
  4473. '.html5-main-video',
  4474. '.video-stream',
  4475. '.video-stream html5-main-video',
  4476. '.html5-video-container video',
  4477. '#movie_player',
  4478. '#player-api_VORAPI_ELEMENT_ID',
  4479. '.nf-player video',
  4480. '.video-viewer--container--3yIje',
  4481. 'video[src]',
  4482. 'video[data-src]',
  4483. '.video-js video',
  4484. '.plyr video'
  4485. ];
  4486. for (const selector of videoSelectors) {
  4487. const video = document.querySelector(selector);
  4488. if (video && !video.dataset.translatorVideoId) {
  4489. this.currentVideo = video;
  4490. console.log("Tìm thấy video element mới:", selector);
  4491. break;
  4492. }
  4493. }
  4494. }
  4495. if (!this.currentVideo) {
  4496. console.log("Không tìm thấy video element");
  4497. return;
  4498. }
  4499. this.createVideoControls();
  4500. console.log("Đã khởi động dịch phụ đề");
  4501. }
  4502. cleanupVideo() {
  4503. if (this.currentVideo) {
  4504. delete this.currentVideo.dataset.translatorVideoId;
  4505. const videoEvents = ['play', 'playing', 'pause', 'ended'];
  4506. videoEvents.forEach(event => {
  4507. const handler = this.currentVideo[`${event}Handler`];
  4508. if (handler) {
  4509. this.currentVideo.removeEventListener(event, handler);
  4510. delete this.currentVideo[`${event}Handler`];
  4511. }
  4512. });
  4513. }
  4514. this.clearAllWatchers();
  4515. this.initialized = false;
  4516. this.hasCaptions = false;
  4517. this.lastCaption = '';
  4518. }
  4519. stop() {
  4520. this.isEnabled = false;
  4521. this.isPlaying = false;
  4522. this.cleanupVideo();
  4523. if (this.subtitleContainer) {
  4524. this.subtitleContainer.remove();
  4525. this.subtitleContainer = null;
  4526. }
  4527. this.currentVideo = null;
  4528. this.subtitleCache.clear();
  4529. this.rateLimitedKeys.clear();
  4530. this.keyIndex = null;
  4531. console.log("Đã dừng dịch phụ đề");
  4532. }
  4533. createVideoControls() {
  4534. if (this.controlsContainer) return;
  4535. const commonSelectors = [
  4536. '.ytp-left-controls',
  4537. '.ytp-right-controls',
  4538. '#player-control-overlay',
  4539. '.player-controls-content',
  4540. '.shaka-control-bar--control-bar--gXZ1u',
  4541. ];
  4542. for (const selector of commonSelectors) {
  4543. this.videoControl = document.querySelector(selector);
  4544. if (this.videoControl) break;
  4545. }
  4546. if (!this.videoControl) this.findVideoContainer();
  4547. if (!this.videoControl || !this.videoControl.parentElement) return;
  4548. this.controlsContainer = document.createElement('div');
  4549. this.controlsContainer.className = 'video-translation-controls';
  4550. Object.assign(this.controlsContainer.style, {
  4551. zIndex: '2147483647',
  4552. display: 'flex',
  4553. gap: '10px',
  4554. });
  4555. const toggleButton = document.createElement('button');
  4556. toggleButton.className = 'video-translate-toggle';
  4557. toggleButton.innerHTML = `<img src="https://raw.githubusercontent.com/king1x32/UserScripts/refs/heads/main/kings.jpg" style="width: 24px; height: 24px;">`;
  4558. Object.assign(toggleButton.style, {
  4559. background: 'rgba(0,0,0,0.7)',
  4560. border: 'none',
  4561. borderRadius: '10px',
  4562. padding: '8px',
  4563. cursor: 'pointer',
  4564. display: 'flex',
  4565. alignItems: 'center',
  4566. justifyContent: 'center'
  4567. });
  4568. toggleButton.onclick = () => {
  4569. if (this.isEnabled) {
  4570. this.stop();
  4571. this.translator.ui.showNotification("Đã tắt dịch phụ đề video", "info");
  4572. } else {
  4573. this.start();
  4574. this.translator.ui.showNotification("Đã bật dịch phụ đề video", "success");
  4575. }
  4576. };
  4577. this.controlsContainer.appendChild(toggleButton);
  4578. this.videoControl.appendChild(this.controlsContainer);
  4579. }
  4580. findVideoContainer() {
  4581. const { config } = this.platformInfo;
  4582. const selectors = config.videoSelector;
  4583. for (const selector of selectors) {
  4584. const container = document.querySelector(selector);
  4585. if (container) return container;
  4586. }
  4587. const commonSelectors = [
  4588. '.video-container',
  4589. '.player-container',
  4590. '.video-player',
  4591. '.video-wrapper',
  4592. '[class*="player"]',
  4593. '[class*="video"]'
  4594. ];
  4595. for (const selector of commonSelectors) {
  4596. const container = this.currentVideo.closest(selector);
  4597. if (container) return container;
  4598. }
  4599. return this.currentVideo.parentElement;
  4600. }
  4601. createSubtitleContainer() {
  4602. if (this.subtitleContainer) return;
  4603. this.subtitleContainer = document.createElement('div');
  4604. this.subtitleContainer.className = 'live-caption-container translated-caption';
  4605. const videoContainer = this.findVideoContainer() || this.currentVideo;
  4606. if (videoContainer) {
  4607. console.log("Tìm thấy container video:", videoContainer);
  4608. if (getComputedStyle(videoContainer).position === 'static') {
  4609. videoContainer.style.position = 'relative';
  4610. }
  4611. const settings = this.settings.videoStreamingOptions;
  4612. Object.assign(this.subtitleContainer.style, {
  4613. zIndex: "2147483647",
  4614. display: "block",
  4615. visibility: "visible",
  4616. opacity: "1",
  4617. position: "absolute",
  4618. left: "50%",
  4619. bottom: "2%",
  4620. transform: "translateX(-50%)",
  4621. zIndex: "2147483647",
  4622. fontSize: settings.fontSize,
  4623. color: settings.textColor,
  4624. backgroundColor: settings.backgroundColor,
  4625. padding: "5px 10px",
  4626. borderRadius: "4px",
  4627. fontFamily: "Arial, sans-serif",
  4628. textAlign: "center",
  4629. maxWidth: "90%",
  4630. width: "auto",
  4631. pointerEvents: "none",
  4632. textShadow: "0px 1px 2px rgba(0, 0, 0, 0.8)",
  4633. whiteSpace: "pre-wrap",
  4634. lineHeight: '1.2'
  4635. });
  4636. videoContainer.appendChild(this.subtitleContainer);
  4637. const originalCaption = document.querySelector('.caption-window-transform');
  4638. if (originalCaption) {
  4639. originalCaption.style.position = "absolute";
  4640. originalCaption.style.opacity = "0";
  4641. originalCaption.style.visibility = "hidden";
  4642. originalCaption.style.pointerEvents = "none";
  4643. originalCaption.style.width = "1px";
  4644. originalCaption.style.height = "1px";
  4645. originalCaption.style.overflow = "hidden";
  4646. originalCaption.style.clip = "rect(0 0 0 0)";
  4647. originalCaption.style.margin = "-1px";
  4648. originalCaption.style.padding = "0";
  4649. originalCaption.style.border = "0";
  4650. }
  4651. } else {
  4652. console.error("Không tìm thấy container video phù hợp");
  4653. }
  4654. }
  4655. updateSubtitles(translationResult) {
  4656. if (!this.subtitleContainer) {
  4657. this.createSubtitleContainer();
  4658. }
  4659. const mode = this.settings.displayOptions.translationMode;
  4660. const showSource = this.settings.displayOptions.languageLearning?.showSource;
  4661. if (this.subtitleContainer) {
  4662. this.subtitleContainer.innerHTML = '';
  4663. const width = this.currentVideo.offsetWidth;
  4664. let tranWidth, origSize, transSize;
  4665. if (width <= 480) {
  4666. tranWidth = '98%';
  4667. origSize = '0.65em';
  4668. transSize = '0.7em';
  4669. } else if (width <= 962) {
  4670. tranWidth = '95%';
  4671. origSize = '0.75em';
  4672. transSize = '0.8em';
  4673. } else if (width <= 1366) {
  4674. tranWidth = '90%';
  4675. origSize = '0.85em';
  4676. transSize = '0.9em';
  4677. } else {
  4678. tranWidth = '90%';
  4679. origSize = '0.95em';
  4680. transSize = '1em';
  4681. }
  4682. this.subtitleContainer.style.maxWidth = tranWidth;
  4683. const createTextElement = (text, className, styles = {}) => {
  4684. const element = document.createElement('span');
  4685. element.className = className;
  4686. element.textContent = text;
  4687. Object.assign(element.style, styles);
  4688. return element;
  4689. }
  4690. switch (mode) {
  4691. case 'translation_only':
  4692. this.subtitleContainer.appendChild(createTextElement(translationResult.translation, 'translated-text', {
  4693. fontSize: transSize
  4694. }));
  4695. break;
  4696. case 'parallel':
  4697. this.subtitleContainer.className = 'translated-caption parallel-mode';
  4698. this.subtitleContainer.appendChild(createTextElement(translationResult.original + `\n`, 'original-text', {
  4699. fontSize: origSize,
  4700. color: '#eeeeee',
  4701. fontSize: '0.95em',
  4702. marginBottom: '6px',
  4703. opacity: '0.9',
  4704. }));
  4705. this.subtitleContainer.appendChild(createTextElement(translationResult.translation, 'translated-text', {
  4706. fontSize: transSize,
  4707. marginBottom: '2px',
  4708. }));
  4709. break;
  4710. case 'language_learning':
  4711. if (showSource) {
  4712. this.subtitleContainer.className = 'translated-caption learning-mode';
  4713. this.subtitleContainer.appendChild(createTextElement(translationResult.original + `\n`, 'original-text', {
  4714. fontSize: origSize,
  4715. color: '#eeeeee',
  4716. fontSize: '0.95em',
  4717. marginBottom: '6px',
  4718. opacity: '0.9',
  4719. }));
  4720. }
  4721. this.subtitleContainer.appendChild(createTextElement(translationResult.translation, 'translated-text', {
  4722. fontSize: transSize,
  4723. marginBottom: '2px',
  4724. }));
  4725. break;
  4726. }
  4727. }
  4728. }
  4729. cleanup() {
  4730. this.stop();
  4731. this.subtitleCache.clear();
  4732. }
  4733. }
  4734. class PageTranslator {
  4735. constructor(translator) {
  4736. this.translator = translator;
  4737. this.MIN_TEXT_LENGTH = 100;
  4738. this.originalTexts = new Map();
  4739. this.isTranslated = false;
  4740. this.languageCode = this.detectLanguage().languageCode;
  4741. this.pageCache = new Map();
  4742. this.pdfLoaded = true;
  4743. }
  4744. getExcludeSelectors() {
  4745. const settings = this.translator.userSettings.settings.pageTranslation;
  4746. if (!settings.useCustomSelectors) {
  4747. return settings.defaultSelectors;
  4748. }
  4749. return settings.combineWithDefault
  4750. ? [
  4751. ...new Set([
  4752. ...settings.defaultSelectors,
  4753. ...settings.customSelectors,
  4754. ]),
  4755. ]
  4756. : settings.customSelectors;
  4757. }
  4758. async makeTranslationRequest(text) {
  4759. const apiKeys = this.translator.userSettings.settings.apiKey[this.translator.userSettings.settings.apiProvider];
  4760. const apiKey = apiKeys[Math.floor(Math.random() * apiKeys.length)];;
  4761. const selectedModel = this.translator.api.getGeminiModel();
  4762. const prompt =
  4763. "Detect language of this text and return only ISO code (e.g. 'en', 'vi'): " +
  4764. text;
  4765. return new Promise((resolve, reject) => {
  4766. GM_xmlhttpRequest({
  4767. method: "POST",
  4768. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${apiKey}`,
  4769. headers: { "Content-Type": "application/json" },
  4770. data: JSON.stringify({
  4771. contents: [{
  4772. parts: [{ text: prompt }]
  4773. }],
  4774. }),
  4775. onload: (response) => {
  4776. if (response.status === 200) {
  4777. try {
  4778. const result = JSON.parse(response.responseText);
  4779. if (result?.candidates?.[0]?.content?.parts?.[0]?.text) {
  4780. resolve(result.candidates[0].content.parts[0].text.trim());
  4781. } else {
  4782. reject(new Error("Invalid response format"));
  4783. }
  4784. } catch (error) {
  4785. reject(new Error("Failed to parse response"));
  4786. }
  4787. } else if (response.status === 429) {
  4788. const error = new Error("Rate limit exceeded");
  4789. error.status = response.status;
  4790. error.headers = response.headers;
  4791. reject(error);
  4792. } else {
  4793. reject(new Error(`API Error: ${response.status}`));
  4794. }
  4795. },
  4796. onerror: (error) => reject(error)
  4797. });
  4798. });
  4799. }
  4800. async detectLanguage() {
  4801. try {
  4802. let text = "";
  4803. if (document.body.innerText) {
  4804. text = document.body.innerText;
  4805. }
  4806. if (!text) {
  4807. const paragraphs = document.querySelectorAll("p");
  4808. paragraphs.forEach((p) => {
  4809. text += p.textContent + " ";
  4810. });
  4811. }
  4812. if (!text) {
  4813. const headings = document.querySelectorAll("h1, h2, h3");
  4814. headings.forEach((h) => {
  4815. text += h.textContent + " ";
  4816. });
  4817. }
  4818. if (!text) {
  4819. text = document.title;
  4820. }
  4821. text = text.slice(0, 1000).trim();
  4822. if (!text.trim()) {
  4823. throw new Error("Không tìm thấy nội dung để phát hiện ngôn ngữ");
  4824. }
  4825. const response = await this.makeTranslationRequest(text);
  4826. this.languageCode = response.trim().toLowerCase();
  4827. console.log("Language Code: ", this.languageCode);
  4828. const targetLanguage =
  4829. this.translator.userSettings.settings.displayOptions.targetLanguage;
  4830. if (this.languageCode === targetLanguage) {
  4831. return {
  4832. isVietnamese: true,
  4833. languageCode: `${this.languageCode}`,
  4834. message: `Trang web đã ngôn ng ${targetLanguage}`,
  4835. };
  4836. }
  4837. return {
  4838. isVietnamese: false,
  4839. languageCode: `${this.languageCode}`,
  4840. message: `Đã phát hin ngôn ngữ: ${this.languageCode}`,
  4841. };
  4842. } catch (error) {
  4843. console.error("Language detection error:", error);
  4844. throw new Error("Không thể phát hiện ngôn ngữ: " + error.message);
  4845. }
  4846. }
  4847. async checkAndTranslate() {
  4848. try {
  4849. const settings = this.translator.userSettings.settings;
  4850. if (!settings.pageTranslation.autoTranslate) {
  4851. return {
  4852. success: false,
  4853. message: "Tự động dịch đang tắt",
  4854. };
  4855. }
  4856. const languageCheck = await this.detectLanguage();
  4857. if (languageCheck.isVietnamese) {
  4858. return {
  4859. success: false,
  4860. message: languageCheck.message,
  4861. };
  4862. }
  4863. const result = await this.translatePage();
  4864. if (result.success) {
  4865. const toolsContainer = this.$(
  4866. ".translator-tools-container"
  4867. );
  4868. if (toolsContainer) {
  4869. const menuItem = toolsContainer.querySelector(
  4870. '[data-type="pageTranslate"]'
  4871. );
  4872. if (menuItem) {
  4873. const itemText = menuItem.querySelector(".item-text");
  4874. if (itemText) {
  4875. itemText.textContent = this.isTranslated
  4876. ? "Bản gốc"
  4877. : "Dịch trang";
  4878. }
  4879. }
  4880. }
  4881. const floatingButton = this.$(
  4882. ".page-translate-button"
  4883. );
  4884. if (floatingButton) {
  4885. floatingButton.innerHTML = this.isTranslated
  4886. ? "📄 Bản gốc"
  4887. : "📄 Dịch trang";
  4888. }
  4889. this.translator.ui.showNotification(result.message, "success");
  4890. } else {
  4891. this.translator.ui.showNotification(result.message, "warning");
  4892. }
  4893. return result;
  4894. } catch (error) {
  4895. console.error("Translation check error:", error);
  4896. return {
  4897. success: false,
  4898. message: error.message,
  4899. };
  4900. }
  4901. }
  4902. async translatePage() {
  4903. try {
  4904. if (!this.domObserver) {
  4905. this.setupDOMObserver();
  4906. }
  4907. if (this.isTranslated) {
  4908. await Promise.all(
  4909. Array.from(this.originalTexts.entries()).map(async ([node, originalText]) => {
  4910. if (node && node.parentNode) {
  4911. node.textContent = originalText;
  4912. }
  4913. })
  4914. );
  4915. this.originalTexts.clear();
  4916. this.isTranslated = false;
  4917. return {
  4918. success: true,
  4919. message: "Đã chuyển về văn bản gốc"
  4920. };
  4921. }
  4922. const textNodes = this.collectTextNodes();
  4923. if (textNodes.length === 0) {
  4924. return {
  4925. success: false,
  4926. message: "Không tìm thấy nội dung cần dịch"
  4927. };
  4928. }
  4929. const chunks = this.createChunks(textNodes, 2000);
  4930. const nodeStatus = new Map();
  4931. await Promise.all(
  4932. textNodes.map(async node => {
  4933. nodeStatus.set(node, {
  4934. translated: false,
  4935. text: node.textContent
  4936. });
  4937. })
  4938. );
  4939. const results = await Promise.all(
  4940. chunks.map(async chunk => {
  4941. try {
  4942. const textsToTranslate = chunk
  4943. .map(node => node.textContent.trim())
  4944. .filter(text => text.length > 0)
  4945. .join('\n');
  4946. if (!textsToTranslate) return;
  4947. const prompt = this.translator.createPrompt(textsToTranslate, "page");
  4948. const translatedText = await this.translator.api.request(prompt, 'page');
  4949. if (!translatedText) return;
  4950. const translations = translatedText.split('\n');
  4951. await Promise.all(
  4952. chunk.map(async (node, index) => {
  4953. if (index >= translations.length) return;
  4954. const text = node.textContent.trim();
  4955. if (text.length > 0 && node.parentNode && document.contains(node)) {
  4956. try {
  4957. this.originalTexts.set(node, node.textContent);
  4958. const translated = translations[index];
  4959. const mode = this.translator.userSettings.settings.displayOptions.translationMode;
  4960. let output = this.formatTranslation(
  4961. text,
  4962. translated,
  4963. mode,
  4964. this.translator.userSettings.settings.displayOptions
  4965. );
  4966. if (await this.updateNode(node, output)) {
  4967. nodeStatus.set(node, {
  4968. translated: true,
  4969. text: node.textContent
  4970. });
  4971. }
  4972. } catch (error) {
  4973. console.error("Node update error:", error);
  4974. nodeStatus.set(node, {
  4975. translated: false,
  4976. error: "Update failed"
  4977. });
  4978. }
  4979. }
  4980. })
  4981. );
  4982. } catch (error) {
  4983. console.error("Chunk processing error:", error);
  4984. await Promise.all(
  4985. chunk.map(async node => {
  4986. nodeStatus.set(node, {
  4987. translated: false,
  4988. error: error.message
  4989. });
  4990. })
  4991. );
  4992. }
  4993. })
  4994. );
  4995. const processedResults = await Promise.all(
  4996. results.map(async result => {
  4997. if (!result) {
  4998. return {
  4999. failed: true,
  5000. error: "Translation failed"
  5001. };
  5002. }
  5003. return {
  5004. failed: false
  5005. };
  5006. })
  5007. );
  5008. const failedCount = processedResults.filter(r => r.failed).length;
  5009. this.isTranslated = true;
  5010. if (failedCount > 0) {
  5011. return {
  5012. success: true,
  5013. message: `Đã dch trang (${failedCount} phn b li)`
  5014. };
  5015. }
  5016. return {
  5017. success: true,
  5018. message: "Đã dịch xong trang"
  5019. };
  5020. } catch (error) {
  5021. console.error("Page translation error:", error);
  5022. return {
  5023. success: false,
  5024. message: error.message
  5025. };
  5026. }
  5027. }
  5028. async updateNode(node, translation) {
  5029. if (!node || !node.parentNode || !document.contains(node)) {
  5030. return false;
  5031. }
  5032. try {
  5033. node.textContent = translation;
  5034. return true;
  5035. } catch (error) {
  5036. console.error("Node update failed:", error);
  5037. return false;
  5038. }
  5039. }
  5040. createChunks(nodes, maxChunkSize = 2000) {
  5041. const chunks = [];
  5042. let currentChunk = [];
  5043. let currentLength = 0;
  5044. const isSentenceEnd = text => /[.!?。!?]$/.test(text.trim());
  5045. const isPunctuationBreak = text => /[,;,;、]$/.test(text.trim());
  5046. const isParagraphBreak = node => {
  5047. const parentTag = node.parentElement?.tagName?.toLowerCase();
  5048. return ['p', 'div', 'h1', 'h2', 'h3', 'li'].includes(parentTag);
  5049. };
  5050. for (const node of nodes) {
  5051. const text = node.textContent.trim();
  5052. if ((currentLength + text.length > maxChunkSize) && currentChunk.length > 0) {
  5053. let splitIndex = currentChunk.length - 1;
  5054. while (splitIndex > 0) {
  5055. if (isParagraphBreak(currentChunk[splitIndex])) break;
  5056. splitIndex--;
  5057. }
  5058. if (splitIndex === 0) {
  5059. splitIndex = currentChunk.length - 1;
  5060. while (splitIndex > 0) {
  5061. if (isSentenceEnd(currentChunk[splitIndex].textContent)) break;
  5062. splitIndex--;
  5063. }
  5064. }
  5065. if (splitIndex === 0) {
  5066. splitIndex = currentChunk.length - 1;
  5067. while (splitIndex > 0) {
  5068. if (isPunctuationBreak(currentChunk[splitIndex].textContent)) break;
  5069. splitIndex--;
  5070. }
  5071. }
  5072. const newChunk = currentChunk.splice(splitIndex + 1);
  5073. chunks.push(currentChunk);
  5074. currentChunk = newChunk;
  5075. currentLength = currentChunk.reduce((len, n) => len + n.textContent.trim().length, 0);
  5076. }
  5077. currentChunk.push(node);
  5078. currentLength += text.length;
  5079. const isLastNode = nodes.indexOf(node) === nodes.length - 1;
  5080. const isEndOfParagraph = isParagraphBreak(node);
  5081. if ((isLastNode || isEndOfParagraph) && currentChunk.length > 0) {
  5082. chunks.push(currentChunk);
  5083. currentChunk = [];
  5084. currentLength = 0;
  5085. }
  5086. }
  5087. if (currentChunk.length > 0) {
  5088. chunks.push(currentChunk);
  5089. }
  5090. const finalChunks = [];
  5091. let previousChunk = null;
  5092. for (const chunk of chunks) {
  5093. const chunkLength = chunk.reduce((len, node) => len + node.textContent.trim().length, 0);
  5094. if (chunkLength < maxChunkSize * 0.3 && previousChunk) {
  5095. const combinedLength = previousChunk.reduce((len, node) => len + node.textContent.trim().length, 0) + chunkLength;
  5096. if (combinedLength <= maxChunkSize) {
  5097. previousChunk.push(...chunk);
  5098. continue;
  5099. }
  5100. }
  5101. finalChunks.push(chunk);
  5102. previousChunk = chunk;
  5103. }
  5104. return finalChunks;
  5105. }
  5106. async detectContext(text) {
  5107. const prompt = `Analyze the context and writing style of this text and return JSON format with these properties:
  5108. - style: formal/informal/technical/casual
  5109. - tone: professional/friendly/neutral/academic
  5110. - domain: general/technical/business/academic/other
  5111. Text: "${text}"`;
  5112. try {
  5113. const analysis = await this.translator.api.request(prompt, "page");
  5114. const result = JSON.parse(analysis);
  5115. return {
  5116. style: result.style,
  5117. tone: result.tone,
  5118. domain: result.domain,
  5119. };
  5120. } catch (error) {
  5121. console.error("Context detection failed:", error);
  5122. return {
  5123. style: "neutral",
  5124. tone: "neutral",
  5125. domain: "general",
  5126. };
  5127. }
  5128. }
  5129. async translateHTML(htmlContent) {
  5130. try {
  5131. const parser = new DOMParser();
  5132. const doc = parser.parseFromString(htmlContent, "text/html");
  5133. const scripts = doc.getElementsByTagName("script");
  5134. const styles = doc.getElementsByTagName("style");
  5135. [...scripts, ...styles].forEach(element => element.remove());
  5136. const translatableNodes = this.getTranslatableHTMLNodes(doc.body);
  5137. const chunks = this.createChunks(translatableNodes, 2000);
  5138. this.translator.ui.showTranslatingStatus();
  5139. await Promise.all(
  5140. chunks.map(async chunk => {
  5141. try {
  5142. const textsToTranslate = await Promise.all(
  5143. chunk.map(node => node.textContent.trim())
  5144. );
  5145. const validTexts = textsToTranslate.filter(text => text.length > 0);
  5146. if (validTexts.length === 0) return;
  5147. const textToTranslate = validTexts.join("\n");
  5148. const prompt = this.translator.createPrompt(textToTranslate, "page");
  5149. const translatedText = await this.translator.api.request(prompt, 'page');
  5150. if (!translatedText) return;
  5151. const translations = translatedText.split("\n");
  5152. await Promise.all(
  5153. chunk.map(async (node, index) => {
  5154. if (index >= translations.length) return;
  5155. const text = node.textContent.trim();
  5156. if (text.length > 0 && node.parentNode && document.contains(node)) {
  5157. try {
  5158. if (node.isAttribute) {
  5159. node.ownerElement.setAttribute(node.attributeName, translations[index].trim());
  5160. } else {
  5161. node.textContent = translations[index].trim();
  5162. }
  5163. } catch (error) {
  5164. console.error("DOM update error:", error);
  5165. }
  5166. }
  5167. })
  5168. );
  5169. } catch (error) {
  5170. console.error("Chunk translation error:", error);
  5171. }
  5172. })
  5173. );
  5174. return doc.documentElement.outerHTML;
  5175. } catch (error) {
  5176. console.error("HTML translation error:", error);
  5177. throw error;
  5178. } finally {
  5179. this.translator.ui.removeTranslatingStatus();
  5180. }
  5181. }
  5182. getTranslatableHTMLNodes(element) {
  5183. const translatableNodes = [];
  5184. const excludeSelectors = this.getExcludeSelectors();
  5185. const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
  5186. acceptNode: (node) => {
  5187. const parent = node.parentElement;
  5188. if (!parent) return NodeFilter.FILTER_REJECT;
  5189. if (excludeSelectors.some((selector) => parent.matches?.(selector))) {
  5190. return NodeFilter.FILTER_REJECT;
  5191. }
  5192. return node.textContent.trim()
  5193. ? NodeFilter.FILTER_ACCEPT
  5194. : NodeFilter.FILTER_REJECT;
  5195. },
  5196. });
  5197. let node;
  5198. while ((node = walker.nextNode())) {
  5199. translatableNodes.push(node);
  5200. }
  5201. const elements = element.getElementsByTagName("*");
  5202. const translatableAttributes = ["title", "alt", "placeholder"];
  5203. for (const el of elements) {
  5204. for (const attr of translatableAttributes) {
  5205. if (el.hasAttribute(attr)) {
  5206. const value = el.getAttribute(attr);
  5207. if (value && value.trim()) {
  5208. const node = document.createTextNode(value);
  5209. node.isAttribute = true;
  5210. node.attributeName = attr;
  5211. node.ownerElement = el;
  5212. translatableNodes.push(node);
  5213. }
  5214. }
  5215. }
  5216. }
  5217. return translatableNodes;
  5218. }
  5219. async loadPDFJS() {
  5220. if (!this.pdfLoaded) {
  5221. pdfjsLib.GlobalWorkerOptions.workerSrc =
  5222. "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
  5223. this.pdfLoaded = true;
  5224. }
  5225. }
  5226. async translatePDF(file) {
  5227. try {
  5228. await this.loadPDFJS();
  5229. const arrayBuffer = await file.arrayBuffer();
  5230. const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
  5231. let translatedContent = [];
  5232. const totalPages = pdf.numPages;
  5233. const canvas = document.createElement("canvas");
  5234. const ctx = canvas.getContext("2d");
  5235. const { translationMode: mode } = this.translator.userSettings.settings.displayOptions;
  5236. const showSource = mode === "language_learning" &&
  5237. this.translator.userSettings.settings.displayOptions.languageLearning.showSource;
  5238. for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
  5239. const page = await pdf.getPage(pageNum);
  5240. const viewport = page.getViewport({ scale: 2.0 });
  5241. canvas.height = viewport.height;
  5242. canvas.width = viewport.width;
  5243. await page.render({
  5244. canvasContext: ctx,
  5245. viewport: viewport,
  5246. }).promise;
  5247. const imageBlob = await new Promise((resolve) =>
  5248. canvas.toBlob(resolve, "image/png")
  5249. );
  5250. const imageFile = new File([imageBlob], "page.png", {
  5251. type: "image/png",
  5252. });
  5253. try {
  5254. const ocrResult = await this.translator.ocr.processImage(imageFile);
  5255. const processedTranslations = ocrResult.split('\n').map((trans) => {
  5256. switch (mode) {
  5257. case "translation_only":
  5258. return `${trans.split("<|>")[0]?.trim() || ''} `;
  5259. case "parallel":
  5260. return `[GC]: ${trans.split("<|>")[0]?.trim() || ''} [DCH]: ${trans.split("<|>")[2]?.trim() || ''} `;
  5261. case "language_learning":
  5262. let parts = [];
  5263. if (showSource) {
  5264. parts.push(`[GC]: ${trans.split("<|>")[0]?.trim() || ''}`);
  5265. }
  5266. const pinyin = trans.split("<|>")[1]?.trim();
  5267. if (pinyin) {
  5268. parts.push(`[PINYIN]: ${pinyin}`);
  5269. }
  5270. const translation = trans.split("<|>")[2]?.trim() || trans;
  5271. parts.push(`[DCH]: ${translation} `);
  5272. return parts.join(" ");
  5273. default:
  5274. return trans;
  5275. }
  5276. });
  5277. translatedContent.push({
  5278. pageNum,
  5279. original: ocrResult,
  5280. translations: processedTranslations,
  5281. displayMode: mode,
  5282. showSource
  5283. });
  5284. } catch (error) {
  5285. console.error(`Error processing page ${pageNum}:`, error);
  5286. translatedContent.push({
  5287. pageNum,
  5288. original: `[Error on page ${pageNum}: ${error.message}]`,
  5289. translations: [{
  5290. original: "",
  5291. translation: `[Translation Error: ${error.message}]`
  5292. }],
  5293. displayMode: mode,
  5294. showSource
  5295. });
  5296. }
  5297. this.translator.ui.updateProgress(
  5298. "Đang xử lý PDF",
  5299. Math.round((pageNum / totalPages) * 100)
  5300. );
  5301. ctx.clearRect(0, 0, canvas.width, canvas.height);
  5302. }
  5303. canvas.remove();
  5304. return this.generateEnhancedTranslatedPDF(translatedContent);
  5305. } catch (error) {
  5306. console.error("PDF translation error:", error);
  5307. throw error;
  5308. }
  5309. }
  5310. generateEnhancedTranslatedPDF(translatedContent) {
  5311. const htmlContent = `
  5312. <!DOCTYPE html>
  5313. <html>
  5314. <head>
  5315. <meta charset="UTF-8">
  5316. <style>
  5317. body {
  5318. font-family: Arial, sans-serif;
  5319. line-height: 1.6;
  5320. max-width: 900px;
  5321. margin: 0 auto;
  5322. padding: 20px;
  5323. }
  5324. .page {
  5325. margin-bottom: 40px;
  5326. padding: 20px;
  5327. border: 1px solid #ddd;
  5328. border-radius: 8px;
  5329. page-break-after: always;
  5330. }
  5331. .page-number {
  5332. font-size: 18px;
  5333. font-weight: bold;
  5334. margin-bottom: 20px;
  5335. color: #666;
  5336. }
  5337. .content {
  5338. margin-bottom: 20px;
  5339. }
  5340. .section {
  5341. margin-bottom: 15px;
  5342. padding: 15px;
  5343. background-color: #fff;
  5344. border: 1px solid #eee;
  5345. border-radius: 8px;
  5346. white-space: pre-wrap;
  5347. }
  5348. .section-title {
  5349. font-weight: bold;
  5350. color: #333;
  5351. margin-bottom: 10px;
  5352. }
  5353. .section-content {
  5354. white-space: pre-wrap;
  5355. line-height: 1.5;
  5356. }
  5357. h3 {
  5358. color: #333;
  5359. margin: 10px 0;
  5360. }
  5361. @media print {
  5362. .page {
  5363. page-break-after: always;
  5364. }
  5365. }
  5366. </style>
  5367. </head>
  5368. <body>
  5369. ${translatedContent.map(page => `
  5370. <div class="page">
  5371. <div class="page-number">Trang ${page.pageNum}</div>
  5372. <div class="content">
  5373. ${page.displayMode === "translation_only" ? `
  5374. <div class="section">
  5375. <div class="section-title">Bn dch:</div>
  5376. <div class="section-content">${this.formatTranslationContent(page.translations.join('\n'))}</div>
  5377. </div>
  5378. ` : page.displayMode === "parallel" ? `
  5379. <div class="section">
  5380. <div class="section-content">${this.formatTranslationContent(page.translations.join('\n'))}</div>
  5381. </div>
  5382. ` : `
  5383. ${page.showSource ? `
  5384. <div class="section">
  5385. <div class="section-title">Bn gc:</div>
  5386. <div class="section-content">${this.formatTranslationContent(page.original)}</div>
  5387. </div>
  5388. ` : ''}
  5389. ${page.translations.some(t => t.includes("[PINYIN]:")) ? `
  5390. <div class="section">
  5391. <div class="section-title">Phiên âm:</div>
  5392. <div class="section-content">${this.formatTranslationContent(
  5393. page.translations
  5394. .map(t => t.split("[PINYIN]:")[1]?.split("[DỊCH]:")[0]?.trim())
  5395. .filter(Boolean)
  5396. .join('\n')
  5397. )}</div>
  5398. </div>
  5399. ` : ''}
  5400. <div class="section">
  5401. <div class="section-title">Bn dch:</div>
  5402. <div class="section-content">${this.formatTranslationContent(
  5403. page.translations
  5404. .map(t => t.split("[DỊCH]:")[1]?.trim())
  5405. .filter(Boolean)
  5406. .join('\n')
  5407. )}</div>
  5408. </div>
  5409. `}
  5410. </div>
  5411. </div>
  5412. `).join('')}
  5413. </body>
  5414. </html>
  5415. `;
  5416. return new Blob([htmlContent], { type: "text/html" });
  5417. }
  5418. formatTranslationContent(content) {
  5419. if (!content) return '';
  5420. return content
  5421. .replace(/&/g, '&amp;')
  5422. .replace(/</g, '&lt;')
  5423. .replace(/>/g, '&gt;')
  5424. .replace(/"/g, '&quot;')
  5425. .replace(/'/g, '&#039;')
  5426. .replace(/\n/g, '<br>');
  5427. }
  5428. collectTextNodes() {
  5429. const excludeSelectors = this.getExcludeSelectors();
  5430. const walker = document.createTreeWalker(
  5431. document.body,
  5432. NodeFilter.SHOW_TEXT,
  5433. {
  5434. acceptNode: (node) => {
  5435. if (!node.textContent.trim()) {
  5436. return NodeFilter.FILTER_REJECT;
  5437. }
  5438. if (!node.parentNode) {
  5439. return NodeFilter.FILTER_REJECT;
  5440. }
  5441. let parent = node.parentElement;
  5442. while (parent) {
  5443. for (const selector of excludeSelectors) {
  5444. try {
  5445. if (parent.matches && parent.matches(selector)) {
  5446. return NodeFilter.FILTER_REJECT;
  5447. }
  5448. } catch (e) {
  5449. console.warn(`Invalid selector: ${selector}`, e);
  5450. }
  5451. }
  5452. if (
  5453. parent.getAttribute("translate") === "no" ||
  5454. parent.getAttribute("class")?.includes("notranslate") ||
  5455. parent.getAttribute("class")?.includes("no-translate")
  5456. ) {
  5457. return NodeFilter.FILTER_REJECT;
  5458. }
  5459. parent = parent.parentElement;
  5460. }
  5461. return NodeFilter.FILTER_ACCEPT;
  5462. },
  5463. }
  5464. );
  5465. const nodes = [];
  5466. let node;
  5467. while ((node = walker.nextNode())) {
  5468. nodes.push(node);
  5469. }
  5470. return nodes;
  5471. }
  5472. setupDOMObserver() {
  5473. if (this.domObserver) {
  5474. this.domObserver.disconnect();
  5475. this.domObserver = null;
  5476. }
  5477. this.domObserver = new MutationObserver((mutations) => {
  5478. const newTextNodes = [];
  5479. for (const mutation of mutations) {
  5480. if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
  5481. const nodes = this.getTextNodesFromNodeList(mutation.addedNodes);
  5482. if (nodes.length > 0) {
  5483. newTextNodes.push(...nodes);
  5484. }
  5485. }
  5486. }
  5487. if (newTextNodes.length > 0) {
  5488. const chunks = this.createChunks(newTextNodes);
  5489. Promise.all(
  5490. chunks.map((chunk) =>
  5491. this.translateChunkParallel(chunk).catch((error) => {
  5492. console.error("Translation error for chunk:", error);
  5493. })
  5494. )
  5495. );
  5496. }
  5497. });
  5498. this.domObserver.observe(document.body, {
  5499. childList: true,
  5500. subtree: true,
  5501. characterData: true,
  5502. });
  5503. }
  5504. getTextNodesFromNodeList(nodeList) {
  5505. const excludeSelectors = this.getExcludeSelectors();
  5506. const textNodes = [];
  5507. const shouldExclude = (node) => {
  5508. if (!node) return true;
  5509. let current = node;
  5510. while (current) {
  5511. if (
  5512. current.getAttribute &&
  5513. (current.getAttribute("translate") === "no" ||
  5514. current.getAttribute("data-notranslate") ||
  5515. current.classList?.contains("notranslate") ||
  5516. current.classList?.contains("no-translate"))
  5517. ) {
  5518. return true;
  5519. }
  5520. for (const selector of excludeSelectors) {
  5521. try {
  5522. if (current.matches && current.matches(selector)) {
  5523. return true;
  5524. }
  5525. } catch (e) {
  5526. console.warn(`Invalid selector: ${selector}`, e);
  5527. }
  5528. }
  5529. current = current.parentElement;
  5530. }
  5531. return false;
  5532. };
  5533. nodeList.forEach((node) => {
  5534. if (node.nodeType === Node.TEXT_NODE) {
  5535. if (node.textContent.trim() && !shouldExclude(node.parentElement)) {
  5536. textNodes.push(node);
  5537. }
  5538. } else if (
  5539. node.nodeType === Node.ELEMENT_NODE &&
  5540. !shouldExclude(node)
  5541. ) {
  5542. const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
  5543. acceptNode: (textNode) => {
  5544. if (
  5545. textNode.textContent.trim() &&
  5546. !shouldExclude(textNode.parentElement)
  5547. ) {
  5548. return NodeFilter.FILTER_ACCEPT;
  5549. }
  5550. return NodeFilter.FILTER_REJECT;
  5551. },
  5552. });
  5553. let textNode;
  5554. while ((textNode = walker.nextNode())) {
  5555. textNodes.push(textNode);
  5556. }
  5557. }
  5558. });
  5559. return textNodes;
  5560. }
  5561. async translateChunkParallel(chunk) {
  5562. try {
  5563. const textsToTranslate = chunk
  5564. .map((node) => node.textContent.trim())
  5565. .filter((text) => text.length > 0)
  5566. .join("\n");
  5567. if (!textsToTranslate) return;
  5568. const prompt = this.translator.createPrompt(textsToTranslate, "page");
  5569. const translatedText = await this.translator.api.request(prompt, 'page');
  5570. if (translatedText) {
  5571. const translations = translatedText.split("\n");
  5572. let translationIndex = 0;
  5573. await Promise.all(chunk.map(async (node, _index) => {
  5574. const text = node.textContent.trim();
  5575. if (text.length > 0 && node.parentNode && document.contains(node)) {
  5576. try {
  5577. this.originalTexts.set(node, node.textContent);
  5578. if (translationIndex < translations.length) {
  5579. const translated = translations[translationIndex++];
  5580. const mode = this.translator.userSettings.settings.displayOptions.translationMode;
  5581. let output = this.formatTranslation(text, translated, mode, this.translator.userSettings.settings.displayOptions);
  5582. node.textContent = output;
  5583. }
  5584. } catch (error) {
  5585. console.error("DOM update error:", error);
  5586. }
  5587. }
  5588. }));
  5589. }
  5590. } catch (error) {
  5591. console.error("Chunk translation error:", error);
  5592. throw error;
  5593. }
  5594. }
  5595. formatTranslation(originalText, translatedText, mode, settings) {
  5596. const showSource = settings.languageLearning.showSource;
  5597. switch (mode) {
  5598. case "translation_only":
  5599. return translatedText;
  5600. case "parallel":
  5601. return `[GC]: ${originalText} [DCH]: ${translatedText.split("<|>")[2]?.trim() || translatedText} `;
  5602. case "language_learning":
  5603. let parts = [];
  5604. if (showSource) {
  5605. parts.push(`[GC]: ${originalText}`);
  5606. }
  5607. const pinyin = translatedText.split("<|>")[1]?.trim();
  5608. if (pinyin) {
  5609. parts.push(`[PINYIN]: ${pinyin}`);
  5610. }
  5611. const translation =
  5612. translatedText.split("<|>")[2]?.trim() || translatedText;
  5613. parts.push(`[DCH]: ${translation} `);
  5614. return parts.join(" ");
  5615. default:
  5616. return translatedText;
  5617. }
  5618. }
  5619. }
  5620. class FileCache {
  5621. constructor(maxSize, expirationTime) {
  5622. this.maxSize = maxSize;
  5623. this.expirationTime = expirationTime;
  5624. this.cache = new Map();
  5625. this.accessOrder = [];
  5626. }
  5627. async generateKey(fileData) {
  5628. const hashBuffer = await crypto.subtle.digest(
  5629. "SHA-256",
  5630. new TextEncoder().encode(fileData)
  5631. );
  5632. const hashArray = Array.from(new Uint8Array(hashBuffer));
  5633. return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
  5634. }
  5635. async set(fileData, result) {
  5636. const key = await this.generateKey(fileData);
  5637. if (this.cache.has(key)) {
  5638. const index = this.accessOrder.indexOf(key);
  5639. this.accessOrder.splice(index, 1);
  5640. this.accessOrder.push(key);
  5641. } else {
  5642. if (this.cache.size >= this.maxSize) {
  5643. const oldestKey = this.accessOrder.shift();
  5644. this.cache.delete(oldestKey);
  5645. }
  5646. this.accessOrder.push(key);
  5647. }
  5648. const compressedData = LZString.compressToUTF16(JSON.stringify({
  5649. result,
  5650. timestamp: Date.now()
  5651. }));
  5652. this.cache.set(key, compressedData);
  5653. }
  5654. async get(fileData) {
  5655. const key = await this.generateKey(fileData);
  5656. const compressedData = this.cache.get(key);
  5657. if (!compressedData) return null;
  5658. const data = JSON.parse(LZString.decompressFromUTF16(compressedData));
  5659. if (Date.now() - data.timestamp > this.expirationTime) {
  5660. this.cache.delete(key);
  5661. const index = this.accessOrder.indexOf(key);
  5662. this.accessOrder.splice(index, 1);
  5663. return null;
  5664. }
  5665. const index = this.accessOrder.indexOf(key);
  5666. this.accessOrder.splice(index, 1);
  5667. this.accessOrder.push(key);
  5668. return data.result;
  5669. }
  5670. clear() {
  5671. this.cache.clear();
  5672. this.accessOrder = [];
  5673. }
  5674. }
  5675. class TranslationCache {
  5676. constructor(maxSize, expirationTime) {
  5677. this.maxSize = maxSize;
  5678. this.expirationTime = expirationTime;
  5679. this.cache = new Map();
  5680. this.accessOrder = [];
  5681. }
  5682. generateKey(text, isAdvanced, targetLanguage) {
  5683. return `${text}_${isAdvanced}_${targetLanguage}`;
  5684. }
  5685. set(text, translation, isAdvanced, targetLanguage) {
  5686. const key = this.generateKey(text, isAdvanced, targetLanguage);
  5687. if (this.cache.has(key)) {
  5688. const index = this.accessOrder.indexOf(key);
  5689. this.accessOrder.splice(index, 1);
  5690. this.accessOrder.push(key);
  5691. } else {
  5692. if (this.cache.size >= this.maxSize) {
  5693. const oldestKey = this.accessOrder.shift();
  5694. this.cache.delete(oldestKey);
  5695. }
  5696. this.accessOrder.push(key);
  5697. }
  5698. const compressedData = LZString.compressToUTF16(JSON.stringify({
  5699. translation,
  5700. timestamp: Date.now()
  5701. }));
  5702. this.cache.set(key, compressedData);
  5703. this.saveToIndexedDB(key, compressedData);
  5704. }
  5705. get(text, isAdvanced, targetLanguage) {
  5706. const key = this.generateKey(text, isAdvanced, targetLanguage);
  5707. const compressedData = this.cache.get(key);
  5708. if (!compressedData) return null;
  5709. const data = JSON.parse(LZString.decompressFromUTF16(compressedData));
  5710. if (Date.now() - data.timestamp > this.expirationTime) {
  5711. this.cache.delete(key);
  5712. const index = this.accessOrder.indexOf(key);
  5713. this.accessOrder.splice(index, 1);
  5714. return null;
  5715. }
  5716. const index = this.accessOrder.indexOf(key);
  5717. this.accessOrder.splice(index, 1);
  5718. this.accessOrder.push(key);
  5719. return data.translation;
  5720. }
  5721. async saveToIndexedDB(key, value) {
  5722. try {
  5723. const db = await this.initDB();
  5724. const tx = db.transaction('translations', 'readwrite');
  5725. const store = tx.objectStore('translations');
  5726. await store.put({
  5727. key: key,
  5728. value: value,
  5729. timestamp: Date.now()
  5730. });
  5731. } catch (error) {
  5732. console.error('Error saving to IndexedDB:', error);
  5733. }
  5734. }
  5735. async loadFromIndexedDB() {
  5736. try {
  5737. const db = await this.initDB();
  5738. const tx = db.transaction('translations', 'readonly');
  5739. const store = tx.objectStore('translations');
  5740. const items = await store.getAll();
  5741. items.forEach(item => {
  5742. if (Date.now() - item.timestamp <= this.expirationTime) {
  5743. this.cache.set(item.key, item.value);
  5744. this.accessOrder.push(item.key);
  5745. }
  5746. });
  5747. } catch (error) {
  5748. console.error('Error loading from IndexedDB:', error);
  5749. }
  5750. }
  5751. clear() {
  5752. this.cache.clear();
  5753. this.accessOrder = [];
  5754. }
  5755. optimizeStorage() {
  5756. if (this.cache.size > this.maxSize * 0.9) {
  5757. const itemsToKeep = Math.floor(this.maxSize * 0.7);
  5758. const sortedItems = [...this.accessOrder].slice(-itemsToKeep);
  5759. const tempCache = new Map();
  5760. sortedItems.forEach((key) => {
  5761. if (this.cache.has(key)) {
  5762. tempCache.set(key, this.cache.get(key));
  5763. }
  5764. });
  5765. this.cache = tempCache;
  5766. this.accessOrder = sortedItems;
  5767. }
  5768. }
  5769. async initDB() {
  5770. if (!window.indexedDB) {
  5771. console.warn("IndexedDB not supported");
  5772. return;
  5773. }
  5774. return new Promise((resolve, reject) => {
  5775. const request = indexedDB.open("translatorCache", 1);
  5776. request.onerror = () => reject(request.error);
  5777. request.onsuccess = () => resolve(request.result);
  5778. request.onupgradeneeded = (event) => {
  5779. const db = event.target.result;
  5780. if (!db.objectStoreNames.contains("translations")) {
  5781. db.createObjectStore("translations", { keyPath: "id" });
  5782. }
  5783. };
  5784. });
  5785. }
  5786. async saveToIndexedDB(key, value) {
  5787. const db = await this.initDB();
  5788. return new Promise((resolve, reject) => {
  5789. const transaction = db.transaction(["translations"], "readwrite");
  5790. const store = transaction.objectStore("translations");
  5791. const request = store.put({ id: key, value, timestamp: Date.now() });
  5792. request.onsuccess = () => resolve();
  5793. request.onerror = () => reject(request.error);
  5794. });
  5795. }
  5796. async loadFromIndexedDB(key) {
  5797. const db = await this.initDB();
  5798. return new Promise((resolve, reject) => {
  5799. const transaction = db.transaction(["translations"], "readonly");
  5800. const store = transaction.objectStore("translations");
  5801. const request = store.get(key);
  5802. request.onsuccess = () => resolve(request.result?.value);
  5803. request.onerror = () => reject(request.error);
  5804. });
  5805. }
  5806. }
  5807. const RELIABLE_FORMATS = {
  5808. text: {
  5809. maxSize: 10 * 1024 * 1024, // 10MB
  5810. formats: [
  5811. { ext: 'txt', mime: 'text/plain' },
  5812. { ext: 'srt', mime: 'application/x-subrip' },
  5813. { ext: 'vtt', mime: 'text/vtt' }, // Phụ đề web
  5814. { ext: 'html', mime: 'text/html' },
  5815. { ext: 'md', mime: 'text/markdown' },
  5816. { ext: 'json', mime: 'application/json' },
  5817. ]
  5818. }
  5819. };
  5820. class FileManager {
  5821. constructor(translator) {
  5822. this.translator = translator;
  5823. this.supportedFormats = RELIABLE_FORMATS;
  5824. }
  5825. isValidFormat(file) {
  5826. const extension = file.name.split('.').pop().toLowerCase();
  5827. const mimeType = file.type;
  5828. return RELIABLE_FORMATS.text.formats.some(format =>
  5829. format.ext === extension || format.mime === mimeType
  5830. );
  5831. }
  5832. isValidSize(file) {
  5833. return file.size <= RELIABLE_FORMATS.text.maxSize;
  5834. }
  5835. async processFile(file) {
  5836. try {
  5837. const content = await this.readFileContent(file);
  5838. const extension = file.name.split('.').pop().toLowerCase();
  5839. switch (extension) {
  5840. case 'txt':
  5841. case 'md':
  5842. return await this.translator.translate(content);
  5843. case 'json':
  5844. return await this.processJSON(content);
  5845. case 'html':
  5846. return await this.translator.page.translateHTML(content);
  5847. case 'srt':
  5848. case 'vtt':
  5849. return await this.processSubtitle(content);
  5850. default:
  5851. throw new Error('Định dạng không được hỗ trợ');
  5852. }
  5853. } catch (error) {
  5854. throw new Error(`Li x lý file: ${error.message}`);
  5855. }
  5856. }
  5857. async processJSON(content) {
  5858. try {
  5859. const json = JSON.parse(content);
  5860. const translated = await this.translateObject(json);
  5861. return JSON.stringify(translated, null, 2);
  5862. } catch (error) {
  5863. throw new Error('Lỗi xử lý JSON');
  5864. }
  5865. }
  5866. async processSubtitle(content) {
  5867. try {
  5868. const parts = content.split('\n\n');
  5869. const translated = [];
  5870. for (const part of parts) {
  5871. const lines = part.split('\n');
  5872. if (lines.length >= 3) {
  5873. const [index, timing, ...text] = lines;
  5874. const translatedText = await this.translator.translate(text.join(' '));
  5875. translated.push([index, timing, translatedText].join('\n'));
  5876. }
  5877. }
  5878. return translated.join('\n\n');
  5879. } catch (error) {
  5880. throw new Error('Lỗi xử lý phụ đề');
  5881. }
  5882. }
  5883. async translateObject(obj) {
  5884. const translated = {};
  5885. for (const key in obj) {
  5886. if (typeof obj[key] === 'string') {
  5887. translated[key] = await this.translator.translate(obj[key]);
  5888. } else if (typeof obj[key] === 'object' && obj[key] !== null) {
  5889. translated[key] = await this.translateObject(obj[key]);
  5890. } else {
  5891. translated[key] = obj[key];
  5892. }
  5893. }
  5894. return translated;
  5895. }
  5896. readFileContent(file) {
  5897. return new Promise((resolve, reject) => {
  5898. const reader = new FileReader();
  5899. reader.onload = () => resolve(reader.result);
  5900. reader.onerror = () => reject(new Error('Không thể đọc file'));
  5901. reader.readAsText(file);
  5902. });
  5903. }
  5904. }
  5905. class UIManager {
  5906. constructor(translator) {
  5907. if (!translator) {
  5908. throw new Error("Translator instance is required");
  5909. }
  5910. this.translator = translator;
  5911. const themeMode = this.translator.userSettings.settings.theme;
  5912. const theme = CONFIG.THEME[themeMode];
  5913. const isDark = themeMode === "dark";
  5914. // this.checkShadow = document.querySelector('#king-translator-root');
  5915. // if (this.checkShadow) this.checkShadow.remove();
  5916. this.container = document.createElement('div');
  5917. this.container.id = 'king-translator-root';
  5918. this.container.style.cssText = `z-index: 2147483647;`;
  5919. this.shadowRoot = this.container.attachShadow({ mode: 'closed' });
  5920. const style = document.createElement('style');
  5921. style.textContent = `
  5922. .translator-settings-container {
  5923. z-index: 2147483647;
  5924. position: fixed;
  5925. background-color: ${theme.background};
  5926. color: ${theme.text};
  5927. padding: 20px;
  5928. border-radius: 15px;
  5929. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  5930. width: auto;
  5931. min-width: 320px;
  5932. max-width: 90vw;
  5933. max-height: 90vh;
  5934. overflow-y: auto;
  5935. top: ${window.innerHeight / 2}px;
  5936. left: ${window.innerWidth / 2}px;
  5937. transform: translate(-50%, -50%);
  5938. display: block;
  5939. visibility: visible;
  5940. opacity: 1;
  5941. font-size: 14px;
  5942. line-height: 1.4;
  5943. }
  5944. .translator-settings-container * {
  5945. font-family: Arial, sans-serif;
  5946. box-sizing: border-box;
  5947. }
  5948. .translator-settings-container input[type="checkbox"],
  5949. .translator-settings-container input[type="radio"] {
  5950. appearance: auto;
  5951. -webkit-appearance: auto;
  5952. -moz-appearance: auto;
  5953. position: relative;
  5954. width: 16px;
  5955. height: 16px;
  5956. margin: 3px 5px;
  5957. padding: 0;
  5958. accent-color: #0000aa;
  5959. border: 1px solid ${theme.border};
  5960. opacity: 1;
  5961. visibility: visible;
  5962. cursor: pointer;
  5963. }
  5964. .radio-group {
  5965. display: flex;
  5966. gap: 15px;
  5967. align-items: center;
  5968. }
  5969. .radio-group label {
  5970. flex: 1;
  5971. display: flex;
  5972. align-items: center;
  5973. justify-content: center;
  5974. padding: 5px;
  5975. gap: 5px;
  5976. }
  5977. .radio-group input[type="radio"] {
  5978. margin: 0;
  5979. position: relative;
  5980. top: 0;
  5981. }
  5982. .translator-settings-container input[type="radio"] {
  5983. border-radius: 50%;
  5984. }
  5985. .translator-settings-container input[type="checkbox"] {
  5986. display: flex;
  5987. position: relative;
  5988. margin: 5px 53% 5px 47%;
  5989. align-items: center;
  5990. justify-content: center;
  5991. }
  5992. .settings-grid input[type="text"],
  5993. .settings-grid input[type="number"],
  5994. .settings-grid select {
  5995. appearance: auto;
  5996. -webkit-appearance: auto;
  5997. -moz-appearance: auto;
  5998. background-color: ${isDark ? "#202020" : "#eeeeee"};
  5999. color: ${theme.text};
  6000. border: 1px solid ${theme.border};
  6001. border-radius: 8px;
  6002. padding: 7px 10px;
  6003. margin: 5px;
  6004. font-size: 14px;
  6005. line-height: normal;
  6006. height: auto;
  6007. width: auto;
  6008. min-width: 100px;
  6009. display: inline-block;
  6010. visibility: visible;
  6011. opacity: 1;
  6012. }
  6013. .settings-grid select {
  6014. padding-right: 20px;
  6015. }
  6016. .settings-grid label {
  6017. display: inline-flex;
  6018. align-items: center;
  6019. margin: 3px 10px;
  6020. color: ${theme.text};
  6021. cursor: pointer;
  6022. user-select: none;
  6023. }
  6024. .settings-grid input:not([type="hidden"]),
  6025. .settings-grid select,
  6026. .settings-grid textarea {
  6027. display: inline-block;
  6028. opacity: 1;
  6029. visibility: visible;
  6030. position: static;
  6031. }
  6032. .settings-grid input:disabled,
  6033. .settings-grid select:disabled {
  6034. opacity: 0.5;
  6035. cursor: not-allowed;
  6036. }
  6037. .translator-settings-container input[type="checkbox"]:hover,
  6038. .translator-settings-container input[type="radio"]:hover {
  6039. border-color: ${theme.mode === "dark" ? "#777" : "#444"};
  6040. }
  6041. .settings-grid input:focus,
  6042. .settings-grid select:focus {
  6043. outline: 2px solid rgba(74, 144, 226, 0.5);
  6044. outline-offset: 1px;
  6045. }
  6046. .settings-grid input::before,
  6047. .settings-grid input::after {
  6048. content: none;
  6049. display: none;
  6050. }
  6051. .translator-settings-container button {
  6052. display: inline-flex;
  6053. align-items: center;
  6054. justify-content: center;
  6055. gap: 8px;
  6056. line-height: 1;
  6057. }
  6058. .translator-settings-container .api-key-entry input[type="text"].gemini-key,
  6059. .translator-settings-container .api-key-entry input[type="text"].openai-key {
  6060. padding: 8px 10px;
  6061. margin: 0px 3px 3px 15px;
  6062. appearance: auto;
  6063. -webkit-appearance: auto;
  6064. -moz-appearance: auto;
  6065. font-size: 14px;
  6066. line-height: normal;
  6067. width: auto;
  6068. min-width: 100px;
  6069. display: inline-block;
  6070. visibility: visible;
  6071. opacity: 1;
  6072. border: 1px solid ${theme.border};
  6073. border-radius: 10px;
  6074. box-sizing: border-box;
  6075. font-family: Arial, sans-serif;
  6076. text-align: left;
  6077. vertical-align: middle;
  6078. background-color: ${isDark ? "#202020" : "#eeeeee"};
  6079. color: ${theme.text};
  6080. }
  6081. .translator-settings-container .api-key-entry input[type="text"].gemini-key:focus,
  6082. .translator-settings-container .api-key-entry input[type="text"].openai-key:focus {
  6083. outline: 3px solid rgba(74, 144, 226, 0.5);
  6084. outline-offset: 1px;
  6085. box-shadow: none;
  6086. }
  6087. .translator-settings-container .api-key-entry {
  6088. display: flex;
  6089. gap: 10px;
  6090. align-items: center;
  6091. }
  6092. .remove-key {
  6093. display: inline-flex;
  6094. align-items: center;
  6095. justify-content: center;
  6096. width: 24px;
  6097. height: 24px;
  6098. padding: 0;
  6099. line-height: 1;
  6100. }
  6101. .translator-settings-container::-webkit-scrollbar {
  6102. width: 8px;
  6103. }
  6104. .translator-settings-container::-webkit-scrollbar-track {
  6105. background-color: ${theme.mode === "dark" ? "#222" : "#eeeeee"};
  6106. border-radius: 8px;
  6107. }
  6108. .translator-settings-container::-webkit-scrollbar-thumb {
  6109. background-color: ${theme.mode === "dark" ? "#666" : "#888"};
  6110. border-radius: 8px;
  6111. }
  6112. .translator-tools-container {
  6113. position: fixed;
  6114. bottom: 40px;
  6115. right: 20px;
  6116. color: ${theme.text};
  6117. border-radius: 10px;
  6118. z-index: 2147483647;
  6119. display: block;
  6120. visibility: visible;
  6121. opacity: 1;
  6122. }
  6123. .translator-tools-container * {
  6124. font-family: Arial, sans-serif;
  6125. box-sizing: border-box;
  6126. }
  6127. .translator-tools-button {
  6128. display: flex;
  6129. align-items: center;
  6130. gap: 8px;
  6131. padding: 8px 11px;
  6132. border: none;
  6133. border-radius: 9px;
  6134. background-color: rgba(74,144,226,0.23);
  6135. color: white;
  6136. cursor: pointer;
  6137. transition: all 0.3s ease;
  6138. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  6139. font-size: 15px;
  6140. line-height: 1;
  6141. visibility: visible;
  6142. opacity: 1;
  6143. }
  6144. .translator-tools-dropdown {
  6145. display: none;
  6146. position: absolute;
  6147. bottom: 100%;
  6148. right: 0;
  6149. margin-bottom: 10px;
  6150. background-color: ${theme.background};
  6151. color: ${theme.text};
  6152. border-radius: 15px;
  6153. box-shadow: 0 2px 10px rgba(0,0,0,0.15);
  6154. padding: 15px 12px 9px 12px;
  6155. min-width: 222px;
  6156. z-index: 2147483647;
  6157. visibility: visible;
  6158. opacity: 1;
  6159. }
  6160. .translator-tools-item {
  6161. display: flex;
  6162. align-items: center;
  6163. gap: 10px;
  6164. padding: 10px;
  6165. margin-bottom: 5px;
  6166. cursor: pointer;
  6167. transition: all 0.2s ease;
  6168. border-radius: 10px;
  6169. background-color: ${theme.backgroundShadow};
  6170. color: ${theme.text};
  6171. border: 1px solid ${theme.border};
  6172. visibility: visible;
  6173. opacity: 1;
  6174. }
  6175. .item-icon, .item-text {
  6176. font-family: Arial, sans-serif;
  6177. visibility: visible;
  6178. opacity: 1;
  6179. }
  6180. .item-icon {
  6181. font-size: 18px;
  6182. }
  6183. .item-text {
  6184. font-size: 14px;
  6185. }
  6186. .translator-tools-item:hover {
  6187. background-color: ${theme.button.translate.background};
  6188. color: ${theme.button.translate.text};
  6189. }
  6190. .translator-tools-item:active {
  6191. transform: scale(0.98);
  6192. }
  6193. .translator-tools-button:hover {
  6194. transform: translateY(-2px);
  6195. background-color: #357abd;
  6196. }
  6197. .translator-tools-button:disabled {
  6198. opacity: 0.7;
  6199. cursor: not-allowed;
  6200. }
  6201. .translator-overlay {
  6202. position: fixed;
  6203. top: 0;
  6204. left: 0;
  6205. width: 100%;
  6206. height: 100%;
  6207. background-color: rgba(0,0,0,0.3);
  6208. z-index: 2147483647;
  6209. cursor: crosshair;
  6210. }
  6211. .translator-guide {
  6212. position: fixed;
  6213. top: 20px;
  6214. left: ${window.innerWidth / 2}px;
  6215. transform: translateX(-50%);
  6216. background-color: rgba(0,0,0,0.8);
  6217. color: white;
  6218. padding: 10px 20px;
  6219. border-radius: 8px;
  6220. font-size: 14px;
  6221. z-index: 2147483647;
  6222. }
  6223. .translator-cancel {
  6224. position: fixed;
  6225. top: 20px;
  6226. right: 20px;
  6227. background-color: #ff4444;
  6228. color: white;
  6229. border: none;
  6230. border-radius: 50%;
  6231. width: 30px;
  6232. height: 30px;
  6233. font-size: 16px;
  6234. cursor: pointer;
  6235. display: flex;
  6236. align-items: center;
  6237. justify-content: center;
  6238. z-index: 2147483647;
  6239. transition: all 0.3s ease;
  6240. }
  6241. .translator-cancel:hover {
  6242. background-color: #ff0000;
  6243. transform: scale(1.1);
  6244. }
  6245. /* Animation */
  6246. @keyframes fadeIn {
  6247. from {
  6248. opacity: 0;
  6249. transform: translateY(10px);
  6250. }
  6251. to {
  6252. opacity: 1;
  6253. transform: translateY(0);
  6254. }
  6255. }
  6256. .translator-tools-container {
  6257. animation: fadeIn 0.3s ease;
  6258. }
  6259. .translator-tools-dropdown {
  6260. animation: fadeIn 0.2s ease;
  6261. }
  6262. .translator-tools-container.hidden,
  6263. .translator-notification.hidden,
  6264. .center-translate-status.hidden {
  6265. visibility: hidden;
  6266. }
  6267. .settings-label,
  6268. .settings-section-title,
  6269. .shortcut-prefix,
  6270. .item-text,
  6271. .translator-settings-container label {
  6272. color: ${theme.text};
  6273. margin: 2px 10px;
  6274. }
  6275. .translator-settings-container input[type="text"],
  6276. .translator-settings-container input[type="number"],
  6277. .translator-settings-container select {
  6278. background-color: ${isDark ? "#202020" : "#eeeeee"};
  6279. color: ${theme.text};
  6280. }
  6281. /* Đảm bảo input không ghi đè lên label */
  6282. .translator-settings-container input {
  6283. color: inherit;
  6284. }
  6285. @keyframes spin {
  6286. 0% { transform: rotate(0deg); }
  6287. 100% { transform: rotate(360deg); }
  6288. }
  6289. .processing-spinner {
  6290. width: 30px;
  6291. height: 30px;
  6292. color: white;
  6293. border: 3px solid rgba(255,255,255,0.3);
  6294. border-radius: 50%;
  6295. border-top-color: white;
  6296. animation: spin 1s ease-in-out infinite;
  6297. margin: 0 auto 10px auto;
  6298. }
  6299. .processing-message {
  6300. margin-bottom: 10px;
  6301. font-size: 14px;
  6302. }
  6303. .processing-progress {
  6304. font-size: 12px;
  6305. opacity: 0.8;
  6306. }
  6307. .translation-div p {
  6308. margin: 5px 0;
  6309. }
  6310. .translation-div strong {
  6311. font-weight: bold;
  6312. }
  6313. .translator-context-menu {
  6314. position: fixed;
  6315. color: ${theme.text};
  6316. background-color: ${theme.background};
  6317. border-radius: 8px;
  6318. padding: 8px 8px 5px 8px;
  6319. min-width: 150px;
  6320. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  6321. z-index: 2147483647;
  6322. font-family: Arial, sans-serif;
  6323. font-size: 14px;
  6324. opacity: 0;
  6325. transform: scale(0.95);
  6326. transition: all 0.1s ease-out;
  6327. animation: menuAppear 0.15s ease-out forwards;
  6328. }
  6329. @keyframes menuAppear {
  6330. from {
  6331. opacity: 0;
  6332. transform: scale(0.95);
  6333. }
  6334. to {
  6335. opacity: 1;
  6336. transform: scale(1);
  6337. }
  6338. }
  6339. .translator-context-menu-item {
  6340. padding: 5px;
  6341. margin-bottom: 3px;
  6342. cursor: pointer;
  6343. color: ${theme.text};
  6344. background-color: ${theme.backgroundShadow};
  6345. border: 1px solid ${theme.border};
  6346. border-radius: 7px;
  6347. transition: all 0.2s ease;
  6348. display: flex;
  6349. align-items: center;
  6350. gap: 8px;
  6351. white-space: nowrap;
  6352. z-index: 2147483647;
  6353. }
  6354. .translator-context-menu-item:hover {
  6355. background-color: ${theme.button.translate.background};
  6356. color: ${theme.button.translate.text};
  6357. }
  6358. .translator-context-menu-item:active {
  6359. transform: scale(0.98);
  6360. }
  6361. .input-translate-button-container {
  6362. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
  6363. }
  6364. .input-translate-button {
  6365. font-family: inherit;
  6366. }
  6367. .translator-notification {
  6368. position: fixed;
  6369. top: 20px;
  6370. left: ${window.innerWidth / 2}px;
  6371. transform: translateX(-50%);
  6372. z-index: 2147483647;
  6373. animation: fadeInOut 2s ease;
  6374. }
  6375. /* Styles cho loading/processing status */
  6376. .center-translate-status {
  6377. position: fixed;
  6378. top: ${window.innerHeight / 2}px;
  6379. left: ${window.innerWidth / 2}px;
  6380. transform: translate(-50%, -50%);
  6381. background-color: rgba(0, 0, 0, 0.8);
  6382. color: white;
  6383. padding: 15px 25px;
  6384. border-radius: 8px;
  6385. z-index: 2147483647;
  6386. }
  6387. /* Styles cho translate button */
  6388. .translator-button {
  6389. position: fixed;
  6390. border: none;
  6391. border-radius: 8px;
  6392. padding: 5px 10px;
  6393. cursor: pointer;
  6394. z-index: 2147483647;
  6395. font-size: 14px;
  6396. }
  6397. /* Styles cho popup */
  6398. .draggable {
  6399. position: fixed;
  6400. background-color: ${theme.background};
  6401. color: ${theme.text};
  6402. border-radius: 12px;
  6403. box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  6404. z-index: 2147483647;
  6405. }
  6406. .tts-button {
  6407. position: absolute;
  6408. right: 10px;
  6409. bottom: 10px;
  6410. background: none;
  6411. border: none;
  6412. font-size: 20px;
  6413. cursor: pointer;
  6414. padding: 5px;
  6415. display: flex;
  6416. align-items: center;
  6417. justify-content: center;
  6418. transition: transform 0.2s ease;
  6419. z-index: 2147483647;
  6420. }
  6421. .tts-button:hover {
  6422. transform: scale(1.1);
  6423. }
  6424. /* Styles cho web image OCR */
  6425. .translator-overlay {
  6426. position: fixed;
  6427. top: 0;
  6428. left: 0;
  6429. width: 100%;
  6430. height: 100%;
  6431. background-color: rgba(0,0,0,0.3);
  6432. z-index: 2147483647;
  6433. }
  6434. /* Styles cho manga translation */
  6435. .manga-translation-container {
  6436. position: absolute;
  6437. top: 0;
  6438. left: 0;
  6439. pointer-events: none;
  6440. z-index: 2147483647;
  6441. }
  6442. /* Animations */
  6443. @keyframes fadeIn {
  6444. from { opacity: 0; transform: translateY(10px); }
  6445. to { opacity: 1; transform: translateY(0); }
  6446. }
  6447. @keyframes fadeInOut {
  6448. 0% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
  6449. 10% { opacity: 1; transform: translateX(-50%) translateY(0); }
  6450. 90% { opacity: 1; transform: translateX(-50%) translateY(0); }
  6451. 100% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
  6452. }
  6453. @keyframes spin {
  6454. 0% { transform: rotate(0deg); }
  6455. 100% { transform: rotate(360deg); }
  6456. }
  6457. `;
  6458. this.shadowRoot.appendChild(style);
  6459. document.body.appendChild(this.container);
  6460. this.isTranslating = false;
  6461. this.translatingStatus = null;
  6462. this.ignoreNextSelectionChange = false;
  6463. this.touchCount = 0;
  6464. this.currentTranslateButton = null;
  6465. this.isProcessing = false;
  6466. this.touchEndProcessed = false;
  6467. this.currentOverlay = null;
  6468. this.currentSelectionBox = null;
  6469. this.currentStatusContainer = null;
  6470. this.currentGuide = null;
  6471. this.currentCancelBtn = null;
  6472. this.currentStyle = null;
  6473. if (localStorage.getItem("translatorToolsEnabled") === null) {
  6474. localStorage.setItem("translatorToolsEnabled", "true");
  6475. }
  6476. this.mobileOptimizer = new MobileOptimizer(this);
  6477. this.videoStreaming = new VideoStreamingTranslator(translator);
  6478. this.page = this.translator.page;
  6479. this.ocr = this.translator.ocr;
  6480. this.media = this.translator.media;
  6481. this.handleSettingsShortcut = this.handleSettingsShortcut.bind(this);
  6482. this.handleTranslationShortcuts =
  6483. this.handleTranslationShortcuts.bind(this);
  6484. this.handleTranslateButtonClick =
  6485. this.handleTranslateButtonClick.bind(this);
  6486. this.setupClickHandlers = this.setupClickHandlers.bind(this);
  6487. this.setupSelectionHandlers = this.setupSelectionHandlers.bind(this);
  6488. this.showTranslatingStatus = this.showTranslatingStatus.bind(this);
  6489. this.removeTranslatingStatus = this.removeTranslatingStatus.bind(this);
  6490. this.resetState = this.resetState.bind(this);
  6491. this.settingsShortcutListener = this.handleSettingsShortcut;
  6492. this.translationShortcutListener = this.handleTranslationShortcuts;
  6493. this.translationButtonEnabled = true;
  6494. this.translationTapEnabled = true;
  6495. this.mediaElement = null;
  6496. this.setupEventListeners();
  6497. if (document.readyState === "complete") {
  6498. if (
  6499. this.translator.userSettings.settings.pageTranslation.autoTranslate
  6500. ) {
  6501. this.page.checkAndTranslate();
  6502. }
  6503. if (
  6504. this.translator.userSettings.settings.pageTranslation
  6505. .showInitialButton
  6506. ) {
  6507. this.setupQuickTranslateButton();
  6508. }
  6509. } else {
  6510. window.addEventListener("load", () => {
  6511. if (
  6512. this.translator.userSettings.settings.pageTranslation.autoTranslate
  6513. ) {
  6514. this.page.checkAndTranslate();
  6515. }
  6516. if (
  6517. this.translator.userSettings.settings.pageTranslation
  6518. .showInitialButton
  6519. ) {
  6520. this.setupQuickTranslateButton();
  6521. }
  6522. });
  6523. }
  6524. setTimeout(() => {
  6525. if (!this.$(".translator-tools-container")) {
  6526. const isEnabled =
  6527. localStorage.getItem("translatorToolsEnabled") === "true";
  6528. if (isEnabled) {
  6529. this.setupTranslatorTools();
  6530. }
  6531. }
  6532. }, 1500);
  6533. this.debouncedCreateButton = debounce((selection, x, y) => {
  6534. this.createTranslateButton(selection, x, y);
  6535. }, 100);
  6536. }
  6537. $(selector) {
  6538. return this.shadowRoot.querySelector(selector);
  6539. }
  6540. $$(selector) {
  6541. return this.shadowRoot.querySelectorAll(selector);
  6542. }
  6543. createCloseButton() {
  6544. const button = document.createElement("span");
  6545. button.textContent = "x";
  6546. Object.assign(button.style, {
  6547. position: "absolute",
  6548. top: "0px" /* Đẩy lên trên một chút */,
  6549. right: "0px" /* Đẩy sang phải một chút */,
  6550. cursor: "pointer",
  6551. color: "black",
  6552. fontSize: "14px",
  6553. fontWeight: "bold",
  6554. padding: "4px 8px" /* Tăng kích thước */,
  6555. lineHeight: "14px",
  6556. });
  6557. button.onclick = () => button.parentElement.remove();
  6558. return button;
  6559. }
  6560. showTranslationBelow(translatedText, targetElement, text) {
  6561. if (
  6562. targetElement.nextElementSibling?.classList.contains(
  6563. "translation-div"
  6564. )
  6565. ) {
  6566. return;
  6567. }
  6568. const settings = this.translator.userSettings.settings.displayOptions;
  6569. const mode = settings.translationMode;
  6570. const showSource = settings.languageLearning.showSource;
  6571. let formattedTranslation = "";
  6572. if (mode === "translation_only") {
  6573. formattedTranslation = translatedText;
  6574. } else if (mode === "parallel") {
  6575. formattedTranslation = `<div style="margin-bottom: 8px">Gc: ${text}</div>
  6576. <div>Dch: ${translatedText.split("<|>")[2] || translatedText}</div>`;
  6577. } else if (mode === "language_learning") {
  6578. let sourceHTML = "";
  6579. if (showSource) {
  6580. sourceHTML = `<div style="margin-bottom: 8px">[Gc]: ${text}</div>`;
  6581. }
  6582. formattedTranslation = `${sourceHTML}
  6583. <div>[Pinyin]: ${translatedText.split("<|>")[1] || ""}</div>
  6584. <div>[Dch]: ${translatedText.split("<|>")[2] || translatedText}</div>`;
  6585. }
  6586. const translationDiv = document.createElement("div");
  6587. translationDiv.classList.add("translation-div");
  6588. Object.assign(translationDiv.style, {
  6589. ...CONFIG.STYLES.translation,
  6590. fontSize: settings.fontSize,
  6591. });
  6592. translationDiv.innerHTML = formattedTranslation;
  6593. const themeMode = this.translator.userSettings.settings.theme;
  6594. const theme = CONFIG.THEME[themeMode];
  6595. translationDiv.appendChild(this.createCloseButton());
  6596. targetElement.insertAdjacentElement('afterend', translationDiv);
  6597. translationDiv.style.cssText = `
  6598. display: block; /* Giữ cho phần dịch không bị kéo dài hết chiều ngang */
  6599. max-width: fit-content; /* Giới hạn chiều rộng */
  6600. width: auto; /* Để nó co giãn theo nội dung */
  6601. min-width: 150px;
  6602. color: ${theme.text};
  6603. background-color: ${theme.background};
  6604. padding: 10px 20px 10px 10px;
  6605. margin-top: 10px;
  6606. border-radius: 8px;
  6607. position: relative;
  6608. z-index: 2147483647;
  6609. border: 1px solid ${theme.border};
  6610. white-space: normal; /* Cho phép xuống dòng nếu quá dài */
  6611. overflow-wrap: break-word; /* Ngắt từ nếu quá dài */
  6612. `;
  6613. }
  6614. displayPopup(
  6615. translatedText,
  6616. originalText,
  6617. title = "Bản dịch",
  6618. pinyin = ""
  6619. ) {
  6620. this.removeTranslateButton();
  6621. const settings = this.translator.userSettings.settings;
  6622. const themeMode = settings.theme;
  6623. const theme = CONFIG.THEME[themeMode];
  6624. const isDark = themeMode === "dark";
  6625. const displayOptions = settings.displayOptions;
  6626. const popup = document.createElement("div");
  6627. popup.classList.add("draggable");
  6628. const popupStyle = {
  6629. ...CONFIG.STYLES.popup,
  6630. backgroundColor: theme.background,
  6631. borderColor: theme.border,
  6632. color: theme.text,
  6633. minWidth: displayOptions.minPopupWidth,
  6634. maxWidth: displayOptions.maxPopupWidth,
  6635. fontSize: displayOptions.fontSize,
  6636. padding: "0",
  6637. overflow: "hidden",
  6638. display: "flex",
  6639. flexDirection: "column",
  6640. };
  6641. Object.assign(popup.style, popupStyle);
  6642. const dragHandle = document.createElement("div");
  6643. Object.assign(dragHandle.style, {
  6644. ...CONFIG.STYLES.dragHandle,
  6645. backgroundColor: "#2c3e50",
  6646. borderColor: "transparent",
  6647. color: "#ffffff",
  6648. padding: "12px 15px",
  6649. borderTopLeftRadius: "15px",
  6650. borderTopRightRadius: "15px",
  6651. boxShadow: "0 1px 3px rgba(0,0,0,0.12)",
  6652. });
  6653. const titleSpan = document.createElement("span");
  6654. titleSpan.textContent = title;
  6655. Object.assign(titleSpan.style, {
  6656. fontWeight: "bold",
  6657. color: "#ffffff",
  6658. fontSize: "15px",
  6659. });
  6660. const closeButton = document.createElement("span");
  6661. closeButton.innerHTML = "×";
  6662. Object.assign(closeButton.style, {
  6663. cursor: "pointer",
  6664. fontSize: "22px",
  6665. color: "#ffffff",
  6666. padding: "0 10px",
  6667. opacity: "0.8",
  6668. transition: "all 0.2s ease",
  6669. fontWeight: "bold",
  6670. display: "flex",
  6671. alignItems: "center",
  6672. justifyContent: "center",
  6673. width: "30px",
  6674. height: "30px",
  6675. borderRadius: "50%",
  6676. });
  6677. closeButton.onmouseover = () => {
  6678. Object.assign(closeButton.style, {
  6679. opacity: "1",
  6680. backgroundColor: "#ff4444",
  6681. });
  6682. };
  6683. closeButton.onmouseout = () => {
  6684. Object.assign(closeButton.style, {
  6685. opacity: "0.8",
  6686. backgroundColor: "transparent",
  6687. });
  6688. };
  6689. closeButton.onclick = () => {
  6690. speechSynthesis.cancel();
  6691. popup.remove();
  6692. };
  6693. dragHandle.appendChild(titleSpan);
  6694. dragHandle.appendChild(closeButton);
  6695. const contentContainer = document.createElement("div");
  6696. Object.assign(contentContainer.style, {
  6697. padding: "15px 20px",
  6698. maxHeight: "70vh",
  6699. overflowY: "auto",
  6700. overflowX: "hidden",
  6701. });
  6702. const scrollbarStyle = document.createElement("style");
  6703. scrollbarStyle.textContent = `
  6704. .translator-content::-webkit-scrollbar {
  6705. width: 8px;
  6706. }
  6707. .translator-content::-webkit-scrollbar-track {
  6708. background-color: ${isDark ? "#202020" : "#eeeeee"};
  6709. border-radius: 8px;
  6710. }
  6711. .translator-content::-webkit-scrollbar-thumb {
  6712. background-color: ${isDark ? "#666" : "#888"};
  6713. border-radius: 8px;
  6714. }
  6715. .translator-content::-webkit-scrollbar-thumb:hover {
  6716. background-color: ${isDark ? "#888" : "#555"};
  6717. }
  6718. `;
  6719. this.shadowRoot.appendChild(scrollbarStyle);
  6720. contentContainer.classList.add("translator-content");
  6721. const cleanedText = translatedText.replace(/(\*\*)(.*?)\1/g, `<b style="color: ${theme.text};">$2</b>`);
  6722. const textContainer = document.createElement("div");
  6723. Object.assign(textContainer.style, {
  6724. display: "flex",
  6725. flexDirection: "column",
  6726. zIndex: "2147483647",
  6727. gap: "15px"
  6728. });
  6729. const createTTSButton = (text, lang) => {
  6730. const button = document.createElement("button");
  6731. let isPlaying = false;
  6732. const updateButtonState = () => {
  6733. button.innerHTML = isPlaying ? "🔈" : "🔊";
  6734. button.title = isPlaying ? "Dừng đọc" : "Đọc văn bản";
  6735. };
  6736. Object.assign(button.style, {
  6737. background: 'none',
  6738. border: 'none',
  6739. fontSize: '20px',
  6740. cursor: 'pointer',
  6741. padding: '5px',
  6742. display: 'flex',
  6743. alignItems: 'center',
  6744. justifyContent: 'center',
  6745. transition: 'transform 0.2s ease',
  6746. marginLeft: '5px'
  6747. });
  6748. button.onmouseover = () => {
  6749. button.style.transform = 'scale(1.1)';
  6750. };
  6751. button.onmouseout = () => {
  6752. button.style.transform = 'scale(1)';
  6753. };
  6754. const utterance = new SpeechSynthesisUtterance(text);
  6755. utterance.lang = lang;
  6756. utterance.onend = () => {
  6757. isPlaying = false;
  6758. updateButtonState();
  6759. };
  6760. utterance.oncancel = () => {
  6761. isPlaying = false;
  6762. updateButtonState();
  6763. };
  6764. button.onclick = () => {
  6765. if (isPlaying) {
  6766. speechSynthesis.cancel();
  6767. isPlaying = false;
  6768. } else {
  6769. speechSynthesis.cancel();
  6770. speechSynthesis.speak(utterance);
  6771. isPlaying = true;
  6772. }
  6773. updateButtonState();
  6774. };
  6775. updateButtonState();
  6776. return button;
  6777. };
  6778. if (
  6779. (displayOptions.translationMode == "parallel" || (displayOptions.translationMode == "language_learning" && displayOptions.languageLearning.showSource === true)) && originalText
  6780. ) {
  6781. const originalContainer = document.createElement("div");
  6782. Object.assign(originalContainer.style, {
  6783. color: theme.text,
  6784. padding: "10px 15px",
  6785. backgroundColor: `${theme.backgroundShadow}`,
  6786. borderRadius: "8px",
  6787. border: `1px solid ${theme.border}`,
  6788. wordBreak: "break-word",
  6789. zIndex: "2147483647",
  6790. });
  6791. const originalHeader = document.createElement("div");
  6792. originalHeader.style.cssText = `
  6793. display: flex;
  6794. align-items: center;
  6795. margin-bottom: 5px;
  6796. `;
  6797. const originalTitle = document.createElement("div");
  6798. originalTitle.style.cssText = `
  6799. font-weight: 500;
  6800. color: ${theme.title};
  6801. `;
  6802. originalTitle.textContent = "Bản gốc:";
  6803. originalHeader.appendChild(originalTitle);
  6804. originalHeader.appendChild(createTTSButton(originalText, settings.displayOptions.sourceLanguage));
  6805. const originalContent = document.createElement("div");
  6806. originalContent.style.cssText = `
  6807. line-height: 1.5;
  6808. color: ${theme.text};
  6809. margin-left: 20px;
  6810. `;
  6811. originalContent.textContent = originalText;
  6812. originalContainer.appendChild(originalHeader);
  6813. originalContainer.appendChild(originalContent);
  6814. textContainer.appendChild(originalContainer);
  6815. }
  6816. if (
  6817. displayOptions.translationMode == "language_learning" &&
  6818. pinyin
  6819. ) {
  6820. const pinyinContainer = document.createElement("div");
  6821. Object.assign(pinyinContainer.style, {
  6822. color: theme.text,
  6823. padding: "10px 15px",
  6824. backgroundColor: `${theme.backgroundShadow}`,
  6825. borderRadius: "8px",
  6826. border: `1px solid ${theme.border}`,
  6827. wordBreak: "break-word",
  6828. zIndex: "2147483647",
  6829. });
  6830. const pinyinHeader = document.createElement("div");
  6831. pinyinHeader.style.cssText = `
  6832. display: flex;
  6833. align-items: center;
  6834. margin-bottom: 5px;
  6835. `;
  6836. const pinyinTitle = document.createElement("div");
  6837. pinyinTitle.style.cssText = `
  6838. font-weight: 500;
  6839. color: ${theme.title};
  6840. `;
  6841. pinyinTitle.textContent = "Pinyin:";
  6842. pinyinHeader.appendChild(pinyinTitle);
  6843. pinyinHeader.appendChild(createTTSButton(pinyin, settings.displayOptions.sourceLanguage));
  6844. const pinyinContent = document.createElement("div");
  6845. pinyinContent.style.cssText = `
  6846. line-height: 1.5;
  6847. color: ${theme.text};
  6848. margin-left: 20px;
  6849. `;
  6850. pinyinContent.textContent = pinyin;
  6851. pinyinContainer.appendChild(pinyinHeader);
  6852. pinyinContainer.appendChild(pinyinContent);
  6853. textContainer.appendChild(pinyinContainer);
  6854. }
  6855. const translationContainer = document.createElement("div");
  6856. Object.assign(translationContainer.style, {
  6857. color: theme.text,
  6858. padding: "10px 15px",
  6859. backgroundColor: `${theme.backgroundShadow}`,
  6860. borderRadius: "8px",
  6861. border: `1px solid ${theme.border}`,
  6862. wordBreak: "break-word",
  6863. zIndex: "2147483647",
  6864. });
  6865. const translationHeader = document.createElement("div");
  6866. translationHeader.style.cssText = `
  6867. display: flex;
  6868. align-items: center;
  6869. margin-bottom: 5px;
  6870. `;
  6871. const translationTitle = document.createElement("div");
  6872. translationTitle.style.cssText = `
  6873. font-weight: 500;
  6874. color: ${theme.title};
  6875. `;
  6876. translationTitle.textContent = "Bản dịch:";
  6877. translationHeader.appendChild(translationTitle);
  6878. translationHeader.appendChild(createTTSButton(
  6879. translatedText.split("<|>")[2]?.trim() || translatedText,
  6880. settings.displayOptions.targetLanguage
  6881. ));
  6882. const translationContent = document.createElement("div");
  6883. translationContent.style.cssText = `
  6884. line-height: 1.5;
  6885. color: ${theme.text};
  6886. margin-left: 20px;
  6887. `;
  6888. translationContent.innerHTML = this.formatTranslation(cleanedText, theme);
  6889. translationContainer.appendChild(translationHeader);
  6890. translationContainer.appendChild(translationContent);
  6891. textContainer.appendChild(translationContainer);
  6892. contentContainer.appendChild(textContainer);
  6893. popup.appendChild(dragHandle);
  6894. popup.appendChild(contentContainer);
  6895. this.makeDraggable(popup, dragHandle);
  6896. this.shadowRoot.appendChild(popup);
  6897. this.handleClickOutside = (e) => {
  6898. if (!popup.contains(e.target)) {
  6899. document.removeEventListener("click", this.handleClickOutside);
  6900. speechSynthesis.cancel();
  6901. popup.remove();
  6902. }
  6903. };
  6904. popup.addEventListener("click", (e) => {
  6905. e.stopPropagation();
  6906. });
  6907. document.addEventListener("click", this.handleClickOutside);
  6908. const handleEscape = (e) => {
  6909. if (e.key === "Escape") {
  6910. document.removeEventListener("keydown", handleEscape);
  6911. speechSynthesis.cancel();
  6912. popup.remove();
  6913. }
  6914. };
  6915. document.addEventListener("keydown", handleEscape);
  6916. }
  6917. makeDraggable(element, handle) {
  6918. let pos1 = 0,
  6919. pos2 = 0,
  6920. pos3 = 0,
  6921. pos4 = 0;
  6922. handle.onmousedown = dragMouseDown;
  6923. function dragMouseDown(e) {
  6924. e.preventDefault();
  6925. pos3 = e.clientX;
  6926. pos4 = e.clientY;
  6927. document.onmouseup = closeDragElement;
  6928. document.onmousemove = elementDrag;
  6929. }
  6930. function elementDrag(e) {
  6931. e.preventDefault();
  6932. pos1 = pos3 - e.clientX;
  6933. pos2 = pos4 - e.clientY;
  6934. pos3 = e.clientX;
  6935. pos4 = e.clientY;
  6936. element.style.top = element.offsetTop - pos2 + "px";
  6937. element.style.left = element.offsetLeft - pos1 + "px";
  6938. }
  6939. function closeDragElement() {
  6940. document.onmouseup = null;
  6941. document.onmousemove = null;
  6942. }
  6943. }
  6944. formatTranslation(text, theme) {
  6945. return text
  6946. .split("<br>")
  6947. .map((line) => {
  6948. if (line.startsWith(`<b style="color: ${theme.text};">KEYWORD</b>:`)) {
  6949. return `<h4 style="margin-bottom: 5px; color: ${theme.text};">${line}</h4>`;
  6950. }
  6951. return `<p style="margin-bottom: 10px; white-space: pre-wrap; word-wrap: break-word; text-align: justify; color: ${theme.text};">${line}</p>`;
  6952. })
  6953. .join("");
  6954. }
  6955. setupSelectionHandlers() {
  6956. if (this.isTranslating) return;
  6957. if (this.ignoreNextSelectionChange || this.isTranslating) {
  6958. this.ignoreNextSelectionChange = false;
  6959. return;
  6960. }
  6961. if (!this.translationButtonEnabled) return;
  6962. document.addEventListener('mousedown', (e) => {
  6963. if (!e.target.classList.contains('translator-button')) {
  6964. this.isSelecting = true;
  6965. this.removeTranslateButton();
  6966. }
  6967. });
  6968. document.addEventListener('mousemove', (e) => {
  6969. if (this.isSelecting) {
  6970. const selection = window.getSelection();
  6971. const selectedText = selection.toString().trim();
  6972. if (selectedText) {
  6973. this.removeTranslateButton();
  6974. this.debouncedCreateButton(selection, e.clientX, e.clientY);
  6975. }
  6976. }
  6977. });
  6978. document.addEventListener('mouseup', (e) => {
  6979. if (!e.target.classList.contains('translator-button')) {
  6980. const selection = window.getSelection();
  6981. const selectedText = selection.toString().trim();
  6982. if (selectedText) {
  6983. this.removeTranslateButton();
  6984. this.createTranslateButton(selection, e.clientX, e.clientY);
  6985. }
  6986. }
  6987. this.isSelecting = false;
  6988. });
  6989. document.addEventListener('touchend', (e) => {
  6990. if (!e.target.classList.contains('translator-button')) {
  6991. const selection = window.getSelection();
  6992. const selectedText = selection.toString().trim();
  6993. if (selectedText && e.changedTouches?.[0]) {
  6994. const touch = e.changedTouches[0];
  6995. this.createTranslateButton(selection, touch.clientX, touch.clientY);
  6996. }
  6997. }
  6998. });
  6999. }
  7000. createTranslateButton(selection, x, y) {
  7001. this.removeTranslateButton();
  7002. const button = document.createElement('button');
  7003. button.className = 'translator-button';
  7004. button.textContent = 'Dịch';
  7005. const viewportWidth = window.innerWidth;
  7006. const viewportHeight = window.innerHeight;
  7007. const buttonWidth = 60;
  7008. const buttonHeight = 30;
  7009. const padding = 10;
  7010. let left = Math.min(x + padding, viewportWidth - buttonWidth - padding);
  7011. let top = Math.min(y + 30, viewportHeight - buttonHeight - 30);
  7012. left = Math.max(padding, left);
  7013. top = Math.max(30, top);
  7014. const themeMode = this.translator.userSettings.settings.theme;
  7015. const theme = CONFIG.THEME[themeMode];
  7016. Object.assign(button.style, {
  7017. ...CONFIG.STYLES.button,
  7018. backgroundColor: theme.button.translate.background,
  7019. color: theme.button.translate.text,
  7020. position: 'fixed',
  7021. left: `${left}px`,
  7022. top: `${top}px`,
  7023. zIndex: '2147483647',
  7024. userSelect: 'none'
  7025. });
  7026. this.shadowRoot.appendChild(button);
  7027. this.currentTranslateButton = button;
  7028. this.setupClickHandlers(selection);
  7029. }
  7030. handleTranslateButtonClick = async (selection, translateType) => {
  7031. try {
  7032. const selectedText = selection.toString().trim();
  7033. if (!selectedText) {
  7034. console.log("No text selected");
  7035. return;
  7036. }
  7037. const targetElement = selection.anchorNode?.parentElement;
  7038. if (!targetElement) {
  7039. console.log("No target element found");
  7040. return;
  7041. }
  7042. this.removeTranslateButton();
  7043. this.showTranslatingStatus();
  7044. console.log("Starting translation with type:", translateType);
  7045. if (!this.translator) {
  7046. throw new Error("Translator instance not found");
  7047. }
  7048. switch (translateType) {
  7049. case "quick":
  7050. await this.translator.translate(selectedText, targetElement);
  7051. break;
  7052. case "popup":
  7053. await this.translator.translate(
  7054. selectedText,
  7055. targetElement,
  7056. false,
  7057. true
  7058. );
  7059. break;
  7060. case "advanced":
  7061. await this.translator.translate(selectedText, targetElement, true);
  7062. break;
  7063. default:
  7064. console.log("Unknown translation type:", translateType);
  7065. }
  7066. } catch (error) {
  7067. console.error("Translation error:", error);
  7068. } finally {
  7069. if (this.isDouble) {
  7070. const newSelection = window.getSelection();
  7071. if (newSelection.toString().trim()) {
  7072. this.resetState();
  7073. this.setupSelectionHandlers();
  7074. }
  7075. } else {
  7076. this.resetState();
  7077. return;
  7078. }
  7079. }
  7080. };
  7081. debug(message) {
  7082. console.log(`[UIManager] ${message}`);
  7083. }
  7084. showTranslatingStatus() {
  7085. this.debug("Showing translating status");
  7086. if (!this.shadowRoot.getElementById("translator-animation-style")) {
  7087. const style = document.createElement("style");
  7088. style.id = "translator-animation-style";
  7089. style.textContent = `
  7090. @keyframes spin {
  7091. 0% { transform: rotate(0deg); }
  7092. 100% { transform: rotate(360deg); }
  7093. }
  7094. .center-translate-status {
  7095. position: fixed;
  7096. top: ${window.innerHeight / 2}px;
  7097. left: ${window.innerWidth / 2}px;
  7098. transform: translate(-50%, -50%);
  7099. background-color: rgba(0, 0, 0, 0.8);
  7100. color: white;
  7101. padding: 15px 25px;
  7102. border-radius: 8px;
  7103. z-index: 2147483647;
  7104. display: flex;
  7105. align-items: center;
  7106. gap: 12px;
  7107. font-family: Arial, sans-serif;
  7108. font-size: 14px;
  7109. box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  7110. }
  7111. .spinner {
  7112. display: inline-block;
  7113. width: 20px;
  7114. height: 20px;
  7115. border: 3px solid rgba(255,255,255,0.3);
  7116. border-radius: 50%;
  7117. border-top-color: #ddd;
  7118. animation: spin 1s ease-in-out infinite;
  7119. }
  7120. `;
  7121. this.shadowRoot.appendChild(style);
  7122. }
  7123. this.removeTranslatingStatus();
  7124. const status = document.createElement("div");
  7125. status.className = "center-translate-status";
  7126. status.innerHTML = `
  7127. <div class="spinner" style="color: white"></div>
  7128. <span style="color: white"ang dch...</span>
  7129. `;
  7130. this.shadowRoot.appendChild(status);
  7131. this.translatingStatus = status;
  7132. this.debug("Translation status shown");
  7133. }
  7134. setupClickHandlers(selection) {
  7135. this.pressTimer = null;
  7136. this.isLongPress = false;
  7137. this.isDown = false;
  7138. this.isDouble = false;
  7139. this.lastTime = 0;
  7140. this.count = 0;
  7141. this.timer = 0;
  7142. const handleStart = (e) => {
  7143. e.preventDefault();
  7144. e.stopPropagation();
  7145. this.ignoreNextSelectionChange = true;
  7146. this.isDown = true;
  7147. this.isLongPress = false;
  7148. const currentTime = Date.now();
  7149. if (currentTime - this.lastTime < 400) {
  7150. this.count++;
  7151. clearTimeout(this.pressTimer);
  7152. clearTimeout(this.timer);
  7153. } else {
  7154. this.count = 1;
  7155. }
  7156. this.lastTime = currentTime;
  7157. this.pressTimer = setTimeout(() => {
  7158. if (!this.isDown) return;
  7159. this.isLongPress = true;
  7160. this.count = 0;
  7161. const holdType =
  7162. this.translator.userSettings.settings.clickOptions.hold
  7163. .translateType;
  7164. this.handleTranslateButtonClick(selection, holdType);
  7165. }, 500);
  7166. };
  7167. const handleEnd = (e) => {
  7168. e.preventDefault();
  7169. e.stopPropagation();
  7170. if (!this.isDown) return;
  7171. clearTimeout(this.pressTimer);
  7172. if (this.isLongPress) return;
  7173. if (this.count === 1) {
  7174. clearTimeout(this.timer);
  7175. this.timer = setTimeout(() => {
  7176. if (this.count !== 1) return;
  7177. const singleClickType =
  7178. this.translator.userSettings.settings.clickOptions.singleClick
  7179. .translateType;
  7180. this.handleTranslateButtonClick(selection, singleClickType);
  7181. }, 400);
  7182. } else if (this.count >= 2) {
  7183. this.isDouble = true;
  7184. const doubleClickType =
  7185. this.translator.userSettings.settings.clickOptions.doubleClick
  7186. .translateType;
  7187. this.handleTranslateButtonClick(selection, doubleClickType);
  7188. }
  7189. this.isDown = false;
  7190. };
  7191. this.currentTranslateButton.addEventListener("mousedown", handleStart);
  7192. this.currentTranslateButton.addEventListener("mouseup", handleEnd);
  7193. this.currentTranslateButton.addEventListener("mouseleave", () => {
  7194. if (this.translateType) {
  7195. this.resetState();
  7196. }
  7197. });
  7198. this.currentTranslateButton.addEventListener("touchstart", handleStart);
  7199. this.currentTranslateButton.addEventListener("touchend", handleEnd);
  7200. this.currentTranslateButton.addEventListener("touchcancel", () => {
  7201. if (this.translateType) {
  7202. this.resetState();
  7203. }
  7204. });
  7205. }
  7206. setupDocumentTapHandler() {
  7207. let touchCount = 0;
  7208. let touchTimer = null;
  7209. let isProcessingTouch = false;
  7210. const handleTouchStart = async (e) => {
  7211. if (this.isTranslating) return;
  7212. const touchOptions = this.translator.userSettings.settings.touchOptions;
  7213. if (!touchOptions?.enabled) return;
  7214. const target = e.target;
  7215. if (
  7216. target.closest(".translation-div") ||
  7217. target.closest(".draggable")
  7218. ) {
  7219. return;
  7220. }
  7221. if (touchTimer) {
  7222. clearTimeout(touchTimer);
  7223. }
  7224. touchCount = e.touches.length;
  7225. touchTimer = setTimeout(async () => {
  7226. if (isProcessingTouch) return;
  7227. switch (touchCount) {
  7228. case 2:
  7229. const twoFingersType = touchOptions.twoFingers?.translateType;
  7230. if (twoFingersType) {
  7231. const selection = window.getSelection();
  7232. const selectedText = selection?.toString().trim();
  7233. if (selectedText) {
  7234. e.preventDefault();
  7235. await this.handleTranslateButtonClick(
  7236. selection,
  7237. twoFingersType
  7238. );
  7239. }
  7240. }
  7241. break;
  7242. case 3:
  7243. const threeFingersType = touchOptions.threeFingers?.translateType;
  7244. if (threeFingersType) {
  7245. const selection = window.getSelection();
  7246. const selectedText = selection?.toString().trim();
  7247. if (selectedText) {
  7248. e.preventDefault();
  7249. await this.handleTranslateButtonClick(
  7250. selection,
  7251. threeFingersType
  7252. );
  7253. }
  7254. }
  7255. break;
  7256. case 4:
  7257. e.preventDefault();
  7258. const settingsUI =
  7259. this.translator.userSettings.createSettingsUI();
  7260. this.shadowRoot.appendChild(settingsUI);
  7261. break;
  7262. case 5:
  7263. e.preventDefault();
  7264. isProcessingTouch = true;
  7265. this.toggleTranslatorTools();
  7266. setTimeout(() => {
  7267. isProcessingTouch = false;
  7268. }, 350);
  7269. break;
  7270. }
  7271. touchCount = 0;
  7272. touchTimer = null;
  7273. }, touchOptions.sensitivity || 100);
  7274. };
  7275. const handleTouch = () => {
  7276. if (touchTimer) {
  7277. clearTimeout(touchTimer);
  7278. touchTimer = null;
  7279. }
  7280. touchCount = 0;
  7281. };
  7282. document.addEventListener("touchstart", handleTouchStart.bind(this), {
  7283. passive: false,
  7284. });
  7285. document.addEventListener("touchend", handleTouch.bind(this));
  7286. document.addEventListener("touchcancel", handleTouch.bind(this));
  7287. }
  7288. toggleTranslatorTools() {
  7289. if (this.isTogglingTools) return;
  7290. this.isTogglingTools = true;
  7291. try {
  7292. const currentState =
  7293. localStorage.getItem("translatorToolsEnabled") === "true";
  7294. const newState = !currentState;
  7295. localStorage.setItem("translatorToolsEnabled", newState.toString());
  7296. const settings = this.translator.userSettings.settings;
  7297. settings.showTranslatorTools.enabled = newState;
  7298. this.translator.userSettings.saveSettings();
  7299. this.removeToolsContainer();
  7300. this.resetState();
  7301. const overlays = this.$$(".translator-overlay");
  7302. overlays.forEach((overlay) => overlay.remove());
  7303. if (newState) {
  7304. this.setupTranslatorTools();
  7305. }
  7306. this.showNotification(
  7307. newState ? "Đã bật Translator Tools" : "Đã tắt Translator Tools"
  7308. );
  7309. } finally {
  7310. setTimeout(() => {
  7311. this.isTogglingTools = false;
  7312. }, 350);
  7313. }
  7314. }
  7315. removeToolsContainer() {
  7316. const container = this.$('.translator-tools-container');
  7317. if (container) {
  7318. const inputs = container.querySelectorAll('input');
  7319. inputs.forEach(input => {
  7320. input.removeEventListener('change', this.handleOCRInput);
  7321. input.removeEventListener('change', this.handleMediaInput);
  7322. });
  7323. container.remove();
  7324. }
  7325. }
  7326. async handlePageTranslation() {
  7327. const settings = this.translator.userSettings.settings;
  7328. if (!settings.pageTranslation?.enabled && !settings.shortcuts?.enabled) {
  7329. this.showNotification("Tính năng dịch trang đang bị tắt", "warning");
  7330. return;
  7331. }
  7332. try {
  7333. this.showTranslatingStatus();
  7334. const result = await this.page.translatePage();
  7335. if (result.success) {
  7336. const toolsContainer = this.$(
  7337. ".translator-tools-container"
  7338. );
  7339. if (toolsContainer) {
  7340. const menuItem = toolsContainer.querySelector(
  7341. '[data-type="pageTranslate"]'
  7342. );
  7343. if (menuItem) {
  7344. const itemText = menuItem.querySelector(".item-text");
  7345. if (itemText) {
  7346. itemText.textContent = this.page.isTranslated
  7347. ? "Bản gốc"
  7348. : "Dịch trang";
  7349. }
  7350. }
  7351. }
  7352. const floatingButton = this.$(
  7353. ".page-translate-button"
  7354. );
  7355. if (floatingButton) {
  7356. floatingButton.innerHTML = this.page.isTranslated
  7357. ? "📄 Bản gốc"
  7358. : "📄 Dịch trang";
  7359. }
  7360. this.showNotification(result.message, "success");
  7361. } else {
  7362. this.showNotification(result.message, "warning");
  7363. }
  7364. } catch (error) {
  7365. console.error("Page translation error:", error);
  7366. this.showNotification(error.message, "error");
  7367. } finally {
  7368. this.removeTranslatingStatus();
  7369. }
  7370. }
  7371. setupQuickTranslateButton() {
  7372. const settings = this.translator.userSettings.settings;
  7373. if (!settings.pageTranslation?.enabled && !settings.shortcuts?.enabled) {
  7374. this.showNotification("Tính năng dịch trang đang bị tắt", "warning");
  7375. return;
  7376. }
  7377. const style = document.createElement("style");
  7378. style.textContent = `
  7379. .page-translate-button {
  7380. position: fixed;
  7381. bottom: 20px;
  7382. left: 20px;
  7383. z-index: 2147483647;
  7384. padding: 8px 16px;
  7385. background-color: #4CAF50;
  7386. color: white;
  7387. border: none;
  7388. border-radius: 8px;
  7389. cursor: pointer;
  7390. font-size: 14px;
  7391. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  7392. transition: all 0.3s ease;
  7393. }
  7394. .page-translate-button:hover {
  7395. background-color: #45a030;
  7396. transform: translateY(-2px);
  7397. }
  7398. `;
  7399. this.shadowRoot.appendChild(style);
  7400. const button = document.createElement("button");
  7401. button.className = "page-translate-button";
  7402. button.innerHTML = this.page.isTranslated
  7403. ? "📄 Bản gốc"
  7404. : "📄 Dịch trang";
  7405. button.onclick = async () => {
  7406. try {
  7407. this.showTranslatingStatus();
  7408. const result = await this.page.translatePage();
  7409. if (result.success) {
  7410. button.innerHTML = this.page.isTranslated
  7411. ? "📄 Bản gốc"
  7412. : "📄 Dịch trang";
  7413. const toolsContainer = this.$(
  7414. ".translator-tools-container"
  7415. );
  7416. if (toolsContainer) {
  7417. const menuItem = toolsContainer.querySelector(
  7418. '[data-type="pageTranslate"]'
  7419. );
  7420. if (menuItem && menuItem.querySelector(".item-text")) {
  7421. menuItem.querySelector(".item-text").textContent = this.page
  7422. .isTranslated
  7423. ? "Bản gốc"
  7424. : "Dịch trang";
  7425. }
  7426. }
  7427. this.showNotification(result.message, "success");
  7428. } else {
  7429. this.showNotification(result.message, "warning");
  7430. }
  7431. } catch (error) {
  7432. console.error("Page translation error:", error);
  7433. this.showNotification(error.message, "error");
  7434. } finally {
  7435. this.removeTranslatingStatus();
  7436. }
  7437. };
  7438. this.shadowRoot.appendChild(button);
  7439. setTimeout(() => {
  7440. if (button && button.parentNode) {
  7441. button.parentNode.removeChild(button);
  7442. }
  7443. if (style && style.parentNode) {
  7444. style.parentNode.removeChild(style);
  7445. }
  7446. }, 10000);
  7447. }
  7448. setupTranslatorTools() {
  7449. const isEnabled =
  7450. localStorage.getItem("translatorToolsEnabled") === "true";
  7451. if (!isEnabled) return;
  7452. if (this.$(".translator-tools-container")) {
  7453. return;
  7454. }
  7455. this.removeToolsContainer();
  7456. // bypassCSP();
  7457. this.createToolsContainer();
  7458. }
  7459. createToolsContainer() {
  7460. this.removeToolsContainer();
  7461. const container = document.createElement("div");
  7462. container.className = "translator-tools-container";
  7463. container.setAttribute("data-permanent", "true");
  7464. container.setAttribute("data-translator-tool", "true");
  7465. const closeButton = document.createElement("span");
  7466. closeButton.innerHTML = "×";
  7467. Object.assign(closeButton.style, {
  7468. cursor: "pointer",
  7469. fontSize: "16px",
  7470. color: "#ffffff",
  7471. backgroundColor: "rgba(85, 85, 85, 0.28)",
  7472. padding: "0 3px",
  7473. opacity: "0.8",
  7474. transition: "all 0.2s ease",
  7475. fontWeight: "bold",
  7476. display: "flex",
  7477. position: "absolute",
  7478. top: "-8px",
  7479. right: "-8px",
  7480. alignItems: "center",
  7481. justifyContent: "center",
  7482. width: "20px",
  7483. height: "20px",
  7484. borderRadius: "50%",
  7485. });
  7486. closeButton.onmouseover = () => {
  7487. Object.assign(closeButton.style, {
  7488. opacity: "1",
  7489. backgroundColor: "#ff4444",
  7490. });
  7491. };
  7492. closeButton.onmouseout = () => {
  7493. Object.assign(closeButton.style, {
  7494. opacity: "0.8",
  7495. backgroundColor: "transparent",
  7496. });
  7497. };
  7498. closeButton.onclick = () => {
  7499. this.removeToolsContainer();
  7500. };
  7501. this.handleOCRInput = async (e) => {
  7502. const file = e.target.files?.[0];
  7503. if (!file) return;
  7504. try {
  7505. this.showTranslatingStatus();
  7506. const result = await this.ocr.processImage(file);
  7507. this.removeTranslatingStatus();
  7508. if (!result) {
  7509. throw new Error("Không thể xử lý ảnh chụp màn hình");
  7510. }
  7511. const translations = result.split("\n");
  7512. let fullTranslation = "";
  7513. let pinyin = "";
  7514. let text = "";
  7515. for (const trans of translations) {
  7516. const parts = trans.split("<|>");
  7517. text += (parts[0]?.trim() || "") + "\n";
  7518. pinyin += (parts[1]?.trim() || "") + "\n";
  7519. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  7520. }
  7521. this.displayPopup(
  7522. fullTranslation.trim(),
  7523. text.trim(),
  7524. "OCR Image Local",
  7525. pinyin.trim()
  7526. );
  7527. } catch (error) {
  7528. this.showNotification(error.message, "error");
  7529. } finally {
  7530. this.removeTranslatingStatus();
  7531. }
  7532. };
  7533. this.handleMediaInput = async (e) => {
  7534. const file = e.target.files?.[0];
  7535. if (!file) return;
  7536. try {
  7537. this.showTranslatingStatus();
  7538. await this.media.processMediaFile(file);
  7539. this.removeTranslatingStatus();
  7540. } catch (error) {
  7541. this.showNotification(error.message);
  7542. } finally {
  7543. this.removeTranslatingStatus();
  7544. }
  7545. };
  7546. const ocrInput = document.createElement("input");
  7547. ocrInput.type = "file";
  7548. ocrInput.accept = "image/*";
  7549. ocrInput.style.display = "none";
  7550. ocrInput.id = "translator-ocr-input";
  7551. ocrInput.addEventListener("change", this.handleOCRInput);
  7552. const mediaInput = document.createElement("input");
  7553. mediaInput.type = "file";
  7554. mediaInput.accept = "audio/*, video/*";
  7555. mediaInput.style.display = "none";
  7556. mediaInput.id = "translator-media-input";
  7557. mediaInput.addEventListener("change", this.handleMediaInput);
  7558. const mainButton = document.createElement("button");
  7559. mainButton.className = "translator-tools-button";
  7560. mainButton.innerHTML = `
  7561. <span class="tools-icon">⚙️</span>
  7562. `;
  7563. const dropdown = document.createElement("div");
  7564. dropdown.className = "translator-tools-dropdown";
  7565. const menuItems = [];
  7566. const settings = this.translator.userSettings.settings;
  7567. if (settings.pageTranslation?.enabled) {
  7568. menuItems.push({
  7569. icon: "📄",
  7570. text: this.page.isTranslated ? "Bản gốc" : "Dịch trang",
  7571. "data-type": "pageTranslate",
  7572. handler: async () => {
  7573. try {
  7574. dropdown.style.display = "none";
  7575. this.showTranslatingStatus();
  7576. const result = await this.page.translatePage();
  7577. this.removeTranslatingStatus();
  7578. if (result.success) {
  7579. const menuItem = dropdown.querySelector(
  7580. '[data-type="pageTranslate"]'
  7581. );
  7582. if (menuItem) {
  7583. const itemText = menuItem.querySelector(".item-text");
  7584. if (itemText) {
  7585. itemText.textContent = this.page.isTranslated
  7586. ? "Bản gốc"
  7587. : "Dịch trang";
  7588. }
  7589. }
  7590. this.showNotification(result.message, "success");
  7591. } else {
  7592. this.showNotification(result.message, "warning");
  7593. }
  7594. } catch (error) {
  7595. console.error("Page translation error:", error);
  7596. this.showNotification(error.message, "error");
  7597. } finally {
  7598. this.removeTranslatingStatus();
  7599. }
  7600. },
  7601. });
  7602. }
  7603. if (settings.ocrOptions?.enabled) {
  7604. menuItems.push(
  7605. {
  7606. icon: "📸",
  7607. text: "Dịch Vùng OCR",
  7608. handler: async () => {
  7609. try {
  7610. dropdown.style.display = "none";
  7611. await new Promise((resolve) => setTimeout(resolve, 100));
  7612. console.log("Starting screen translation...");
  7613. this.showTranslatingStatus();
  7614. const screenshot = await this.ocr.captureScreen();
  7615. if (!screenshot) {
  7616. throw new Error("Không thể tạo ảnh chụp màn hình");
  7617. }
  7618. const result = await this.ocr.processImage(screenshot);
  7619. this.removeTranslatingStatus();
  7620. if (!result) {
  7621. throw new Error("Không thể xử lý ảnh chụp màn hình");
  7622. }
  7623. const translations = result.split("\n");
  7624. let fullTranslation = "";
  7625. let pinyin = "";
  7626. let text = "";
  7627. for (const trans of translations) {
  7628. const parts = trans.split("<|>");
  7629. text += (parts[0]?.trim() || "") + "\n";
  7630. pinyin += (parts[1]?.trim() || "") + "\n";
  7631. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  7632. }
  7633. this.displayPopup(
  7634. fullTranslation.trim(),
  7635. text.trim(),
  7636. "OCR Vùng Màn Hình",
  7637. pinyin.trim()
  7638. );
  7639. } catch (error) {
  7640. console.error("Screen translation error:", error);
  7641. this.showNotification(error.message, "error");
  7642. } finally {
  7643. this.removeTranslatingStatus();
  7644. }
  7645. },
  7646. },
  7647. {
  7648. icon: "🖼️",
  7649. text: "Dịch Ảnh Web",
  7650. handler: () => {
  7651. dropdown.style.display = "none";
  7652. this.startWebImageOCR();
  7653. },
  7654. },
  7655. {
  7656. icon: "📚",
  7657. text: "Dịch Manga Web",
  7658. handler: () => {
  7659. dropdown.style.display = "none";
  7660. this.startMangaTranslation();
  7661. },
  7662. },
  7663. {
  7664. icon: "📷",
  7665. text: "Dịch File Ảnh",
  7666. handler: () => ocrInput.click(),
  7667. }
  7668. );
  7669. }
  7670. if (settings.mediaOptions?.enabled) {
  7671. menuItems.push({
  7672. icon: "🎵",
  7673. text: "Dịch File Media",
  7674. handler: () => mediaInput.click(),
  7675. });
  7676. }
  7677. menuItems.push({
  7678. icon: "📄",
  7679. text: "Dịch File HTML",
  7680. handler: () => {
  7681. const input = document.createElement("input");
  7682. input.type = "file";
  7683. input.accept = ".html,.htm";
  7684. input.style.display = "none";
  7685. input.onchange = async (e) => {
  7686. const file = e.target.files[0];
  7687. if (!file) return;
  7688. try {
  7689. this.showTranslatingStatus();
  7690. const content = await this.readFileContent(file);
  7691. const translatedHTML = await this.page.translateHTML(content);
  7692. const blob = new Blob([translatedHTML], { type: "text/html" });
  7693. const url = URL.createObjectURL(blob);
  7694. const a = document.createElement("a");
  7695. a.href = url;
  7696. a.download = `king1x32_translated_${file.name}`;
  7697. a.click();
  7698. URL.revokeObjectURL(url);
  7699. this.removeTranslatingStatus();
  7700. this.showNotification("Dịch file HTML thành công", "success");
  7701. } catch (error) {
  7702. console.error("Lỗi dịch file HTML:", error);
  7703. this.showNotification(error.message, "error");
  7704. } finally {
  7705. this.removeTranslatingStatus();
  7706. }
  7707. };
  7708. input.click();
  7709. },
  7710. },
  7711. {
  7712. icon: "📑",
  7713. text: "Dịch File PDF",
  7714. handler: () => {
  7715. const input = document.createElement("input");
  7716. input.type = "file";
  7717. input.accept = ".pdf";
  7718. input.style.display = "none";
  7719. input.onchange = async (e) => {
  7720. const file = e.target.files[0];
  7721. if (!file) return;
  7722. try {
  7723. this.showLoadingStatus("Đang xử lý PDF...");
  7724. const translatedBlob = await this.page.translatePDF(file);
  7725. const url = URL.createObjectURL(translatedBlob);
  7726. const a = document.createElement("a");
  7727. a.href = url;
  7728. a.download = `king1x32_translated_${file.name.replace(".pdf", ".html")}`;
  7729. a.click();
  7730. URL.revokeObjectURL(url);
  7731. this.removeTranslatingStatus();
  7732. this.showNotification("Dịch PDF thành công", "success");
  7733. } catch (error) {
  7734. console.error("Lỗi dịch PDF:", error);
  7735. this.showNotification(error.message, "error");
  7736. } finally {
  7737. this.removeLoadingStatus();
  7738. }
  7739. };
  7740. input.click();
  7741. },
  7742. }, {
  7743. icon: "📄",
  7744. text: "Dịch File",
  7745. handler: () => {
  7746. dropdown.style.display = "none";
  7747. const supportedFormats = RELIABLE_FORMATS.text.formats.map(f => `.${f.ext}`).join(',');
  7748. const input = document.createElement("input");
  7749. input.type = "file";
  7750. input.accept = supportedFormats;
  7751. input.style.display = "none";
  7752. input.onchange = async (e) => {
  7753. const file = e.target.files[0];
  7754. if (!file) return;
  7755. try {
  7756. this.showTranslatingStatus();
  7757. const result = await this.translator.translateFile(file);
  7758. const blob = new Blob([result], { type: file.type });
  7759. const url = URL.createObjectURL(blob);
  7760. const a = document.createElement('a');
  7761. a.href = url;
  7762. a.download = `king1x32_translated_${file.name}`;
  7763. this.shadowRoot.appendChild(a);
  7764. a.click();
  7765. URL.revokeObjectURL(url);
  7766. a.remove();
  7767. this.removeTranslatingStatus();
  7768. this.showNotification("Dịch file thành công", "success");
  7769. } catch (error) {
  7770. console.error("Lỗi dịch file:", error);
  7771. this.showNotification(error.message, "error");
  7772. } finally {
  7773. this.removeTranslatingStatus();
  7774. }
  7775. };
  7776. input.click();
  7777. }
  7778. },
  7779. {
  7780. icon: "⚙️",
  7781. text: "Cài đặt King AI",
  7782. handler: () => {
  7783. dropdown.style.display = "none";
  7784. const settingsUI = this.translator.userSettings.createSettingsUI();
  7785. this.shadowRoot.appendChild(settingsUI);
  7786. }
  7787. });
  7788. menuItems.forEach((item) => {
  7789. const menuItem = document.createElement("div");
  7790. menuItem.className = "translator-tools-item";
  7791. if (item["data-type"]) {
  7792. menuItem.setAttribute("data-type", item["data-type"]);
  7793. }
  7794. menuItem.innerHTML = `
  7795. <span class="item-icon">${item.icon}</span>
  7796. <span class="item-text">${item.text}</span>
  7797. `;
  7798. menuItem.handler = item.handler;
  7799. menuItem.addEventListener("click", item.handler);
  7800. dropdown.appendChild(menuItem);
  7801. });
  7802. this.handleButtonClick = (e) => {
  7803. e.stopPropagation();
  7804. dropdown.style.display =
  7805. dropdown.style.display === "none" ? "block" : "none";
  7806. };
  7807. mainButton.addEventListener("click", this.handleButtonClick);
  7808. this.handleClickOutside = () => {
  7809. dropdown.style.display = "none";
  7810. };
  7811. document.addEventListener("click", this.handleClickOutside);
  7812. container.appendChild(closeButton);
  7813. container.appendChild(mainButton);
  7814. container.appendChild(dropdown);
  7815. container.appendChild(ocrInput);
  7816. container.appendChild(mediaInput);
  7817. this.shadowRoot.appendChild(container);
  7818. if (!this.shadowRoot.contains(container)) {
  7819. this.shadowRoot.appendChild(container);
  7820. }
  7821. container.style.zIndex = "2147483647";
  7822. }
  7823. showProcessingStatus(message) {
  7824. this.removeProcessingStatus();
  7825. const status = document.createElement("div");
  7826. status.className = "processing-status";
  7827. status.innerHTML = `
  7828. <div class="processing-spinner" style="color: white"></div>
  7829. <div class="processing-message" style="color: white">${message}</div>
  7830. <div class="processing-progress" style="color: white">0%</div>
  7831. `;
  7832. Object.assign(status.style, {
  7833. position: "fixed",
  7834. top: `${window.innerHeight / 2}px`,
  7835. left: `${window.innerWidth / 2}px`,
  7836. transform: "translate(-50%, -50%)",
  7837. backgroundColor: "rgba(0, 0, 0, 0.8)",
  7838. color: "white",
  7839. padding: "20px",
  7840. borderRadius: "8px",
  7841. zIndex: "2147483647",
  7842. textAlign: "center",
  7843. minWidth: "200px",
  7844. });
  7845. this.shadowRoot.appendChild(status);
  7846. this.processingStatus = status;
  7847. }
  7848. updateProcessingStatus(message, progress) {
  7849. if (this.processingStatus) {
  7850. const messageEl = this.processingStatus.querySelector(
  7851. ".processing-message"
  7852. );
  7853. const progressEl = this.processingStatus.querySelector(
  7854. ".processing-progress"
  7855. );
  7856. if (messageEl) messageEl.textContent = message;
  7857. if (progressEl) progressEl.textContent = `${progress}%`;
  7858. }
  7859. }
  7860. removeProcessingStatus() {
  7861. if (this.processingStatus) {
  7862. this.processingStatus.remove();
  7863. this.processingStatus = null;
  7864. }
  7865. const status = this.$('.processing-status');
  7866. if (status) status.remove();
  7867. }
  7868. readFileContent(file) {
  7869. return new Promise((resolve, reject) => {
  7870. const reader = new FileReader();
  7871. reader.onload = (e) => resolve(e.target.result);
  7872. reader.onerror = () => reject(new Error("Không thể đọc file"));
  7873. reader.readAsText(file);
  7874. });
  7875. }
  7876. showLoadingStatus(message) {
  7877. const loading = document.createElement("div");
  7878. loading.id = "pdf-loading-status";
  7879. loading.style.cssText = `
  7880. position: fixed;
  7881. top: ${window.innerHeight / 2}px;
  7882. left: ${window.innerWidth / 2}px;
  7883. transform: translate(-50%, -50%);
  7884. background-color: rgba(0, 0, 0, 0.8);
  7885. color: white;
  7886. padding: 20px;
  7887. border-radius: 8px;
  7888. z-index: 2147483647;
  7889. `;
  7890. loading.innerHTML = `
  7891. <div style="text-align: center;">
  7892. <div class="spinner" style="color: white"></div>
  7893. <div style="color: white">${message}</div>
  7894. </div>
  7895. `;
  7896. this.shadowRoot.appendChild(loading);
  7897. }
  7898. removeLoadingStatus() {
  7899. const loading = this.shadowRoot.getElementById("pdf-loading-status");
  7900. if (loading) loading.remove();
  7901. }
  7902. updateProgress(message, percent) {
  7903. const loading = this.shadowRoot.getElementById("pdf-loading-status");
  7904. if (loading) {
  7905. loading.innerHTML = `
  7906. <div style="text-align: center;">
  7907. <div class="spinner" style="color: white"></div>
  7908. <div style="color: white">${message}</div>
  7909. <div style="color: white">${percent}%</div>
  7910. </div>
  7911. `;
  7912. }
  7913. }
  7914. startWebImageOCR() {
  7915. const style = document.createElement("style");
  7916. style.textContent = `
  7917. .translator-overlay {
  7918. position: fixed;
  7919. top: 0;
  7920. left: 0;
  7921. width: 100%;
  7922. height: 100%;
  7923. background-color: rgba(0,0,0,0.3);
  7924. z-index: 2147483647;
  7925. pointer-events: none;
  7926. }
  7927. .translator-overlay.translating-done {
  7928. background-color: transparent;
  7929. }
  7930. .translator-guide {
  7931. position: fixed;
  7932. top: 20px;
  7933. left: ${window.innerWidth / 2}px;
  7934. transform: translateX(-50%);
  7935. background-color: rgba(0,0,0,0.8);
  7936. color: white;
  7937. padding: 10px 20px;
  7938. border-radius: 8px;
  7939. font-size: 14px;
  7940. z-index: 2147483647;
  7941. pointer-events: none;
  7942. }
  7943. .translator-cancel {
  7944. position: fixed;
  7945. top: 20px;
  7946. right: 20px;
  7947. background-color: #ff4444;
  7948. color: white;
  7949. border: none;
  7950. border-radius: 50%;
  7951. width: 30px;
  7952. height: 30px;
  7953. font-size: 16px;
  7954. cursor: pointer;
  7955. display: flex;
  7956. align-items: center;
  7957. justify-content: center;
  7958. z-index: 2147483647;
  7959. pointer-events: auto;
  7960. }
  7961. `;
  7962. this.shadowRoot.appendChild(style);
  7963. const globalStyle = document.createElement('style');
  7964. globalStyle.textContent = `
  7965. img:hover, canvas:hover {
  7966. outline: 3px solid #4a90e2;
  7967. outline-offset: -3px;
  7968. cursor: pointer;
  7969. position: relative;
  7970. z-index: 2147483647;
  7971. pointer-events: auto;
  7972. }
  7973. `;
  7974. document.head.appendChild(globalStyle);
  7975. const overlay = document.createElement("div");
  7976. overlay.className = "translator-overlay";
  7977. const guide = document.createElement("div");
  7978. guide.className = "translator-guide";
  7979. guide.textContent = "Click vào ảnh để OCR";
  7980. const cancelBtn = document.createElement("button");
  7981. cancelBtn.className = "translator-cancel";
  7982. cancelBtn.textContent = "✕";
  7983. this.shadowRoot.appendChild(overlay);
  7984. this.shadowRoot.appendChild(guide);
  7985. this.shadowRoot.appendChild(cancelBtn);
  7986. const handleClick = async (e) => {
  7987. if (e.target.tagName === "IMG" || e.target.tagName === "CANVAS") {
  7988. e.preventDefault();
  7989. e.stopPropagation();
  7990. try {
  7991. this.showTranslatingStatus();
  7992. const targetElement = e.target;
  7993. const canvas = document.createElement("canvas");
  7994. const ctx = canvas.getContext("2d", { willReadFrequently: true });
  7995. if (targetElement.tagName === "IMG") {
  7996. const imageUrl = new URL(targetElement.src);
  7997. const referer = window.location.href;
  7998. const loadImage = async (url) => {
  7999. return new Promise((resolve, reject) => {
  8000. GM_xmlhttpRequest({
  8001. method: "GET",
  8002. url: url,
  8003. headers: {
  8004. "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
  8005. "Accept-Encoding": "gzip, deflate, br",
  8006. "Accept-Language": "en-US,en;q=0.9",
  8007. "Cache-Control": "no-cache",
  8008. "Pragma": "no-cache",
  8009. "Referer": referer,
  8010. "Origin": imageUrl.origin,
  8011. "Sec-Fetch-Dest": "image",
  8012. "Sec-Fetch-Mode": "no-cors",
  8013. "Sec-Fetch-Site": "cross-site",
  8014. "User-Agent": navigator.userAgent
  8015. },
  8016. responseType: "blob",
  8017. anonymous: true,
  8018. onload: function(response) {
  8019. if (response.status === 200) {
  8020. const blob = response.response;
  8021. const img = new Image();
  8022. img.onload = () => {
  8023. canvas.width = img.naturalWidth;
  8024. canvas.height = img.naturalHeight;
  8025. ctx.drawImage(img, 0, 0);
  8026. resolve();
  8027. };
  8028. img.onerror = () => reject(new Error("Không thể load ảnh"));
  8029. img.src = URL.createObjectURL(blob);
  8030. } else {
  8031. const img = new Image();
  8032. img.crossOrigin = "anonymous";
  8033. img.onload = () => {
  8034. canvas.width = img.naturalWidth;
  8035. canvas.height = img.naturalHeight;
  8036. ctx.drawImage(img, 0, 0);
  8037. resolve();
  8038. };
  8039. img.onerror = () => reject(new Error("Không thể load ảnh"));
  8040. img.src = url;
  8041. }
  8042. },
  8043. onerror: function() {
  8044. const img = new Image();
  8045. img.crossOrigin = "anonymous";
  8046. img.onload = () => {
  8047. canvas.width = img.naturalWidth;
  8048. canvas.height = img.naturalHeight;
  8049. ctx.drawImage(img, 0, 0);
  8050. resolve();
  8051. };
  8052. img.onerror = () => reject(new Error("Không thể load ảnh"));
  8053. img.src = url;
  8054. }
  8055. });
  8056. });
  8057. };
  8058. await loadImage(targetElement.src);
  8059. } else if (targetElement.tagName === "CANVAS") {
  8060. try {
  8061. canvas.width = targetElement.width;
  8062. canvas.height = targetElement.height;
  8063. const sourceCtx = targetElement.getContext("2d", { willReadFrequently: true });
  8064. try {
  8065. const imageData = sourceCtx.getImageData(0, 0, targetElement.width, targetElement.height);
  8066. ctx.putImageData(imageData, 0, 0);
  8067. } catch (error) {
  8068. if (error.name === "SecurityError") {
  8069. throw new Error("Canvas chứa nội dung từ domain khác không thể được truy cập");
  8070. }
  8071. throw error;
  8072. }
  8073. } catch (error) {
  8074. throw new Error(`Li x lý canvas: ${error.message}`);
  8075. }
  8076. }
  8077. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  8078. const hasContent = imageData.data.some(pixel => pixel !== 0);
  8079. if (!hasContent) {
  8080. throw new Error('Không thể capture nội dung từ element');
  8081. }
  8082. const blob = await new Promise((resolve, reject) => {
  8083. canvas.toBlob(blob => {
  8084. if (!blob || blob.size < 100) {
  8085. reject(new Error("Không thể tạo ảnh hợp lệ"));
  8086. return;
  8087. }
  8088. resolve(blob);
  8089. }, 'image/png', 1.0);
  8090. });
  8091. const file = new File([blob], "web-image.png", { type: "image/png" });
  8092. const result = await this.ocr.processImage(file);
  8093. if (!result) {
  8094. throw new Error("Không thể xử lý ảnh");
  8095. }
  8096. const translations = result.split("\n");
  8097. let fullTranslation = "";
  8098. let pinyin = "";
  8099. let text = "";
  8100. for (const trans of translations) {
  8101. const parts = trans.split("<|>");
  8102. text += (parts[0]?.trim() || "") + "\n";
  8103. pinyin += (parts[1]?.trim() || "") + "\n";
  8104. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  8105. }
  8106. overlay.classList.add("translating-done");
  8107. this.displayPopup(
  8108. fullTranslation.trim(),
  8109. text.trim(),
  8110. "OCR Web Image",
  8111. pinyin.trim()
  8112. );
  8113. } catch (error) {
  8114. console.error("OCR error:", error);
  8115. this.showNotification(error.message, "error");
  8116. } finally {
  8117. this.removeTranslatingStatus();
  8118. }
  8119. }
  8120. };
  8121. document.addEventListener("click", handleClick, true);
  8122. cancelBtn.addEventListener("click", () => {
  8123. document.removeEventListener("click", handleClick, true);
  8124. overlay.remove();
  8125. guide.remove();
  8126. cancelBtn.remove();
  8127. style.remove();
  8128. globalStyle.remove();
  8129. });
  8130. this.webImageListeners = {
  8131. click: handleClick,
  8132. overlay,
  8133. guide,
  8134. cancelBtn,
  8135. style,
  8136. globalStyle
  8137. };
  8138. }
  8139. startMangaTranslation() {
  8140. const style = document.createElement("style");
  8141. style.textContent = `
  8142. .translator-overlay {
  8143. position: fixed;
  8144. top: 0;
  8145. left: 0;
  8146. width: 100%;
  8147. height: 100%;
  8148. background-color: rgba(0,0,0,0.3);
  8149. z-index: 2147483647;
  8150. pointer-events: none;
  8151. }
  8152. .translator-overlay.translating-done {
  8153. background-color: transparent;
  8154. }
  8155. .translator-guide {
  8156. position: fixed;
  8157. top: 20px;
  8158. left: ${window.innerWidth / 2}px;
  8159. transform: translateX(-50%);
  8160. background-color: rgba(0,0,0,0.8);
  8161. color: white;
  8162. padding: 10px 20px;
  8163. border-radius: 8px;
  8164. font-size: 14px;
  8165. z-index: 2147483647;
  8166. pointer-events: none;
  8167. }
  8168. .translator-cancel {
  8169. position: fixed;
  8170. top: 20px;
  8171. right: 20px;
  8172. background-color: #ff4444;
  8173. color: white;
  8174. border: none;
  8175. border-radius: 50%;
  8176. width: 30px;
  8177. height: 30px;
  8178. font-size: 16px;
  8179. cursor: pointer;
  8180. display: flex;
  8181. align-items: center;
  8182. justify-content: center;
  8183. z-index: 2147483647;
  8184. pointer-events: auto;
  8185. }
  8186. `;
  8187. this.shadowRoot.appendChild(style);
  8188. const globalStyle = document.createElement('style');
  8189. globalStyle.textContent = `
  8190. img:hover, canvas:hover {
  8191. outline: 3px solid #4a90e2 !important;
  8192. outline-offset: -3px !important;
  8193. cursor: pointer !important;
  8194. position: relative !important;
  8195. z-index: 2147483647 !important;
  8196. pointer-events: auto !important;
  8197. }
  8198. `;
  8199. document.head.appendChild(globalStyle);
  8200. const overlay = document.createElement("div");
  8201. overlay.className = "translator-overlay";
  8202. const guide = document.createElement("div");
  8203. guide.className = "translator-guide";
  8204. guide.textContent = "Click vào ảnh để dịch manga";
  8205. const cancelBtn = document.createElement("button");
  8206. cancelBtn.className = "translator-cancel";
  8207. cancelBtn.textContent = "✕";
  8208. const overlayContainer = document.createElement("div");
  8209. overlayContainer.style.cssText = `
  8210. position: fixed;
  8211. top: 0;
  8212. left: 0;
  8213. width: 100%;
  8214. height: 100%;
  8215. pointer-events: none;
  8216. z-index: 2147483647;
  8217. `;
  8218. this.shadowRoot.appendChild(overlay);
  8219. this.shadowRoot.appendChild(guide);
  8220. this.shadowRoot.appendChild(cancelBtn);
  8221. this.shadowRoot.appendChild(overlayContainer);
  8222. let existingOverlays = [];
  8223. const handleClick = async (e) => {
  8224. if (e.target.tagName === "IMG" || e.target.tagName === "CANVAS") {
  8225. e.preventDefault();
  8226. e.stopPropagation();
  8227. try {
  8228. this.showTranslatingStatus();
  8229. const targetElement = e.target;
  8230. const canvas = document.createElement("canvas");
  8231. const ctx = canvas.getContext("2d", { willReadFrequently: true });
  8232. if (targetElement.tagName === "IMG") {
  8233. const imageUrl = new URL(targetElement.src);
  8234. const referer = window.location.href;
  8235. const loadImage = async (url) => {
  8236. return new Promise((resolve, reject) => {
  8237. GM_xmlhttpRequest({
  8238. method: "GET",
  8239. url: url,
  8240. headers: {
  8241. "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
  8242. "Accept-Encoding": "gzip, deflate, br",
  8243. "Accept-Language": "en-US,en;q=0.9",
  8244. "Cache-Control": "no-cache",
  8245. "Pragma": "no-cache",
  8246. "Referer": referer,
  8247. "Origin": imageUrl.origin,
  8248. "Sec-Fetch-Dest": "image",
  8249. "Sec-Fetch-Mode": "no-cors",
  8250. "Sec-Fetch-Site": "cross-site",
  8251. "User-Agent": navigator.userAgent
  8252. },
  8253. responseType: "blob",
  8254. anonymous: true,
  8255. onload: function(response) {
  8256. if (response.status === 200) {
  8257. const blob = response.response;
  8258. const img = new Image();
  8259. img.onload = () => {
  8260. canvas.width = img.naturalWidth;
  8261. canvas.height = img.naturalHeight;
  8262. ctx.drawImage(img, 0, 0);
  8263. resolve();
  8264. };
  8265. img.onerror = () => reject(new Error("Không thể load ảnh"));
  8266. img.src = URL.createObjectURL(blob);
  8267. } else {
  8268. const img = new Image();
  8269. img.crossOrigin = "anonymous";
  8270. img.onload = () => {
  8271. canvas.width = img.naturalWidth;
  8272. canvas.height = img.naturalHeight;
  8273. ctx.drawImage(img, 0, 0);
  8274. resolve();
  8275. };
  8276. img.onerror = () => reject(new Error("Không thể load ảnh"));
  8277. img.src = url;
  8278. }
  8279. },
  8280. onerror: function() {
  8281. const img = new Image();
  8282. img.crossOrigin = "anonymous";
  8283. img.onload = () => {
  8284. canvas.width = img.naturalWidth;
  8285. canvas.height = img.naturalHeight;
  8286. ctx.drawImage(img, 0, 0);
  8287. resolve();
  8288. };
  8289. img.onerror = () => reject(new Error("Không thể load ảnh"));
  8290. img.src = url;
  8291. }
  8292. });
  8293. });
  8294. };
  8295. await loadImage(targetElement.src);
  8296. } else if (targetElement.tagName === "CANVAS") {
  8297. try {
  8298. canvas.width = targetElement.width;
  8299. canvas.height = targetElement.height;
  8300. const sourceCtx = targetElement.getContext("2d", { willReadFrequently: true });
  8301. try {
  8302. const imageData = sourceCtx.getImageData(0, 0, targetElement.width, targetElement.height);
  8303. ctx.putImageData(imageData, 0, 0);
  8304. } catch (error) {
  8305. if (error.name === "SecurityError") {
  8306. throw new Error("Canvas chứa nội dung từ domain khác không thể được truy cập");
  8307. }
  8308. throw error;
  8309. }
  8310. } catch (error) {
  8311. throw new Error(`Li x lý canvas: ${error.message}`);
  8312. }
  8313. }
  8314. const blob = await new Promise((resolve, reject) => {
  8315. canvas.toBlob((b) => {
  8316. if (b) resolve(b);
  8317. else reject(new Error("Không thể tạo blob"));
  8318. }, "image/png");
  8319. });
  8320. const file = new File([blob], "manga.png", { type: "image/png" });
  8321. const result = await this.detectTextPositions(file);
  8322. overlayContainer.innerHTML = "";
  8323. existingOverlays = [];
  8324. if (result?.regions) {
  8325. overlayContainer.innerHTML = "";
  8326. existingOverlays = [];
  8327. overlay.classList.add("translating-done");
  8328. const sortedRegions = result.regions.sort((a, b) => {
  8329. if (Math.abs(a.position.y - b.position.y) < 20) {
  8330. return b.position.x - a.position.x;
  8331. }
  8332. return a.position.y - b.position.y;
  8333. });
  8334. sortedRegions.forEach((region) => {
  8335. const overlay = document.createElement("div");
  8336. overlay.className = "manga-translation-overlay";
  8337. const calculatePosition = () => {
  8338. const imageRect = targetElement.getBoundingClientRect();
  8339. const x = (imageRect.width * region.position.x) / 100 + imageRect.left;
  8340. const y = (imageRect.height * region.position.y) / 100 + imageRect.top;
  8341. const width = (imageRect.width * region.position.width) / 100;
  8342. const height = (imageRect.height * region.position.height) / 100;
  8343. return { x, y, width, height };
  8344. };
  8345. const pos = calculatePosition();
  8346. const padding = 2;
  8347. const themeMode = this.translator.userSettings.settings.theme;
  8348. const theme = CONFIG.THEME[themeMode];
  8349. Object.assign(overlay.style, {
  8350. position: "fixed",
  8351. left: `${pos.x}px`,
  8352. top: `${pos.y}px`,
  8353. minWidth: `${pos.width - padding * 2}px`,
  8354. width: "auto",
  8355. maxWidth: `${pos.width * 1.4 - padding * 2}px`,
  8356. height: "auto",
  8357. backgroundColor: `${theme.background}`,
  8358. color: `${theme.text}`,
  8359. padding: `${padding * 2}px ${padding * 4}px`,
  8360. borderRadius: "8px",
  8361. display: "flex",
  8362. alignItems: "center",
  8363. justifyContent: "center",
  8364. textAlign: "center",
  8365. wordBreak: "keep-all",
  8366. wordWrap: "break-word",
  8367. lineHeight: "1.2",
  8368. pointerEvents: "none",
  8369. zIndex: "2147483647",
  8370. fontSize: this.translator.userSettings.settings.displayOptions.webImageTranslation.fontSize || "9px",
  8371. fontWeight: "600",
  8372. margin: "0",
  8373. flexWrap: "wrap",
  8374. whiteSpace: "pre-wrap",
  8375. overflow: "visible",
  8376. boxSizing: "border-box",
  8377. transform: "none",
  8378. transformOrigin: "center center",
  8379. });
  8380. overlay.textContent = region.translation;
  8381. overlayContainer.appendChild(overlay);
  8382. const updatePosition = debounce(() => {
  8383. const newPos = calculatePosition();
  8384. overlay.style.left = `${newPos.x}px`;
  8385. overlay.style.top = `${newPos.y}px`;
  8386. overlay.style.minWidth = `${newPos.width - padding * 2}px`;
  8387. overlay.style.maxWidth = `${newPos.width * 1.4 - padding * 2}px`;
  8388. }, 16);
  8389. window.addEventListener("scroll", updatePosition, { passive: true });
  8390. window.addEventListener("resize", updatePosition, { passive: true });
  8391. existingOverlays.push({ overlay, updatePosition });
  8392. });
  8393. }
  8394. } catch (error) {
  8395. console.error("Translation error:", error);
  8396. this.showNotification(error.message, "error");
  8397. } finally {
  8398. this.removeTranslatingStatus();
  8399. }
  8400. }
  8401. };
  8402. document.addEventListener("click", handleClick, true);
  8403. cancelBtn.addEventListener("click", () => {
  8404. document.removeEventListener("click", handleClick, true);
  8405. existingOverlays.forEach(({ overlay, updatePosition }) => {
  8406. window.removeEventListener("scroll", updatePosition);
  8407. window.removeEventListener("resize", updatePosition);
  8408. overlay.remove();
  8409. });
  8410. existingOverlays = [];
  8411. overlay.remove();
  8412. guide.remove();
  8413. cancelBtn.remove();
  8414. style.remove();
  8415. globalStyle.remove();
  8416. overlayContainer.remove();
  8417. });
  8418. this.mangaListeners = {
  8419. click: handleClick,
  8420. overlay,
  8421. guide,
  8422. cancelBtn,
  8423. style,
  8424. globalStyle,
  8425. overlayContainer,
  8426. existingOverlays
  8427. };
  8428. }
  8429. async detectTextPositions(file) {
  8430. try {
  8431. const base64Image = await this.ocr.fileToBase64(file);
  8432. const settings = this.translator.userSettings.settings;
  8433. const selectedModel = this.translator.api.getGeminiModel();
  8434. const targetLanguage = settings.displayOptions.targetLanguage;
  8435. const requestBody = {
  8436. contents: [
  8437. {
  8438. parts: [
  8439. {
  8440. text: `Analyze this image and extract all text regions. For each text region:
  8441. 1. Extract the original text
  8442. 2. Translate to the language with language code '${targetLanguage}' with the following requirements:
  8443. You are a professional story translator, specializing in creating accurate and natural translations. You need to translate a story passage into the language with language code '${targetLanguage}'. Ensure that your translation maintains the original meaning of the source text and matches the style of the target language. When translating, pay attention to cultural context and story background so readers can understand the content accurately. Important rules you need to follow include:
  8444. - Ensure the meaning of sentences remains unchanged during translation.
  8445. - Use slang or common phrases when necessary to make the translation more relatable to readers.
  8446. - Check spelling and grammar in the translation.
  8447. - Add language particles of language code '${targetLanguage}' when needed to make sentences more complete.
  8448. - Use natural and contextually appropriate pronouns in language code '${targetLanguage}'.
  8449. - Reference the following websites for appropriate writing style for sex-related story passages: [https://truyensex.moe/, https://truyensexvn.org/, https://truyensex.vip/, https://truyensextv69.com/].
  8450. Notes:
  8451. - For proper names and locations, translate according to Sino-Vietnamese meanings, for example: Dip Trn, Lc Thiếu Du, Long kiếm, Long Sĩ Đầu, Thiên kiếp, ngõ Nê Bình, ... keeping the Sino-Vietnamese meaning is better than translating fully to language code '${targetLanguage}'.
  8452. - Only return the translation in language code '${targetLanguage}', no additional explanation.
  8453. 3. Determine PRECISE position and size:
  8454. - x, y: exact percentage position relative to image (0-100)
  8455. - width, height: exact percentage size relative to image (0-100)
  8456. - text_length: character count of original text
  8457. - text_lines: number of text lines
  8458. - bubble_type: speech/thought/narration/sfx
  8459. Return ONLY a JSON object like:
  8460. {
  8461. "regions": [{
  8462. "text": "original text",
  8463. "translation": "translated text",
  8464. "position": {
  8465. "x": 20.5,
  8466. "y": 30.2,
  8467. "width": 15.3,
  8468. "height": 10.1,
  8469. "text_length": 25,
  8470. "text_lines": 2,
  8471. "bubble_type": "speech"
  8472. }
  8473. }]
  8474. }`,
  8475. },
  8476. {
  8477. inline_data: {
  8478. mime_type: file.type,
  8479. data: base64Image,
  8480. },
  8481. },
  8482. ],
  8483. },
  8484. ],
  8485. generationConfig: {
  8486. temperature: settings.ocrOptions.temperature,
  8487. topP: settings.ocrOptions.topP,
  8488. topK: settings.ocrOptions.topK,
  8489. },
  8490. };
  8491. const responses =
  8492. await this.translator.api.keyManager.executeWithMultipleKeys(
  8493. async (key) => {
  8494. const response = await new Promise((resolve, reject) => {
  8495. GM_xmlhttpRequest({
  8496. method: "POST",
  8497. url: `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${key}`,
  8498. headers: { "Content-Type": "application/json" },
  8499. data: JSON.stringify(requestBody),
  8500. onload: (response) => {
  8501. if (response.status === 200) {
  8502. try {
  8503. const result = JSON.parse(response.responseText);
  8504. const text =
  8505. result?.candidates?.[0]?.content?.parts?.[0]?.text;
  8506. if (text) {
  8507. const jsonMatch = text.match(/\{[\s\S]*\}/);
  8508. if (jsonMatch) {
  8509. const parsedJson = JSON.parse(jsonMatch[0]);
  8510. resolve(parsedJson);
  8511. } else {
  8512. reject(new Error("No JSON found in response"));
  8513. }
  8514. } else {
  8515. reject(new Error("Invalid response format"));
  8516. }
  8517. } catch (error) {
  8518. console.error("Parse error:", error);
  8519. reject(error);
  8520. }
  8521. } else {
  8522. reject(new Error(`API Error: ${response.status}`));
  8523. }
  8524. },
  8525. onerror: (error) => reject(error),
  8526. });
  8527. });
  8528. return response;
  8529. },
  8530. settings.apiProvider
  8531. );
  8532. const response = responses.find((r) => r && r.regions);
  8533. if (!response) {
  8534. throw new Error("No valid response found");
  8535. }
  8536. return response;
  8537. } catch (error) {
  8538. console.error("Text detection error:", error);
  8539. throw error;
  8540. }
  8541. }
  8542. getBrowserContextMenuSize() {
  8543. const browser = navigator.userAgent;
  8544. const sizes = {
  8545. firefox: {
  8546. width: 275,
  8547. height: 340,
  8548. itemHeight: 34,
  8549. },
  8550. chrome: {
  8551. width: 250,
  8552. height: 320,
  8553. itemHeight: 32,
  8554. },
  8555. safari: {
  8556. width: 240,
  8557. height: 300,
  8558. itemHeight: 30,
  8559. },
  8560. edge: {
  8561. width: 260,
  8562. height: 330,
  8563. itemHeight: 33,
  8564. },
  8565. };
  8566. let size;
  8567. if (browser.includes("Firefox")) {
  8568. size = sizes.firefox;
  8569. } else if (browser.includes("Safari") && !browser.includes("Chrome")) {
  8570. size = sizes.safari;
  8571. } else if (browser.includes("Edge")) {
  8572. size = sizes.edge;
  8573. } else {
  8574. size = sizes.chrome;
  8575. }
  8576. const dpi = window.devicePixelRatio || 1;
  8577. return {
  8578. width: Math.round(size.width * dpi),
  8579. height: Math.round(size.height * dpi),
  8580. itemHeight: Math.round(size.itemHeight * dpi),
  8581. };
  8582. }
  8583. setupContextMenu() {
  8584. if (!this.translator.userSettings.settings.contextMenu?.enabled) return;
  8585. let isSpeaking = false;
  8586. document.addEventListener("contextmenu", (e) => {
  8587. const selection = window.getSelection();
  8588. const selectedText = selection.toString().trim();
  8589. if (selectedText) {
  8590. const oldMenus = this.$$(".translator-context-menu");
  8591. oldMenus.forEach((menu) => menu.remove());
  8592. const contextMenu = document.createElement("div");
  8593. contextMenu.className = "translator-context-menu";
  8594. const menuItems = [
  8595. { text: "Dịch nhanh", action: "quick" },
  8596. { text: "Dịch popup", action: "popup" },
  8597. { text: "Dịch nâng cao", action: "advanced" },
  8598. {
  8599. text: "Đọc văn bản",
  8600. action: "tts",
  8601. getLabel: () => isSpeaking ? "Dừng đọc" : "Đọc văn bản"
  8602. }
  8603. ];
  8604. const range = selection.getRangeAt(0).cloneRange();
  8605. menuItems.forEach((item) => {
  8606. const menuItem = document.createElement("div");
  8607. menuItem.className = "translator-context-menu-item";
  8608. menuItem.textContent = item.getLabel ? item.getLabel() : item.text;
  8609. menuItem.onclick = (e) => {
  8610. e.preventDefault();
  8611. e.stopPropagation();
  8612. const newSelection = window.getSelection();
  8613. newSelection.removeAllRanges();
  8614. newSelection.addRange(range);
  8615. if (item.action === "tts") {
  8616. if (isSpeaking) {
  8617. speechSynthesis.cancel();
  8618. isSpeaking = false;
  8619. } else {
  8620. speechSynthesis.cancel();
  8621. const utterance = new SpeechSynthesisUtterance(selectedText);
  8622. utterance.lang = this.translator.userSettings.settings.displayOptions.sourceLanguage;
  8623. utterance.onend = () => {
  8624. isSpeaking = false;
  8625. menuItem.textContent = "Đọc văn bản";
  8626. };
  8627. utterance.oncancel = () => {
  8628. isSpeaking = false;
  8629. menuItem.textContent = "Đọc văn bản";
  8630. };
  8631. speechSynthesis.speak(utterance);
  8632. isSpeaking = true;
  8633. }
  8634. menuItem.textContent = isSpeaking ? "Dừng đọc" : "Đọc văn bản";
  8635. } else {
  8636. this.handleTranslateButtonClick(newSelection, item.action);
  8637. contextMenu.remove();
  8638. }
  8639. };
  8640. contextMenu.appendChild(menuItem);
  8641. });
  8642. const viewportWidth = window.innerWidth;
  8643. const viewportHeight = window.innerHeight;
  8644. const menuWidth = 150;
  8645. const menuHeight = (menuItems.length * 40);
  8646. const browserMenu = this.getBrowserContextMenuSize();
  8647. const browserMenuWidth = browserMenu.width;
  8648. const browserMenuHeight = browserMenu.height;
  8649. const spaceWidth = browserMenuWidth + menuWidth;
  8650. const remainingWidth = viewportWidth - e.clientX;
  8651. const rightEdge = viewportWidth - menuWidth;
  8652. const bottomEdge = viewportHeight - menuHeight;
  8653. const browserMenuWidthEdge = viewportWidth - browserMenuWidth;
  8654. const browserMenuHeightEdge = viewportHeight - browserMenuHeight;
  8655. let left, top;
  8656. if (e.clientX < menuWidth && e.clientY < menuHeight) {
  8657. left = e.clientX + browserMenuWidth + 10;
  8658. top = e.clientY;
  8659. } else if (
  8660. e.clientX > browserMenuWidthEdge &&
  8661. e.clientY < browserMenuHeight
  8662. ) {
  8663. left = e.clientX - spaceWidth + remainingWidth;
  8664. top = e.clientY;
  8665. } else if (
  8666. e.clientX > browserMenuWidthEdge &&
  8667. e.clientY > viewportHeight - browserMenuHeight
  8668. ) {
  8669. left = e.clientX - spaceWidth + remainingWidth;
  8670. top = e.clientY - menuHeight;
  8671. } else if (
  8672. e.clientX < menuWidth &&
  8673. e.clientY > viewportHeight - browserMenuHeight
  8674. ) {
  8675. left = e.clientX + browserMenuWidth + 10;
  8676. top = e.clientY - menuHeight;
  8677. } else if (e.clientY < menuHeight) {
  8678. left = e.clientX - menuWidth;
  8679. top = e.clientY;
  8680. } else if (e.clientX > browserMenuWidthEdge) {
  8681. left = e.clientX - spaceWidth + remainingWidth;
  8682. top = e.clientY;
  8683. } else if (e.clientY > browserMenuHeightEdge - menuHeight / 2) {
  8684. left = e.clientX - menuWidth;
  8685. top = e.clientY - menuHeight;
  8686. } else {
  8687. left = e.clientX;
  8688. top = e.clientY - menuHeight;
  8689. }
  8690. left = Math.max(5, Math.min(left, rightEdge - 5));
  8691. top = Math.max(5, Math.min(top, bottomEdge - 5));
  8692. contextMenu.style.left = `${left}px`;
  8693. contextMenu.style.top = `${top}px`;
  8694. this.shadowRoot.appendChild(contextMenu);
  8695. const closeMenu = (e) => {
  8696. if (!contextMenu.contains(e.target)) {
  8697. speechSynthesis.cancel();
  8698. contextMenu.remove();
  8699. document.removeEventListener("click", closeMenu);
  8700. }
  8701. };
  8702. document.addEventListener("click", closeMenu);
  8703. const handleScroll = debounce(() => {
  8704. speechSynthesis.cancel();
  8705. contextMenu.remove();
  8706. window.removeEventListener("scroll", handleScroll);
  8707. }, 150);
  8708. window.addEventListener("scroll", handleScroll, { passive: true });
  8709. }
  8710. });
  8711. }
  8712. removeWebImageListeners() {
  8713. if (this.webImageListeners) {
  8714. document.removeEventListener(
  8715. "mouseover",
  8716. this.webImageListeners.hover,
  8717. true
  8718. );
  8719. document.removeEventListener(
  8720. "mouseout",
  8721. this.webImageListeners.leave,
  8722. true
  8723. );
  8724. document.removeEventListener(
  8725. "click",
  8726. this.webImageListeners.click,
  8727. true
  8728. );
  8729. this.webImageListeners.overlay?.remove();
  8730. this.webImageListeners.guide?.remove();
  8731. this.webImageListeners.cancelBtn?.remove();
  8732. this.webImageListeners.style?.remove();
  8733. document
  8734. .querySelectorAll(".translator-image-highlight")
  8735. .forEach((el) => {
  8736. el.classList.remove("translator-image-highlight");
  8737. });
  8738. this.webImageListeners = null;
  8739. }
  8740. }
  8741. handleSettingsShortcut(e) {
  8742. if (!this.translator.userSettings.settings.shortcuts?.settingsEnabled)
  8743. return;
  8744. if ((e.altKey || e.metaKey) && e.key === "s") {
  8745. e.preventDefault();
  8746. const settingsUI = this.translator.userSettings.createSettingsUI();
  8747. this.shadowRoot.appendChild(settingsUI);
  8748. }
  8749. }
  8750. async handleTranslationShortcuts(e) {
  8751. if (!this.translator.userSettings.settings.shortcuts?.enabled) return;
  8752. const shortcuts = this.translator.userSettings.settings.shortcuts;
  8753. if (e.altKey || e.metaKey) {
  8754. let translateType = null;
  8755. if (e.key === shortcuts.pageTranslate.key) {
  8756. e.preventDefault();
  8757. await this.handlePageTranslation();
  8758. return;
  8759. } else if (e.key === shortcuts.inputTranslate.key) {
  8760. e.preventDefault();
  8761. const activeElement = document.activeElement;
  8762. if (this.translator.input.isValidEditor(activeElement)) {
  8763. const text = this.translator.input.getEditorContent(activeElement);
  8764. if (text) {
  8765. await this.translator.input.translateEditor(activeElement, true);
  8766. }
  8767. }
  8768. return;
  8769. }
  8770. const selection = window.getSelection();
  8771. const selectedText = selection?.toString().trim();
  8772. if (!selectedText || this.isTranslating) return;
  8773. const targetElement = selection.anchorNode?.parentElement;
  8774. if (!targetElement) return;
  8775. if (e.key === shortcuts.quickTranslate.key) {
  8776. e.preventDefault();
  8777. translateType = "quick";
  8778. } else if (e.key === shortcuts.popupTranslate.key) {
  8779. e.preventDefault();
  8780. translateType = "popup";
  8781. } else if (e.key === shortcuts.advancedTranslate.key) {
  8782. e.preventDefault();
  8783. translateType = "advanced";
  8784. }
  8785. if (translateType) {
  8786. await this.handleTranslateButtonClick(selection, translateType);
  8787. }
  8788. }
  8789. }
  8790. updateSettingsListener(enabled) {
  8791. if (enabled) {
  8792. document.addEventListener("keydown", this.settingsShortcutListener);
  8793. } else {
  8794. document.removeEventListener("keydown", this.settingsShortcutListener);
  8795. }
  8796. }
  8797. updateSettingsTranslationListeners(enabled) {
  8798. if (enabled) {
  8799. document.addEventListener("keydown", this.translationShortcutListener);
  8800. } else {
  8801. document.removeEventListener(
  8802. "keydown",
  8803. this.translationShortcutListener
  8804. );
  8805. }
  8806. }
  8807. updateSelectionListeners(enabled) {
  8808. if (enabled) this.setupSelectionHandlers();
  8809. }
  8810. updateTapListeners(enabled) {
  8811. if (enabled) this.setupDocumentTapHandler();
  8812. }
  8813. setupEventListeners() {
  8814. const shortcuts = this.translator.userSettings.settings.shortcuts;
  8815. const clickOptions = this.translator.userSettings.settings.clickOptions;
  8816. const touchOptions = this.translator.userSettings.settings.touchOptions;
  8817. if (this.translator.userSettings.settings.contextMenu?.enabled) {
  8818. this.setupContextMenu();
  8819. }
  8820. if (shortcuts?.settingsEnabled) {
  8821. this.updateSettingsListener(true);
  8822. }
  8823. if (shortcuts?.enabled) {
  8824. this.updateSettingsTranslationListeners(true);
  8825. }
  8826. if (clickOptions?.enabled) {
  8827. this.updateSelectionListeners(true);
  8828. this.translationButtonEnabled = true;
  8829. }
  8830. if (touchOptions?.enabled) {
  8831. this.updateTapListeners(true);
  8832. this.translationTapEnabled = true;
  8833. }
  8834. const isEnabled =
  8835. localStorage.getItem("translatorToolsEnabled") === "true";
  8836. if (isEnabled) {
  8837. this.setupTranslatorTools();
  8838. }
  8839. this.shadowRoot.addEventListener("settingsChanged", (e) => {
  8840. this.removeToolsContainer();
  8841. const newSettings = e.detail;
  8842. if (newSettings.theme !== this.translator.userSettings.settings.theme) {
  8843. this.updateAllButtonStyles();
  8844. }
  8845. this.updateSettingsListener(newSettings.shortcuts?.settingsEnabled);
  8846. this.updateSettingsTranslationListeners(newSettings.shortcuts?.enabled);
  8847. if (newSettings.clickOptions?.enabled !== undefined) {
  8848. this.translationButtonEnabled = newSettings.clickOptions.enabled;
  8849. this.updateSelectionListeners(newSettings.clickOptions.enabled);
  8850. if (!newSettings.clickOptions.enabled) {
  8851. this.removeTranslateButton();
  8852. }
  8853. }
  8854. if (newSettings.touchOptions?.enabled !== undefined) {
  8855. this.translationTapEnabled = newSettings.touchOptions.enabled;
  8856. this.updateTapListeners(newSettings.touchOptions.enabled);
  8857. if (!newSettings.touchOptions.enabled) {
  8858. this.removeTranslateButton();
  8859. }
  8860. }
  8861. this.cache = new TranslationCache(
  8862. newSettings.cacheOptions.text.maxSize,
  8863. newSettings.cacheOptions.text.expirationTime
  8864. );
  8865. this.cache.clear();
  8866. if (this.ocr?.imageCache) {
  8867. this.ocr.imageCache.clear();
  8868. }
  8869. const apiConfig = {
  8870. providers: CONFIG.API.providers,
  8871. currentProvider: newSettings.apiProvider,
  8872. apiKey: newSettings.apiKey,
  8873. maxRetries: CONFIG.API.maxRetries,
  8874. retryDelay: CONFIG.API.retryDelay,
  8875. };
  8876. this.api = new APIManager(
  8877. apiConfig,
  8878. () => this.translator.userSettings.settings
  8879. );
  8880. const isEnabled =
  8881. localStorage.getItem("translatorToolsEnabled") === "true";
  8882. if (isEnabled) {
  8883. this.setupTranslatorTools();
  8884. }
  8885. });
  8886. }
  8887. showNotification(message, type = "info") {
  8888. const notification = document.createElement("div");
  8889. notification.className = "translator-notification";
  8890. const colors = {
  8891. info: "#4a90e2",
  8892. success: "#28a745",
  8893. warning: "#ffc107",
  8894. error: "#dc3545",
  8895. };
  8896. const backgroundColor = colors[type] || colors.info;
  8897. const textColor = type === "warning" ? "#000" : "#fff";
  8898. Object.assign(notification.style, {
  8899. position: "fixed",
  8900. top: "20px",
  8901. left: `${window.innerWidth / 2}px`,
  8902. transform: "translateX(-50%)",
  8903. backgroundColor,
  8904. color: textColor,
  8905. padding: "10px 20px",
  8906. borderRadius: "8px",
  8907. zIndex: "2147483647",
  8908. animation: "fadeInOut 2s ease",
  8909. fontFamily: "Arial, sans-serif",
  8910. fontSize: "14px",
  8911. boxShadow: "0 2px 10px rgba(0,0,0,0.2)",
  8912. });
  8913. notification.textContent = message;
  8914. this.shadowRoot.appendChild(notification);
  8915. setTimeout(() => notification.remove(), 3000);
  8916. }
  8917. resetState() {
  8918. if (this.pressTimer) clearTimeout(this.pressTimer);
  8919. if (this.timer) clearTimeout(this.timer);
  8920. this.isLongPress = false;
  8921. this.lastTime = 0;
  8922. this.count = 0;
  8923. this.isDown = false;
  8924. this.isTranslating = false;
  8925. this.ignoreNextSelectionChange = false;
  8926. this.removeTranslateButton();
  8927. this.removeTranslatingStatus();
  8928. }
  8929. removeTranslateButton() {
  8930. if (this.currentTranslateButton) {
  8931. const button = this.$('.translator-button');
  8932. if (button) button.remove();
  8933. this.currentTranslateButton = null;
  8934. }
  8935. }
  8936. removeTranslatingStatus() {
  8937. if (this.translatingStatus) {
  8938. this.translatingStatus.remove();
  8939. this.translatingStatus = null;
  8940. }
  8941. const status = this.$('.center-translate-status');
  8942. if (status) status.remove();
  8943. }
  8944. }
  8945. class Translator {
  8946. constructor() {
  8947. window.translator = this;
  8948. this.userSettings = new UserSettings(this);
  8949. const apiConfig = {
  8950. ...CONFIG.API,
  8951. currentProvider: this.userSettings.getSetting("apiProvider"),
  8952. apiKey: this.userSettings.getSetting("apiKey"),
  8953. };
  8954. this.videoStreaming = new VideoStreamingTranslator(this);
  8955. this.api = new APIManager(apiConfig, () => this.userSettings.settings);
  8956. this.page = new PageTranslator(this);
  8957. this.input = new InputTranslator(this);
  8958. this.ocr = new OCRManager(this);
  8959. this.media = new MediaManager(this);
  8960. this.fileManager = new FileManager(this);
  8961. this.cache = new TranslationCache(
  8962. this.userSettings.settings.cacheOptions.text.maxSize,
  8963. this.userSettings.settings.cacheOptions.text.expirationTime
  8964. );
  8965. this.ui = new UIManager(this);
  8966. this.cache.optimizeStorage();
  8967. this.autoCorrectEnabled = true;
  8968. }
  8969. async translate(
  8970. text,
  8971. targetElement,
  8972. isAdvanced = false,
  8973. popup = false,
  8974. targetLang = ""
  8975. ) {
  8976. try {
  8977. if (!text) return null;
  8978. const settings = this.userSettings.settings.displayOptions;
  8979. const targetLanguage = targetLang || settings.targetLanguage;
  8980. const promptType = isAdvanced ? "advanced" : "normal";
  8981. const prompt = this.createPrompt(text, promptType, targetLanguage);
  8982. let translatedText;
  8983. const cacheEnabled =
  8984. this.userSettings.settings.cacheOptions.text.enabled;
  8985. if (cacheEnabled) {
  8986. translatedText = this.cache.get(text, isAdvanced, targetLanguage);
  8987. }
  8988. if (!translatedText) {
  8989. translatedText = await this.api.request(prompt, 'page');
  8990. if (cacheEnabled && translatedText) {
  8991. this.cache.set(text, translatedText, isAdvanced, targetLanguage);
  8992. }
  8993. }
  8994. if (
  8995. translatedText &&
  8996. targetElement &&
  8997. !targetElement.isPDFTranslation
  8998. ) {
  8999. if (isAdvanced || popup) {
  9000. const translations = translatedText.split("\n");
  9001. let fullTranslation = "";
  9002. let pinyin = "";
  9003. for (const trans of translations) {
  9004. const parts = trans.split("<|>");
  9005. pinyin += (parts[1]?.trim() || "") + "\n";
  9006. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  9007. }
  9008. this.ui.displayPopup(
  9009. fullTranslation.trim(),
  9010. text,
  9011. "King1x32 <3",
  9012. pinyin.trim()
  9013. );
  9014. } else {
  9015. this.ui.showTranslationBelow(translatedText, targetElement, text);
  9016. }
  9017. }
  9018. return translatedText;
  9019. } catch (error) {
  9020. console.error("Lỗi dịch:", error);
  9021. this.ui.showNotification(error.message, "error");
  9022. }
  9023. }
  9024. async translateFile(file) {
  9025. try {
  9026. if (!this.fileManager.isValidFormat(file)) {
  9027. throw new Error('Định dạng file không được hỗ trợ. Chỉ hỗ trợ: txt, srt, vtt, html, md, json');
  9028. }
  9029. if (!this.fileManager.isValidSize(file)) {
  9030. throw new Error('File quá lớn. Tối đa 10MB');
  9031. }
  9032. return await this.fileManager.processFile(file);
  9033. } catch (error) {
  9034. throw new Error(`Li dch file: ${error.message}`);
  9035. }
  9036. }
  9037. async autoCorrect(translation) {
  9038. const targetLanguage =
  9039. this.userSettings.settings.displayOptions.targetLanguage;
  9040. 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.`;
  9041. try {
  9042. const corrected = await this.api.request(prompt, 'page');
  9043. return corrected.trim();
  9044. } catch (error) {
  9045. console.error("Auto-correction failed:", error);
  9046. return translation;
  9047. }
  9048. }
  9049. createPrompt(text, type = "normal", targetLang = "") {
  9050. const settings = this.userSettings.settings;
  9051. const targetLanguage =
  9052. targetLang || settings.displayOptions.targetLanguage;
  9053. const sourceLanguage = settings.displayOptions.sourceLanguage;
  9054. const isPinyinMode =
  9055. settings.displayOptions.translationMode !== "translation_only"
  9056. if (
  9057. settings.promptSettings?.enabled &&
  9058. settings.promptSettings?.useCustom
  9059. ) {
  9060. const prompts = settings.promptSettings.customPrompts;
  9061. const promptKey = isPinyinMode ? `${type}_chinese` : type;
  9062. let promptTemplate = prompts[promptKey];
  9063. if (promptTemplate) {
  9064. return promptTemplate
  9065. .replace(/\{text\}/g, text)
  9066. .replace(/\{targetLang\}/g, targetLanguage)
  9067. .replace(
  9068. /\{sourceLang\}/g,
  9069. sourceLanguage || this.page.languageCode
  9070. );
  9071. }
  9072. }
  9073. return this.createDefaultPrompt(text, type, isPinyinMode, targetLanguage);
  9074. }
  9075. createDefaultPrompt(
  9076. text,
  9077. type = "normal",
  9078. isPinyinMode = false,
  9079. targetLang = ""
  9080. ) {
  9081. const settings = this.userSettings.settings;
  9082. const targetLanguage =
  9083. targetLang || settings.displayOptions.targetLanguage;
  9084. const share_normal = `
  9085. Hãy dch văn bn cn x lý sang ngôn ng có mã ngôn ng là '${targetLanguage}' vi các yêu cu sau:
  9086. - Dch phi tuân th cht ch bi cnh và sc thái ban đầu ca văn bn.
  9087. - Đảm bo s lưu loát và t nhiên như người bn xứ.
  9088. - Không thêm bt k gii thích hay din gii nào ngoài bn dch.
  9089. - Bo toàn các thut ng và danh t riêng vi t l 1:1.
  9090. Nếu bn nhn thy văn bn là truyn thì hãy dch truyn theo yêu cu sau:
  9091. 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:
  9092. - Đảm bo nghĩa ca các câu không b thay đổi khi dch.
  9093. - 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.
  9094. - Kim tra chính t và ng pháp trong bn dch.
  9095. - 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.
  9096. - 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.
  9097. - 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/].
  9098. `;
  9099. const share_ocr = `
  9100. 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:
  9101. - Đảm bo nghĩa ca các câu không b thay đổi khi dch.
  9102. - 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.
  9103. - Kim tra chính t và ng pháp trong bn dch.
  9104. - 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.
  9105. - 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.
  9106. - 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/].
  9107. `;
  9108. const share_media = `
  9109. 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:
  9110. - Đảm bo nghĩa ca các câu không b thay đổi khi dch.
  9111. - 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.
  9112. - Kim tra chính t và ng pháp trong bn dch.
  9113. - 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.
  9114. - 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.
  9115. - 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/].
  9116. `;
  9117. const share_pinyin = `
  9118. Hãy tr v theo format sau, mi phn cách nhau bng du <|> và không gii thích thêm:
  9119. Văn bn gc <|> phiên âm IPA <|> bn dch sang ngôn ng có mã ngôn ng là '${targetLanguage}'
  9120. Ví dụ: Hello <|> heˈloʊ <|> Xin chào
  9121. Lưu ý:
  9122. - Nếu có t là tiếng Trung, hãy tr v giá tr phiên âm ca t đó chính là pinyin + s tone (1-4) ca t đó. Ví dụ: 你好 <|> Nǐ3 hǎo3 <|> Xin chào
  9123. - Bn dch phi hoàn toàn là 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,...
  9124. - Ch tr v bn dch theo format trên, mi 1 cm theo format s 1 dòng, gi nguyên định dng phông ch ban đầu và không gii thích thêm.
  9125. `;
  9126. const basePrompts = {
  9127. normal: `${share_normal}
  9128. Lưu ý:
  9129. - Bn dch phi hoàn toàn là 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,...
  9130. - 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.
  9131. Văn bn cn x lý: "${text}"`,
  9132. advanced: `Dch và phân tích t khóa: "${text}"`,
  9133. ocr: `${share_ocr}
  9134. Lưu ý:
  9135. - Bn dch phi hoàn toàn là 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,..
  9136. - Đọc hiu tht kĩ và x lý toàn b văn bn trong hình nh.
  9137. - Ch tr v bn dch, không gii thích.`,
  9138. media: `${share_media}
  9139. Lưu ý:
  9140. - Bn dch phi hoàn toàn là 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,..
  9141. - Đị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.
  9142. - Ch tr v bn dch, không gii thích.`,
  9143. page: `${share_normal}
  9144. Lưu ý:
  9145. - Bn dch phi hoàn toàn là 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,...
  9146. - 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.
  9147. Văn bn cn x lý: "${text}"`,
  9148. };
  9149. const pinyinPrompts = {
  9150. normal: `${share_normal}
  9151. ${share_pinyin}
  9152. Văn bn cn x lý: "${text}"`,
  9153. advanced: `Dch và phân tích t khóa: "${text}"`,
  9154. ocr: `${share_ocr}
  9155. ${share_pinyin}
  9156. Đọc hiu tht kĩ và x lý toàn b văn bn trong hình nh.`,
  9157. media: `${share_media}
  9158. Lưu ý:
  9159. - Bn dch phi hoàn toàn là 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,..
  9160. - Đị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.
  9161. - Ch tr v bn dch, không gii thích.`,
  9162. page: `${share_normal}
  9163. ${share_pinyin}
  9164. Văn bn cn x lý: "${text}"`,
  9165. };
  9166. return isPinyinMode ? pinyinPrompts[type] : basePrompts[type];
  9167. }
  9168. showSettingsUI() {
  9169. const settingsUI = this.userSettings.createSettingsUI();
  9170. this.ui.shadowRoot.appendChild(settingsUI);
  9171. }
  9172. handleError(error, targetElement) {
  9173. console.error("Translation failed:", error);
  9174. const message = error.message.includes("Rate limit")
  9175. ? "Vui lòng chờ giữa các lần dịch"
  9176. : error.message.includes("Gemini API")
  9177. ? "Lỗi Gemini: " + error.message
  9178. : error.message.includes("API Key")
  9179. ? "Lỗi xác thực API"
  9180. : "Lỗi dịch thuật: " + error.message;
  9181. this.ui.showTranslationBelow(targetElement, message);
  9182. }
  9183. }
  9184. function debounce(func, wait) {
  9185. let timeout;
  9186. return function executedFunction(...args) {
  9187. const later = () => {
  9188. clearTimeout(timeout);
  9189. func(...args);
  9190. };
  9191. clearTimeout(timeout);
  9192. timeout = setTimeout(later, wait);
  9193. };
  9194. }
  9195. function createFileInput(accept, onFileSelected) {
  9196. return new Promise((resolve) => {
  9197. const translator = window.translator;
  9198. const themeMode = translator.userSettings.settings.theme;
  9199. const theme = CONFIG.THEME[themeMode];
  9200. const div = document.createElement('div');
  9201. div.style.cssText = `
  9202. position: fixed;
  9203. top: 0;
  9204. left: 0;
  9205. width: 100vw;
  9206. height: 100vh;
  9207. background: rgba(0,0,0,0.5);
  9208. z-index: 2147483647;
  9209. display: flex;
  9210. justify-content: center;
  9211. align-items: center;
  9212. font-family: Arial, sans-serif;
  9213. `;
  9214. const container = document.createElement('div');
  9215. container.style.cssText = `
  9216. background: ${theme.background};
  9217. padding: 20px;
  9218. border-radius: 12px;
  9219. box-shadow: 0 4px 20px rgba(0,0,0,0.2);
  9220. display: flex;
  9221. flex-direction: column;
  9222. gap: 15px;
  9223. min-width: 300px;
  9224. border: 1px solid ${theme.border};
  9225. `;
  9226. const title = document.createElement('div');
  9227. title.style.cssText = `
  9228. color: ${theme.title};
  9229. font-size: 16px;
  9230. font-weight: bold;
  9231. text-align: center;
  9232. margin-bottom: 5px;
  9233. `;
  9234. title.textContent = 'Chọn file để dịch';
  9235. const inputContainer = document.createElement('div');
  9236. inputContainer.style.cssText = `
  9237. display: flex;
  9238. flex-direction: column;
  9239. gap: 10px;
  9240. align-items: center;
  9241. `;
  9242. const input = document.createElement('input');
  9243. input.type = 'file';
  9244. input.accept = accept;
  9245. input.style.cssText = `
  9246. padding: 8px;
  9247. border-radius: 8px;
  9248. border: 1px solid ${theme.border};
  9249. background: ${themeMode === 'dark' ? '#444' : '#fff'};
  9250. color: ${theme.text};
  9251. width: 100%;
  9252. cursor: pointer;
  9253. font-family: inherit;
  9254. font-size: 14px;
  9255. `;
  9256. const buttonContainer = document.createElement('div');
  9257. buttonContainer.style.cssText = `
  9258. display: flex;
  9259. gap: 10px;
  9260. justify-content: center;
  9261. margin-top: 10px;
  9262. `;
  9263. const cancelButton = document.createElement('button');
  9264. cancelButton.style.cssText = `
  9265. padding: 8px 16px;
  9266. border-radius: 8px;
  9267. border: none;
  9268. background: ${theme.button.close.background};
  9269. color: ${theme.button.close.text};
  9270. cursor: pointer;
  9271. font-size: 14px;
  9272. transition: all 0.2s ease;
  9273. font-family: inherit;
  9274. `;
  9275. cancelButton.textContent = 'Hủy';
  9276. cancelButton.onmouseover = () => {
  9277. cancelButton.style.transform = 'translateY(-2px)';
  9278. cancelButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
  9279. };
  9280. cancelButton.onmouseout = () => {
  9281. cancelButton.style.transform = 'none';
  9282. cancelButton.style.boxShadow = 'none';
  9283. };
  9284. const translateButton = document.createElement('button');
  9285. translateButton.style.cssText = `
  9286. padding: 8px 16px;
  9287. border-radius: 8px;
  9288. border: none;
  9289. background: ${theme.button.translate.background};
  9290. color: ${theme.button.translate.text};
  9291. cursor: pointer;
  9292. font-size: 14px;
  9293. transition: all 0.2s ease;
  9294. opacity: 0.5;
  9295. font-family: inherit;
  9296. `;
  9297. translateButton.textContent = 'Dịch';
  9298. translateButton.disabled = true;
  9299. translateButton.onmouseover = () => {
  9300. if (!translateButton.disabled) {
  9301. translateButton.style.transform = 'translateY(-2px)';
  9302. translateButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
  9303. }
  9304. };
  9305. translateButton.onmouseout = () => {
  9306. translateButton.style.transform = 'none';
  9307. translateButton.style.boxShadow = 'none';
  9308. };
  9309. const cleanup = () => {
  9310. div.remove();
  9311. resolve();
  9312. };
  9313. input.addEventListener('change', (e) => {
  9314. const file = e.target.files?.[0];
  9315. if (file) {
  9316. translateButton.disabled = false;
  9317. translateButton.style.opacity = '1';
  9318. } else {
  9319. translateButton.disabled = true;
  9320. translateButton.style.opacity = '0.5';
  9321. }
  9322. });
  9323. cancelButton.addEventListener('click', cleanup);
  9324. translateButton.addEventListener('click', async () => {
  9325. const file = input.files?.[0];
  9326. if (file) {
  9327. try {
  9328. translateButton.disabled = true;
  9329. translateButton.style.opacity = '0.5';
  9330. translateButton.textContent = 'Đang xử lý...';
  9331. await onFileSelected(file);
  9332. } catch (error) {
  9333. console.error('Error processing file:', error);
  9334. }
  9335. cleanup();
  9336. }
  9337. });
  9338. buttonContainer.appendChild(cancelButton);
  9339. buttonContainer.appendChild(translateButton);
  9340. inputContainer.appendChild(input);
  9341. container.appendChild(title);
  9342. container.appendChild(inputContainer);
  9343. container.appendChild(buttonContainer);
  9344. div.appendChild(container);
  9345. translator.ui.shadowRoot.appendChild(div);
  9346. div.addEventListener('click', (e) => {
  9347. if (e.target === div) cleanup();
  9348. });
  9349. });
  9350. }
  9351. GM_registerMenuCommand("📄 Dịch trang", async () => {
  9352. const translator = window.translator;
  9353. if (translator) {
  9354. try {
  9355. translator.ui.showTranslatingStatus();
  9356. const result = await translator.page.translatePage();
  9357. translator.ui.removeTranslatingStatus();
  9358. if (result.success) {
  9359. translator.ui.showNotification(result.message, "success");
  9360. } else {
  9361. translator.ui.showNotification(result.message, "warning");
  9362. }
  9363. } catch (error) {
  9364. console.error("Page translation error:", error);
  9365. translator.ui.showNotification(error.message, "error");
  9366. } finally {
  9367. translator.ui.removeTranslatingStatus();
  9368. }
  9369. }
  9370. });
  9371. GM_registerMenuCommand("📸 Dịch Vùng OCR", async () => {
  9372. const translator = window.translator;
  9373. if (translator) {
  9374. try {
  9375. translator.ui.showTranslatingStatus();
  9376. const screenshot = await translator.ocr.captureScreen();
  9377. if (!screenshot) {
  9378. throw new Error("Không thể tạo ảnh chụp màn hình");
  9379. }
  9380. const result = await translator.ocr.processImage(screenshot);
  9381. translator.ui.removeTranslatingStatus();
  9382. if (!result) {
  9383. throw new Error("Không thể xử lý ảnh chụp màn hình");
  9384. }
  9385. const translations = result.split("\n");
  9386. let fullTranslation = "";
  9387. let pinyin = "";
  9388. let text = "";
  9389. for (const trans of translations) {
  9390. const parts = trans.split("<|>");
  9391. text += (parts[0]?.trim() || "") + "\n";
  9392. pinyin += (parts[1]?.trim() || "") + "\n";
  9393. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  9394. }
  9395. translator.ui.displayPopup(
  9396. fullTranslation.trim(),
  9397. text.trim(),
  9398. "OCR Vùng Màn Hình",
  9399. pinyin.trim()
  9400. );
  9401. } catch (error) {
  9402. console.error("Screen translation error:", error);
  9403. translator.ui.showNotification(error.message, "error");
  9404. } finally {
  9405. translator.ui.removeTranslatingStatus();
  9406. }
  9407. }
  9408. });
  9409. GM_registerMenuCommand("🖼️ Dịch Ảnh Web", () => {
  9410. const translator = window.translator;
  9411. if (translator) {
  9412. translator.ui.startWebImageOCR();
  9413. }
  9414. });
  9415. GM_registerMenuCommand("📚 Dịch Manga Web", () => {
  9416. const translator = window.translator;
  9417. if (translator) {
  9418. translator.ui.startMangaTranslation();
  9419. }
  9420. });
  9421. GM_registerMenuCommand("📷 Dịch File Ảnh", async () => {
  9422. const translator = window.translator;
  9423. if (!translator) return;
  9424. await createFileInput("image/*", async (file) => {
  9425. try {
  9426. translator.ui.showTranslatingStatus();
  9427. const result = await translator.ocr.processImage(file);
  9428. translator.ui.removeTranslatingStatus();
  9429. const translations = result.split("\n");
  9430. let fullTranslation = "";
  9431. let pinyin = "";
  9432. let text = "";
  9433. for (const trans of translations) {
  9434. const parts = trans.split("<|>");
  9435. text += (parts[0]?.trim() || "") + "\n";
  9436. pinyin += (parts[1]?.trim() || "") + "\n";
  9437. fullTranslation += (parts[2]?.trim() || trans) + "\n";
  9438. }
  9439. translator.ui.displayPopup(
  9440. fullTranslation.trim(),
  9441. text.trim(),
  9442. "OCR Image Local",
  9443. pinyin.trim()
  9444. );
  9445. } catch (error) {
  9446. translator.ui.showNotification(error.message);
  9447. } finally {
  9448. translator.ui.removeTranslatingStatus();
  9449. }
  9450. });
  9451. });
  9452. GM_registerMenuCommand("🎵 Dịch File Media", async () => {
  9453. const translator = window.translator;
  9454. if (!translator) return;
  9455. await createFileInput("audio/*, video/*", async (file) => {
  9456. try {
  9457. translator.ui.showTranslatingStatus();
  9458. await translator.media.processMediaFile(file);
  9459. translator.ui.removeTranslatingStatus();
  9460. } catch (error) {
  9461. translator.ui.showNotification(error.message);
  9462. } finally {
  9463. translator.ui.removeTranslatingStatus();
  9464. }
  9465. });
  9466. });
  9467. GM_registerMenuCommand("📄 Dịch File HTML", async () => {
  9468. const translator = window.translator;
  9469. if (!translator) return;
  9470. await createFileInput(".html,.htm", async (file) => {
  9471. try {
  9472. translator.ui.showTranslatingStatus();
  9473. const content = await translator.ui.readFileContent(file);
  9474. const translatedHTML = await translator.page.translateHTML(content);
  9475. const blob = new Blob([translatedHTML], { type: "text/html" });
  9476. const url = URL.createObjectURL(blob);
  9477. const a = document.createElement("a");
  9478. a.href = url;
  9479. a.download = `king1x32_translated_${file.name}`;
  9480. translator.ui.shadowRoot.appendChild(a);
  9481. a.click();
  9482. URL.revokeObjectURL(url);
  9483. a.remove();
  9484. translator.ui.removeTranslatingStatus();
  9485. translator.ui.showNotification("Dịch file HTML thành công", "success");
  9486. } catch (error) {
  9487. console.error("Lỗi dịch file HTML:", error);
  9488. translator.ui.showNotification(error.message, "error");
  9489. } finally {
  9490. translator.ui.removeTranslatingStatus();
  9491. }
  9492. });
  9493. });
  9494. GM_registerMenuCommand("📑 Dịch File PDF", async () => {
  9495. const translator = window.translator;
  9496. if (!translator) return;
  9497. await createFileInput(".pdf", async (file) => {
  9498. try {
  9499. translator.ui.showLoadingStatus("Đang xử lý PDF...");
  9500. const translatedBlob = await translator.page.translatePDF(file);
  9501. const url = URL.createObjectURL(translatedBlob);
  9502. const a = document.createElement("a");
  9503. a.href = url;
  9504. a.download = `king1x32_translated_${file.name.replace(".pdf", ".html")}`;
  9505. translator.ui.shadowRoot.appendChild(a);
  9506. a.click();
  9507. URL.revokeObjectURL(url);
  9508. a.remove();
  9509. translator.ui.removeTranslatingStatus();
  9510. translator.ui.showNotification("Dịch PDF thành công", "success");
  9511. } catch (error) {
  9512. console.error("Lỗi dịch PDF:", error);
  9513. translator.ui.showNotification(error.message, "error");
  9514. } finally {
  9515. translator.ui.removeLoadingStatus();
  9516. }
  9517. });
  9518. });
  9519. GM_registerMenuCommand("📄 Dịch File (srt, vtt, md, json, txt, html)", async () => {
  9520. const translator = window.translator;
  9521. if (!translator) return;
  9522. const supportedFormats = RELIABLE_FORMATS.text.formats
  9523. .map(f => `.${f.ext}`)
  9524. .join(',');
  9525. await createFileInput(supportedFormats, async (file) => {
  9526. try {
  9527. translator.ui.showTranslatingStatus();
  9528. const result = await translator.translateFile(file);
  9529. const blob = new Blob([result], { type: file.type });
  9530. const url = URL.createObjectURL(blob);
  9531. const a = document.createElement('a');
  9532. a.href = url;
  9533. a.download = `king1x32_translated_${file.name}`;
  9534. translator.ui.shadowRoot.appendChild(a);
  9535. a.click();
  9536. URL.revokeObjectURL(url);
  9537. a.remove();
  9538. translator.ui.removeTranslatingStatus();
  9539. translator.ui.showNotification("Dịch file thành công", "success");
  9540. } catch (error) {
  9541. console.error("Lỗi dịch file:", error);
  9542. translator.ui.showNotification(error.message, "error");
  9543. } finally {
  9544. translator.ui.removeTranslatingStatus();
  9545. }
  9546. });
  9547. });
  9548. GM_registerMenuCommand("⚙️ Cài đặt King Translator AI", () => {
  9549. const translator = window.translator;
  9550. if (translator) {
  9551. const settingsUI = translator.userSettings.createSettingsUI();
  9552. translator.ui.shadowRoot.appendChild(settingsUI);
  9553. }
  9554. });
  9555. new Translator();
  9556. })();