// ==UserScript== // @name PolyGlot Translator // @name:zh-CN 多语智能翻译器 // @description 多引擎智能网页翻译工具,支持实时双语对照、批量翻译、自定义排除 // @version 1.0.0 // @author PolyGlot Team // @namespace https://github.com/polyglot-translator // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @connect translate.googleapis.com // @connect api.cognitive.microsofttranslator.com // @connect api.transmart.qq.com // @run-at document-start // @license MIT // ==/UserScript== /*! * PolyGlot Translator v1.0.0 * 多引擎智能网页翻译工具 * 版权所有 (c) 2024 PolyGlot Team * 许可证:MIT License */ (function() { 'use strict'; // ==================== 配置管理器 ==================== class ConfigManager { static DEFAULTS = { engine: 'smart', targetLang: 'zh-CN', autoMode: true, excludedHosts: [], displayMode: 'bilingual', uiPosition: { x: -1, y: -1 }, cacheEnabled: true, batchSize: 25, concurrency: 4, theme: 'auto' }; static async load() { const config = {}; for (const [key, defaultValue] of Object.entries(this.DEFAULTS)) { config[key] = await GM_getValue(key, defaultValue); } // 特殊处理数组类型 if (typeof config.excludedHosts === 'string') { try { config.excludedHosts = JSON.parse(config.excludedHosts); } catch { config.excludedHosts = []; } } // 特殊处理对象类型 if (typeof config.uiPosition === 'string') { try { config.uiPosition = JSON.parse(config.uiPosition); } catch { config.uiPosition = this.DEFAULTS.uiPosition; } } return config; } static async save(updates) { for (const [key, value] of Object.entries(updates)) { if (Array.isArray(value) || typeof value === 'object') { await GM_setValue(key, JSON.stringify(value)); } else { await GM_setValue(key, value); } } } } // ==================== 缓存系统 ==================== class AdvancedCache { constructor(namespace = 'translation_cache', maxSize = 5000, ttl = 86400000) { this.namespace = namespace; this.maxSize = maxSize; this.ttl = ttl; this.memoryCache = new Map(); this.hits = 0; this.misses = 0; this.init(); } async init() { try { const saved = localStorage.getItem(this.namespace); if (saved) { const data = JSON.parse(saved); const now = Date.now(); for (const [key, entry] of Object.entries(data)) { if (now - entry.t < this.ttl) { this.memoryCache.set(key, entry.v); } } } } catch (e) { console.warn('[PolyGlot] 缓存加载失败:', e); } } async get(key) { const memoryResult = this.memoryCache.get(key); if (memoryResult && this.isValid(memoryResult)) { this.hits++; return memoryResult.value; } this.misses++; return null; } async set(key, value) { const entry = { value, timestamp: Date.now(), accessCount: 0 }; this.updateMemoryCache(key, entry); this.updatePersistentCache(key, entry).catch(() => {}); } updateMemoryCache(key, entry) { if (this.memoryCache.size >= this.maxSize) { const entries = Array.from(this.memoryCache.entries()); entries.sort((a, b) => { const scoreA = this.calculateEvictionScore(a[1]); const scoreB = this.calculateEvictionScore(b[1]); return scoreA - scoreB; }); const toDelete = Math.floor(this.maxSize * 0.2); for (let i = 0; i < toDelete; i++) { this.memoryCache.delete(entries[i][0]); } } this.memoryCache.set(key, entry); } calculateEvictionScore(entry) { const age = Date.now() - entry.timestamp; const accessRatio = entry.accessCount / (age / 1000 / 60); return (age / this.ttl) * 0.7 + (1 / (accessRatio + 1)) * 0.3; } isValid(entry) { return Date.now() - entry.timestamp < this.ttl; } async updatePersistentCache(key, entry) { try { const saved = localStorage.getItem(this.namespace) || '{}'; const data = JSON.parse(saved); data[key] = { v: entry.value, t: entry.timestamp }; if (Object.keys(data).length > this.maxSize * 2) { const entries = Object.entries(data); entries.sort((a, b) => b[1].t - a[1].t); const trimmed = Object.fromEntries(entries.slice(0, this.maxSize)); localStorage.setItem(this.namespace, JSON.stringify(trimmed)); } else { localStorage.setItem(this.namespace, JSON.stringify(data)); } } catch (e) { // 忽略持久化失败 } } clear() { this.memoryCache.clear(); localStorage.removeItem(this.namespace); } getStats() { return { size: this.memoryCache.size, hits: this.hits, misses: this.misses, hitRate: this.hits + this.misses === 0 ? 0 : (this.hits / (this.hits + this.misses) * 100).toFixed(1) }; } } // ==================== 翻译引擎 ==================== class TranslationEngine { constructor(name) { this.name = name; this.cache = new AdvancedCache(`engine_${name}`); } async translate(text, targetLang) { throw new Error('必须由子类实现'); } async batchTranslate(texts, targetLang) { const results = []; for (const text of texts) { try { results.push(await this.translate(text, targetLang)); } catch (error) { results.push(null); } } return results; } } class GoogleEngine extends TranslationEngine { constructor() { super('google'); this.apiKey = null; this.apiKeyTime = 0; } async getApiKey() { if (this.apiKey && Date.now() - this.apiKeyTime < 1200000) { return this.apiKey; } try { const response = await this.request({ method: 'GET', url: 'https://translate.googleapis.com/_/translate_http/_/js/k=translate_http.tr.en_US.YusFYy3P_ro.O/am=AAg/d=1/exm=el_conf/ed=1/rs=AN8SPfq1Hb8iJRleQqQc8zhdzXmF9E56eQ/m=el_main', timeout: 5000 }); const match = response.responseText.match(/['"]x-goog-api-key['"]\s*:\s*['"](\w{39})['"]/i); if (match) { this.apiKey = match[1]; this.apiKeyTime = Date.now(); return this.apiKey; } } catch (error) { // 使用备用密钥 this.apiKey = 'AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw'; } return this.apiKey; } async translate(text, targetLang) { const cacheKey = `${targetLang}:${text}`; const cached = await this.cache.get(cacheKey); if (cached) return cached; const apiKey = await this.getApiKey(); const data = { q: text, target: targetLang, format: 'text' }; try { const response = await this.request({ method: 'POST', url: 'https://translation.googleapis.com/language/translate/v2', headers: { 'Content-Type': 'application/json', 'X-Goog-Api-Key': apiKey }, data: JSON.stringify(data) }); const result = JSON.parse(response.responseText); if (result.data && result.data.translations && result.data.translations[0]) { const translation = result.data.translations[0].translatedText; await this.cache.set(cacheKey, translation); return translation; } throw new Error('Invalid response'); } catch (error) { throw new Error(`Google翻译失败: ${error.message}`); } } async request(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 10000, ...options, onload: resolve, onerror: reject, ontimeout: reject }); }); } } class MicrosoftEngine extends TranslationEngine { constructor() { super('microsoft'); this.authToken = null; this.tokenTime = 0; } async getAuthToken() { if (this.authToken && Date.now() - this.tokenTime < 480000) { return this.authToken; } try { const response = await this.request({ method: 'GET', url: 'https://edge.microsoft.com/translate/auth', timeout: 5000 }); this.authToken = response.responseText; this.tokenTime = Date.now(); return this.authToken; } catch (error) { throw new Error('Microsoft认证失败'); } } async translate(text, targetLang) { const cacheKey = `${targetLang}:${text}`; const cached = await this.cache.get(cacheKey); if (cached) return cached; const token = await this.getAuthToken(); const data = [{ Text: text }]; try { const response = await this.request({ method: 'POST', url: `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${targetLang}`, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); const result = JSON.parse(response.responseText); if (result[0] && result[0].translations && result[0].translations[0]) { const translation = result[0].translations[0].text; await this.cache.set(cacheKey, translation); return translation; } throw new Error('Invalid response'); } catch (error) { throw new Error(`Microsoft翻译失败: ${error.message}`); } } async request(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 10000, ...options, onload: resolve, onerror: reject, ontimeout: reject }); }); } } class TencentEngine extends TranslationEngine { constructor() { super('tencent'); this.clientKey = `browser-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } async translate(text, targetLang) { const cacheKey = `${targetLang}:${text}`; const cached = await this.cache.get(cacheKey); if (cached) return cached; const data = { header: { fn: 'auto_translation', client_key: this.clientKey }, type: 'plain', model_category: 'normal', source: { lang: 'auto', text_list: [text] }, target: { lang: targetLang === 'zh-CN' ? 'zh' : targetLang } }; try { const response = await this.request({ method: 'POST', url: 'https://api.transmart.qq.com/api/imt', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); const result = JSON.parse(response.responseText); if (result.auto_translation && result.auto_translation[0]) { const translation = result.auto_translation[0]; await this.cache.set(cacheKey, translation); return translation; } throw new Error('Invalid response'); } catch (error) { throw new Error(`腾讯翻译失败: ${error.message}`); } } async request(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 10000, ...options, onload: resolve, onerror: reject, ontimeout: reject }); }); } } // ==================== 引擎管理器 ==================== class EngineManager { constructor() { this.engines = { google: new GoogleEngine(), microsoft: new MicrosoftEngine(), tencent: new TencentEngine() }; this.currentEngine = 'google'; this.performanceMetrics = []; this.fallbackOrder = ['google', 'microsoft', 'tencent']; } async translate(text, targetLang, options = {}) { if (!text || !text.trim()) return null; const engine = this.engines[this.currentEngine]; if (!engine) { return await this.tryFallback(text, targetLang, 'Engine not found'); } const startTime = performance.now(); try { const result = await engine.translate(text, targetLang); const duration = performance.now() - startTime; this.recordPerformance(this.currentEngine, { textLength: text.length, duration, success: true }); return result; } catch (error) { return await this.tryFallback(text, targetLang, error); } } async tryFallback(text, targetLang, originalError) { for (const engineName of this.fallbackOrder) { if (engineName === this.currentEngine) continue; try { const engine = this.engines[engineName]; if (!engine) continue; console.log(`[PolyGlot] 切换到备用引擎: ${engineName}`); const result = await engine.translate(text, targetLang); // 切换当前引擎 this.currentEngine = engineName; return result; } catch (fallbackError) { continue; } } console.error('[PolyGlot] 所有翻译引擎均失败:', originalError); return null; } async batchTranslate(texts, targetLang) { const results = new Array(texts.length).fill(null); const engine = this.engines[this.currentEngine]; if (!engine) { return results; } try { const startTime = performance.now(); const translations = await engine.batchTranslate(texts, targetLang); const duration = performance.now() - startTime; this.recordPerformance(this.currentEngine, { textLength: texts.reduce((sum, t) => sum + t.length, 0), duration, success: true, batchSize: texts.length }); return translations; } catch (error) { console.error('[PolyGlot] 批量翻译失败:', error); return results; } } recordPerformance(engineName, metrics) { this.performanceMetrics.push({ engine: engineName, timestamp: Date.now(), ...metrics }); // 保留最近100条记录 if (this.performanceMetrics.length > 100) { this.performanceMetrics.shift(); } } getPerformanceStats() { const stats = {}; for (const engineName in this.engines) { const engineMetrics = this.performanceMetrics.filter(m => m.engine === engineName); if (engineMetrics.length === 0) continue; stats[engineName] = { count: engineMetrics.length, avgDuration: engineMetrics.reduce((sum, m) => sum + m.duration, 0) / engineMetrics.length, successRate: engineMetrics.filter(m => m.success).length / engineMetrics.length * 100 }; } return stats; } setEngine(engineName) { if (this.engines[engineName]) { this.currentEngine = engineName; return true; } return false; } } // ==================== DOM处理器 ==================== class DOMProcessor { constructor(config) { this.config = config; this.translatedNodes = new WeakSet(); this.skipTags = /^(script|style|code|pre|svg|math|noscript|iframe|canvas|video|audio|img|br|hr|input|select|option|textarea)$/i; this.skipClasses = /polyglot-translated|notranslate|katex|mathjax|code/i; this.languagePatterns = { 'zh': /^[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\s\d\p{P}]+$/u, 'en': /^[a-zA-Z\s\d\p{P}]+$/u, 'ja': /^[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff\s\d\p{P}]+$/u, 'ko': /^[\uac00-\ud7af\u1100-\u11ff\s\d\p{P}]+$/u, 'ar': /^[\u0600-\u06ff\u0750-\u077f\s\d\p{P}]+$/u, 'ru': /^[\u0400-\u04ff\s\d\p{P}]+$/u }; } shouldSkip(node) { if (!node) return true; if (node.nodeType === Node.ELEMENT_NODE) { if (this.skipTags.test(node.tagName)) return true; if (node.classList && this.skipClasses.test(node.className)) return true; if (node.isContentEditable) return true; if (node.dataset && node.dataset.polyglotTranslated) return true; } return false; } isTargetLanguage(text) { if (!text || !text.trim()) return true; const lang = this.config.targetLang.split('-')[0]; const pattern = this.languagePatterns[lang]; if (!pattern) return false; return pattern.test(text.trim()); } collectTextNodes(root, options = {}) { const { skipTranslated = true, minLength = 2, maxLength = 1000 } = options; const nodes = []; const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { if (this.shouldSkip(node.parentElement)) { return NodeFilter.FILTER_REJECT; } if (skipTranslated && this.translatedNodes.has(node)) { return NodeFilter.FILTER_REJECT; } const text = node.textContent.trim(); if (text.length < minLength || text.length > maxLength) { return NodeFilter.FILTER_REJECT; } if (/^\d+$/.test(text)) { return NodeFilter.FILTER_REJECT; } if (this.isTargetLanguage(text)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); while (walker.nextNode()) { nodes.push(walker.currentNode); } return nodes; } collectPlaceholders(root) { const elements = []; const placeholders = root.querySelectorAll('input[placeholder], textarea[placeholder]'); for (const el of placeholders) { if (this.shouldSkip(el)) continue; const text = el.placeholder.trim(); if (!text || text.length < 2) continue; if (this.isTargetLanguage(text)) continue; elements.push(el); } return elements; } applyTranslation(node, translation, mode) { if (!translation) return; const parent = node.parentElement; if (!parent) return; this.translatedNodes.add(node); parent.dataset.polyglotTranslated = 'true'; if (!parent.dataset.originalText) { parent.dataset.originalText = node.textContent; } switch (mode) { case 'translated': node.textContent = translation; break; case 'bilingual': const translationSpan = document.createElement('span'); translationSpan.className = 'polyglot-translation'; translationSpan.textContent = translation; translationSpan.style.cssText = ` display: block; margin-top: 4px; color: #7C3AED; font-size: 0.9em; border-left: 2px solid rgba(124, 58, 237, 0.3); padding-left: 8px; `; if (node.nextSibling) { parent.insertBefore(translationSpan, node.nextSibling); } else { parent.appendChild(translationSpan); } break; case 'original': // 不进行翻译 break; } } applyPlaceholderTranslation(element, translation) { if (!translation) return; element.dataset.polyglotTranslated = 'true'; if (!element.dataset.originalPlaceholder) { element.dataset.originalPlaceholder = element.placeholder; } element.placeholder = translation; } restoreTranslations() { // 恢复翻译 document.querySelectorAll('[data-polyglot-translated]').forEach(element => { if (element.dataset.originalText) { const textNodes = Array.from(element.childNodes).filter(n => n.nodeType === Node.TEXT_NODE); if (textNodes.length > 0) { textNodes[0].textContent = element.dataset.originalText; } delete element.dataset.originalText; } if (element.dataset.originalPlaceholder) { element.placeholder = element.dataset.originalPlaceholder; delete element.dataset.originalPlaceholder; } delete element.dataset.polyglotTranslated; }); // 移除翻译span document.querySelectorAll('.polyglot-translation').forEach(el => el.remove()); // 清除标记 this.translatedNodes = new WeakSet(); } } // ==================== 主应用 ==================== class PolyGlotTranslator { constructor() { this.config = null; this.engineManager = null; this.domProcessor = null; this.uiManager = null; this.cache = new AdvancedCache(); this.isTranslating = false; this.pendingTranslation = false; this.observer = null; this.scrollHandler = null; this.initialized = false; } async init() { if (this.initialized) return; try { // 加载配置 this.config = await ConfigManager.load(); // 检查排除列表 if (this.config.excludedHosts.includes(window.location.host)) { console.log('[PolyGlot] 当前网站在排除列表中'); return; } // 检查内容类型 if (document.contentType === 'application/xml' || document.contentType === 'application/pdf') { return; } // 初始化组件 this.engineManager = new EngineManager(); this.engineManager.setEngine(this.config.engine); this.domProcessor = new DOMProcessor(this.config); this.uiManager = new UIManager(this); // 等待DOM准备就绪 await this.waitForDOM(); // 初始化UI await this.uiManager.init(); // 设置观察者 this.setupObservers(); // 自动翻译 if (this.config.autoMode && this.config.displayMode !== 'original') { setTimeout(() => this.translatePage(), 300); } this.initialized = true; console.log('[PolyGlot] 初始化完成'); } catch (error) { console.error('[PolyGlot] 初始化失败:', error); } } async waitForDOM() { if (document.body) return; return new Promise(resolve => { const observer = new MutationObserver(() => { if (document.body) { observer.disconnect(); resolve(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); }); } setupObservers() { // 滚动监听 this.scrollHandler = () => { if (!this.config.autoMode) return; const currentHeight = document.documentElement.scrollHeight; if (currentHeight > (this.lastScrollHeight || 0)) { this.lastScrollHeight = currentHeight; this.scheduleTranslation(); } }; window.addEventListener('scroll', this.scrollHandler, { passive: true }); // DOM变化监听 this.observer = new MutationObserver(mutations => { if (!this.config.autoMode) return; let hasChanges = false; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && !this.domProcessor.shouldSkip(node)) { hasChanges = true; break; } } if (hasChanges) break; } if (hasChanges) { this.scheduleTranslation(); } }); this.observer.observe(document.body, { childList: true, subtree: true }); } scheduleTranslation() { if (this.pendingTranslation) return; this.pendingTranslation = true; setTimeout(() => { this.pendingTranslation = false; if (this.config.autoMode) { this.translatePage(); } }, 500); } async translatePage(root = document.body) { if (this.isTranslating) { this.scheduleTranslation(); return; } this.isTranslating = true; try { // 收集需要翻译的内容 const textNodes = this.domProcessor.collectTextNodes(root); const placeholders = this.domProcessor.collectPlaceholders(root); if (textNodes.length === 0 && placeholders.length === 0) { this.isTranslating = false; return; } // 显示进度 this.uiManager.showProgress(textNodes.length + placeholders.length); // 提取文本 const allTexts = []; const nodeMap = []; for (const node of textNodes) { allTexts.push(node.textContent.trim()); nodeMap.push({ type: 'text', node }); } for (const element of placeholders) { allTexts.push(element.placeholder.trim()); nodeMap.push({ type: 'placeholder', element }); } // 批量翻译 const translations = await this.engineManager.batchTranslate(allTexts, this.config.targetLang); // 应用翻译结果 for (let i = 0; i < nodeMap.length; i++) { const item = nodeMap[i]; const translation = translations[i]; if (!translation) continue; if (item.type === 'text') { this.domProcessor.applyTranslation(item.node, translation, this.config.displayMode); } else { this.domProcessor.applyPlaceholderTranslation(item.element, translation); } // 更新进度 this.uiManager.updateProgress(i + 1); } // 隐藏进度 this.uiManager.hideProgress(); } catch (error) { console.error('[PolyGlot] 翻译失败:', error); this.uiManager.showError('翻译失败: ' + error.message); } finally { this.isTranslating = false; } } restorePage() { this.domProcessor.restoreTranslations(); this.uiManager.showMessage('已恢复原文'); } async setEngine(engineName) { if (this.engineManager.setEngine(engineName)) { this.config.engine = engineName; await ConfigManager.save({ engine: engineName }); this.cache.clear(); return true; } return false; } async setTargetLang(langCode) { this.config.targetLang = langCode; await ConfigManager.save({ targetLang: langCode }); this.cache.clear(); } async setDisplayMode(mode) { this.config.displayMode = mode; await ConfigManager.save({ displayMode: mode }); } async setAutoMode(enabled) { this.config.autoMode = enabled; await ConfigManager.save({ autoMode: enabled }); } async excludeCurrentSite() { const host = window.location.host; if (!this.config.excludedHosts.includes(host)) { this.config.excludedHosts.push(host); await ConfigManager.save({ excludedHosts: this.config.excludedHosts }); // 恢复页面并移除UI this.restorePage(); if (this.uiManager) { this.uiManager.remove(); } // 停止观察者 if (this.observer) { this.observer.disconnect(); } if (this.scrollHandler) { window.removeEventListener('scroll', this.scrollHandler); } return true; } return false; } getStats() { return { cache: this.cache.getStats(), performance: this.engineManager.getPerformanceStats(), config: { ...this.config } }; } } // ==================== UI管理器 ==================== class UIManager { constructor(app) { this.app = app; this.container = null; this.button = null; this.panel = null; this.progressBar = null; this.messageBox = null; this.isDragging = false; this.dragStart = { x: 0, y: 0 }; this.position = { x: 0, y: 0 }; } async init() { this.injectStyles(); this.createUI(); this.setupEventListeners(); this.loadPosition(); // 创建菜单命令 this.createMenuCommands(); } injectStyles() { const css = ` .polyglot-ui { position: fixed; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; pointer-events: none; } .polyglot-ui * { pointer-events: auto; box-sizing: border-box; } .polyglot-button { width: 48px; height: 48px; border-radius: 24px; border: none; background: linear-gradient(135deg, #7C3AED, #A78BFA); color: white; cursor: move; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3); transition: all 0.2s ease; touch-action: none; user-select: none; } .polyglot-button:hover { transform: scale(1.05); box-shadow: 0 6px 16px rgba(124, 58, 237, 0.4); } .polyglot-button:active { transform: scale(0.95); } .polyglot-button svg { width: 24px; height: 24px; } .polyglot-panel { position: absolute; top: 60px; right: 0; width: 280px; background: white; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); padding: 16px; display: none; pointer-events: auto; } .polyglot-panel.show { display: block; } .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .panel-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #1F2937; } .panel-close { width: 24px; height: 24px; border: none; background: none; font-size: 20px; cursor: pointer; color: #6B7280; display: flex; align-items: center; justify-content: center; } .panel-close:hover { color: #374151; } .form-group { margin-bottom: 16px; } .form-group label { display: block; margin-bottom: 6px; font-size: 12px; font-weight: 500; color: #6B7280; text-transform: uppercase; letter-spacing: 0.5px; } .form-group select { width: 100%; padding: 8px 12px; border: 1px solid #D1D5DB; border-radius: 6px; font-size: 14px; color: #374151; background: white; cursor: pointer; } .form-group select:focus { outline: none; border-color: #7C3AED; box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1); } .mode-buttons { display: flex; gap: 8px; margin-bottom: 16px; } .mode-button { flex: 1; padding: 8px 12px; border: 1px solid #D1D5DB; border-radius: 6px; background: white; font-size: 12px; font-weight: 500; color: #6B7280; cursor: pointer; text-align: center; } .mode-button:hover { background: #F9FAFB; } .mode-button.active { background: #7C3AED; color: white; border-color: #7C3AED; } .action-buttons { display: flex; gap: 8px; } .action-button { flex: 1; padding: 10px 16px; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; } .action-button.primary { background: #7C3AED; color: white; } .action-button.primary:hover { background: #6D28D9; } .action-button.secondary { background: #F3F4F6; color: #374151; } .action-button.secondary:hover { background: #E5E7EB; } .action-button.danger { background: #FEE2E2; color: #DC2626; } .action-button.danger:hover { background: #FECACA; } .polyglot-progress { position: fixed; top: 20px; right: 20px; background: white; padding: 12px 16px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); display: none; align-items: center; gap: 12px; z-index: 2147483646; } .polyglot-progress.show { display: flex; } .progress-text { font-size: 14px; color: #374151; min-width: 100px; } .progress-bar { width: 100px; height: 4px; background: #E5E7EB; border-radius: 2px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #7C3AED, #A78BFA); transition: width 0.3s ease; } .polyglot-message { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1F2937; color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); font-size: 14px; display: none; z-index: 2147483646; } .polyglot-message.show { display: block; animation: slideUp 0.3s ease; } .polyglot-message.error { background: #DC2626; } .polyglot-message.success { background: #059669; } @keyframes slideUp { from { transform: translateX(-50%) translateY(20px); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } } @media (prefers-color-scheme: dark) { .polyglot-panel { background: #1F2937; color: #F9FAFB; } .panel-header h3 { color: #F9FAFB; } .form-group label { color: #D1D5DB; } .form-group select { background: #374151; border-color: #4B5563; color: #F9FAFB; } .mode-button { background: #374151; border-color: #4B5563; color: #D1D5DB; } .mode-button:hover { background: #4B5563; } .action-button.secondary { background: #374151; color: #D1D5DB; } .action-button.secondary:hover { background: #4B5563; } .polyglot-progress { background: #1F2937; color: #F9FAFB; } .progress-text { color: #F9FAFB; } } `; GM_addStyle(css); } createUI() { // 创建主容器 this.container = document.createElement('div'); this.container.className = 'polyglot-ui'; this.container.style.cssText = ` left: ${this.position.x}px; top: ${this.position.y}px; `; // 创建按钮 this.button = document.createElement('button'); this.button.className = 'polyglot-button'; this.button.innerHTML = ` `; // 创建面板 this.panel = document.createElement('div'); this.panel.className = 'polyglot-panel'; this.panel.innerHTML = this.getPanelHTML(); // 创建进度条 this.progressBar = document.createElement('div'); this.progressBar.className = 'polyglot-progress'; this.progressBar.innerHTML = `
翻译中...
`; // 创建消息框 this.messageBox = document.createElement('div'); this.messageBox.className = 'polyglot-message'; // 组装UI this.container.appendChild(this.button); this.container.appendChild(this.panel); document.body.appendChild(this.container); document.body.appendChild(this.progressBar); document.body.appendChild(this.messageBox); } getPanelHTML() { const languages = { 'zh-CN': '简体中文', 'zh-TW': '繁體中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어', 'fr': 'Français', 'de': 'Deutsch', 'es': 'Español', 'ru': 'Русский' }; const languageOptions = Object.entries(languages) .map(([code, name]) => `` ).join(''); return `

多语翻译

`; } setupEventListeners() { // 按钮点击 this.button.addEventListener('click', (e) => { e.stopPropagation(); this.togglePanel(); }); // 关闭按钮 this.panel.querySelector('.panel-close').addEventListener('click', () => { this.hidePanel(); }); // 引擎选择 this.panel.querySelector('#polyglot-engine').addEventListener('change', async (e) => { const success = await this.app.setEngine(e.target.value); if (success) { this.showMessage('已切换翻译引擎'); } }); // 语言选择 this.panel.querySelector('#polyglot-lang').addEventListener('change', async (e) => { await this.app.setTargetLang(e.target.value); this.showMessage('已切换目标语言'); }); // 模式选择 this.panel.querySelectorAll('.mode-button').forEach(button => { button.addEventListener('click', async (e) => { const mode = e.target.dataset.mode; await this.app.setDisplayMode(mode); // 更新按钮状态 this.panel.querySelectorAll('.mode-button').forEach(btn => { btn.classList.toggle('active', btn.dataset.mode === mode); }); this.showMessage(`已切换到${this.getModeName(mode)}模式`); // 重新翻译 if (mode !== 'original') { setTimeout(() => this.app.translatePage(), 100); } else { this.app.restorePage(); } }); }); // 翻译按钮 this.panel.querySelector('#polyglot-translate').addEventListener('click', () => { this.hidePanel(); this.app.setAutoMode(true); this.app.translatePage(); this.showMessage('开始翻译页面...'); }); // 还原按钮 this.panel.querySelector('#polyglot-restore').addEventListener('click', () => { this.hidePanel(); this.app.setAutoMode(false); this.app.restorePage(); }); // 排除按钮 this.panel.querySelector('#polyglot-exclude').addEventListener('click', async () => { const success = await this.app.excludeCurrentSite(); if (success) { this.showMessage('已排除当前网站'); } }); // 拖拽功能 this.setupDragEvents(); // 点击外部关闭面板 document.addEventListener('click', (e) => { if (this.panel.classList.contains('show') && !this.container.contains(e.target)) { this.hidePanel(); } }); // 键盘快捷键 document.addEventListener('keydown', (e) => { if (e.altKey && e.shiftKey) { switch(e.key.toLowerCase()) { case 't': e.preventDefault(); this.togglePanel(); break; case 'r': e.preventDefault(); this.app.restorePage(); break; case 'e': e.preventDefault(); this.app.translatePage(); break; } } }); } setupDragEvents() { this.button.addEventListener('mousedown', (e) => { if (e.button !== 0) return; this.isDragging = true; this.dragStart.x = e.clientX - this.position.x; this.dragStart.y = e.clientY - this.position.y; this.hidePanel(); const moveHandler = (e) => { if (!this.isDragging) return; this.position.x = e.clientX - this.dragStart.x; this.position.y = e.clientY - this.dragStart.y; this.container.style.left = `${this.position.x}px`; this.container.style.top = `${this.position.y}px`; }; const upHandler = () => { this.isDragging = false; document.removeEventListener('mousemove', moveHandler); document.removeEventListener('mouseup', upHandler); this.savePosition(); }; document.addEventListener('mousemove', moveHandler); document.addEventListener('mouseup', upHandler); }); // 触摸屏支持 this.button.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; this.isDragging = true; this.dragStart.x = touch.clientX - this.position.x; this.dragStart.y = touch.clientY - this.position.y; this.hidePanel(); }, { passive: false }); document.addEventListener('touchmove', (e) => { if (!this.isDragging) return; const touch = e.touches[0]; this.position.x = touch.clientX - this.dragStart.x; this.position.y = touch.clientY - this.dragStart.y; this.container.style.left = `${this.position.x}px`; this.container.style.top = `${this.position.y}px`; }, { passive: false }); document.addEventListener('touchend', () => { this.isDragging = false; this.savePosition(); }); } createMenuCommands() { GM_registerMenuCommand('翻译当前页面', () => { this.app.translatePage(); }); GM_registerMenuCommand('恢复原文', () => { this.app.restorePage(); }); GM_registerMenuCommand('切换翻译引擎', () => { this.showPanel(); }); GM_registerMenuCommand('排除当前网站', async () => { await this.app.excludeCurrentSite(); }); } togglePanel() { this.panel.classList.toggle('show'); } showPanel() { this.panel.classList.add('show'); } hidePanel() { this.panel.classList.remove('show'); } showProgress(total) { this.progressBar.classList.add('show'); this.progressTotal = total; this.progressCurrent = 0; this.updateProgress(0); } updateProgress(current) { this.progressCurrent = current; const percent = this.progressTotal > 0 ? (current / this.progressTotal * 100) : 0; const fill = this.progressBar.querySelector('.progress-fill'); const text = this.progressBar.querySelector('.progress-text'); if (fill) fill.style.width = `${percent}%`; if (text) { text.textContent = `翻译中... ${current}/${this.progressTotal}`; } } hideProgress() { this.progressBar.classList.remove('show'); } showMessage(text, type = 'info', duration = 3000) { this.messageBox.textContent = text; this.messageBox.className = 'polyglot-message show'; if (type === 'error') { this.messageBox.classList.add('error'); } else if (type === 'success') { this.messageBox.classList.add('success'); } setTimeout(() => { this.messageBox.classList.remove('show'); this.messageBox.className = 'polyglot-message'; }, duration); } showError(message) { this.showMessage(message, 'error', 5000); } loadPosition() { const saved = this.app.config.uiPosition; if (saved.x >= 0 && saved.y >= 0) { this.position = saved; } else { // 默认位置:右下角 this.position = { x: window.innerWidth - 60, y: window.innerHeight - 60 }; } this.container.style.left = `${this.position.x}px`; this.container.style.top = `${this.position.y}px`; } savePosition() { this.app.config.uiPosition = this.position; ConfigManager.save({ uiPosition: this.position }); } getModeName(mode) { const names = { translated: '仅译文', bilingual: '双语', original: '原文' }; return names[mode] || mode; } remove() { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } if (this.progressBar && this.progressBar.parentNode) { this.progressBar.parentNode.removeChild(this.progressBar); } if (this.messageBox && this.messageBox.parentNode) { this.messageBox.parentNode.removeChild(this.messageBox); } } } // ==================== 启动应用 ==================== // 等待DOM加载完成后启动应用 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { new PolyGlotTranslator().init(); }); } else { new PolyGlotTranslator().init(); } })();