// ==UserScript== // @name 自动翻译助手 // @name:zh-CN 自动翻译助手 // @name:zh-TW 自動翻譯助手 // @name:zh-HK 自動翻譯助手 // @name:en Auto Translation Assistant // @namespace https://github.com/eraycc // @version 2.5.2 // @description 强大的网页翻译工具,支持多语言,可移动可缩放悬浮窗,白名单管理,微软API引擎支持单显/双显及颜色自定义,切换引擎立即生效,样式完全独立 // @author Eray // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAAhUlEQVQ4je2SMQ6AIAxFeyfH4hLiYh8OoGMZ8Uok1UJ5Tdv0Jf/Sl/db0hAaC6FEUXgHxIGYgRZJqGQpRGOAMUABNkVnzI5I4Qz5PkBOtB6WQlTEnUqxhwhHAK54HUE5/5DxTSyG1njDQRIDaM3zkI5YV/hIlgyjXzGf/6xX1oB+jw8fAAAAAElFTkSuQmCC // @run-at document-start // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect edge.microsoft.com // @connect api-edge.cognitive.microsofttranslator.com // @license Apache-2.0 // @require https://unpkg.com/i18n-jsautotranslate@3.18.89/index.js // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iMTYiIGZpbGw9IiMyOTUzZTgiLz48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxMiIgZmlsbD0iIzQyYTVmNSIvPjxwYXRoIGQ9Ik00IDE2YzAgNi42IDUuNCAxMiAxMiAxMnMxMi01LjQgMTItMTIiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTE2IDR2MjRNMjggMTZINCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSI0IiBmaWxsPSIjMjk1M2U4Ii8+PC9zdmc+ // ==/UserScript== (function() { 'use strict'; // ==================== 工具函数 ==================== function isMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; } function isDarkMode() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } function getCurrentDomain() { return window.location.hostname; } // ==================== 语言映射 ==================== function mapToMicrosoftLang(langCode) { const map = { 'chinese_simplified': 'zh-Hans', 'chinese_traditional': 'zh-Hant', 'english': 'en', 'spanish': 'es', 'french': 'fr', 'german': 'de', 'japanese': 'ja', 'korean': 'ko', 'russian': 'ru', 'portuguese': 'pt', 'italian': 'it', 'dutch': 'nl', 'polish': 'pl', 'turkish': 'tr', 'vietnamese': 'vi', 'thai': 'th', 'indonesian': 'id', 'arabic': 'ar', 'hindi': 'hi' }; return map[langCode] || langCode; } // ==================== 微软翻译引擎实现 ==================== class MicrosoftTranslator { constructor(configManager) { this.configManager = configManager; this.authToken = null; this.active = false; this.pageTranslating = false; this.translationCache = new Map(); this.originalTextMap = new Map(); this.domObserver = null; this.continuousTimer = null; this.lastTranslationTime = 0; this.isObserving = false; this.domChangeTimer = null; this.mode = configManager.get('microsoftMode') || 'replace'; this.textColor = configManager.get('microsoftTextColor') || '#0066cc'; this.bgColor = configManager.get('microsoftBgColor') || 'rgba(0,102,204,0.1)'; } async init() { if (this.active) return true; try { await this.getAuthToken(); this.active = true; console.log('微软翻译引擎初始化成功'); return true; } catch (err) { console.error('微软翻译引擎初始化失败:', err); return false; } } getAuthToken() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://edge.microsoft.com/translate/auth", onload: (res) => { if (res.status === 200) { this.authToken = res.responseText; resolve(); } else { reject(new Error('获取微软认证token失败')); } }, onerror: () => reject(new Error('网络错误')) }); }); } translateBatch(texts, fromLang = null) { return new Promise((resolve, reject) => { if (!this.authToken) { reject(new Error('未获取到认证token')); return; } if (!texts || texts.length === 0) { resolve([]); return; } const fromParam = fromLang ? `&from=${fromLang}` : ''; let targetLang = this.configManager.get('targetLanguage'); targetLang = mapToMicrosoftLang(targetLang); const data = texts.map(text => ({ "Text": text })); GM_xmlhttpRequest({ method: "POST", url: `https://api-edge.cognitive.microsofttranslator.com/translate?to=${targetLang}${fromParam}&api-version=3.0&includeSentenceLength=true`, headers: { "Authorization": `Bearer ${this.authToken}`, "Content-Type": "application/json", }, data: JSON.stringify(data), onload: (res) => { if (res.status === 200) { try { const result = JSON.parse(res.responseText); const translations = result.map(item => item && item.translations && item.translations[0] ? item.translations[0].text : null ); resolve(translations); } catch (e) { reject(new Error('解析微软响应失败')); } } else { reject(new Error(`微软翻译失败: ${res.status}`)); } }, onerror: () => reject(new Error('网络请求失败')) }); }); } collectTextNodes() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const excludedSelectors = [ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'CODE', 'PRE', '.microsoft-translated', '.translation-popup', '.translating-indicator', '.yandex-translate-widget', '.translation-settings-modal', '[data-no-translate]', '.no-translate', '.yandex-no-translate', '.yandex-script-element', '[data-protect-translation="true"]' ]; for (const sel of excludedSelectors) { if (parent.matches(sel) || parent.closest(sel)) return NodeFilter.FILTER_REJECT; } const ignoredClasses = this.configManager.get('ignoredClasses'); if (ignoredClasses) { const classes = ignoredClasses.split(',').map(c => c.trim()); for (const cls of classes) { if (parent.classList && parent.classList.contains(cls)) return NodeFilter.FILTER_REJECT; } } const ignoredIds = this.configManager.get('ignoredIds'); if (ignoredIds) { const ids = ignoredIds.split(',').map(id => id.trim()); for (const id of ids) { if (parent.id === id) return NodeFilter.FILTER_REJECT; } } const text = node.textContent.trim(); if (!text || text.length < 2) return NodeFilter.FILTER_REJECT; if (/^\d+$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*[KMGT]?[B]?$/i.test(text)) return NodeFilter.FILTER_REJECT; if (/^\d+\.?\d*\s*%$/.test(text)) return NodeFilter.FILTER_REJECT; if (/^[\d\s\W]+$/.test(text)) return NodeFilter.FILTER_REJECT; if (!/[a-zA-Z\u0400-\u04FF\u4e00-\u9fff]/.test(text)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); let node; while (node = walker.nextNode()) { const text = node.textContent.trim(); if (text) { textNodes.push({ node: node, text: text, parent: node.parentElement, originalText: node.textContent }); } } return textNodes; } applyTranslation(nodeInfo, translation) { const originalNode = nodeInfo.node; const parent = nodeInfo.parent; const originalText = nodeInfo.originalText || originalNode.textContent; const elementId = 'translated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); this.originalTextMap.set(elementId, originalText); const translatedElement = document.createElement('span'); translatedElement.className = 'microsoft-translated'; if (this.mode === 'replace') { translatedElement.classList.add('replace-mode'); translatedElement.textContent = translation; translatedElement.title = `原文: ${originalText}\n点击恢复原文`; translatedElement.style.color = this.textColor; translatedElement.addEventListener('click', (e) => { e.stopPropagation(); const original = this.originalTextMap.get(elementId); if (original) { const textNode = document.createTextNode(original); parent.replaceChild(textNode, translatedElement); } }); translatedElement.addEventListener('mouseenter', () => { translatedElement.style.backgroundColor = this.bgColor; }); translatedElement.addEventListener('mouseleave', () => { translatedElement.style.backgroundColor = ''; }); } else { // dual mode translatedElement.classList.add('dual-mode'); const originalSpan = document.createElement('span'); originalSpan.className = 'original-text'; originalSpan.textContent = originalText; originalSpan.setAttribute('translate', 'no'); originalSpan.setAttribute('data-no-translate', 'true'); const translatedSpan = document.createElement('span'); translatedSpan.className = 'translated-text'; translatedSpan.textContent = translation; translatedSpan.style.color = this.textColor; translatedSpan.setAttribute('translate', 'no'); translatedSpan.setAttribute('data-no-translate', 'true'); translatedElement.appendChild(originalSpan); translatedElement.appendChild(translatedSpan); } translatedElement.setAttribute('data-original-id', elementId); translatedElement.setAttribute('translate', 'no'); translatedElement.setAttribute('data-no-translate', 'true'); parent.replaceChild(translatedElement, originalNode); } async translateWholePage() { if (this.pageTranslating) return; if (!this.active || !this.authToken) { const ok = await this.init(); if (!ok) return; } this.pageTranslating = true; console.log('开始微软翻译页面...'); let textNodes = this.collectTextNodes(); if (textNodes.length === 0) { this.pageTranslating = false; return; } const MAX_TEXT_LENGTH = 5000; const BATCH_SIZE = 50; let batches = []; let currentBatch = { nodes: [], totalLen: 0 }; for (const nodeInfo of textNodes) { const len = nodeInfo.text.length; if (len > MAX_TEXT_LENGTH) { if (currentBatch.nodes.length) batches.push(currentBatch); batches.push({ nodes: [{ ...nodeInfo, text: nodeInfo.text.substring(0, MAX_TEXT_LENGTH) }], totalLen: MAX_TEXT_LENGTH }); currentBatch = { nodes: [], totalLen: 0 }; continue; } if (currentBatch.totalLen + len > MAX_TEXT_LENGTH || currentBatch.nodes.length >= BATCH_SIZE) { batches.push(currentBatch); currentBatch = { nodes: [], totalLen: 0 }; } currentBatch.nodes.push(nodeInfo); currentBatch.totalLen += len; } if (currentBatch.nodes.length) batches.push(currentBatch); let translatedCount = 0; for (let i = 0; i < batches.length; i++) { const batch = batches[i]; const textsToTranslate = []; const nodeIndices = []; batch.nodes.forEach((nodeInfo, idx) => { const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; if (this.translationCache.has(cacheKey)) { const translation = this.translationCache.get(cacheKey); this.applyTranslation(nodeInfo, translation); translatedCount++; } else { textsToTranslate.push(nodeInfo.text); nodeIndices.push(idx); } }); if (textsToTranslate.length) { try { const fromLang = null; const translations = await this.translateBatch(textsToTranslate, fromLang); translations.forEach((translation, ti) => { const nodeIdx = nodeIndices[ti]; const nodeInfo = batch.nodes[nodeIdx]; if (translation && translation !== nodeInfo.text) { this.applyTranslation(nodeInfo, translation); const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; this.translationCache.set(cacheKey, translation); translatedCount++; } }); } catch (err) { console.error('批次翻译失败:', err); if (err.message.includes('token') || err.message.includes('认证')) { console.log('token可能过期,尝试重新获取'); try { await this.getAuthToken(); const retryTranslations = await this.translateBatch(textsToTranslate, fromLang); retryTranslations.forEach((translation, ti) => { const nodeIdx = nodeIndices[ti]; const nodeInfo = batch.nodes[nodeIdx]; if (translation && translation !== nodeInfo.text) { this.applyTranslation(nodeInfo, translation); const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; this.translationCache.set(cacheKey, translation); translatedCount++; } }); } catch (retryErr) { console.error('重试失败:', retryErr); } } } } await new Promise(r => setTimeout(r, 20)); } this.pageTranslating = false; this.lastTranslationTime = Date.now(); console.log(`微软翻译完成,翻译了 ${translatedCount} 个文本`); } async checkAndTranslate() { if (this.pageTranslating) return; const untranslated = this.collectTextNodes(); if (untranslated.length > 0) { console.log(`发现 ${untranslated.length} 个未翻译文本,重新翻译...`); await this.translateWholePage(); } } startDOMObservation() { if (this.isObserving) return; if (!this.domObserver) { this.domObserver = new MutationObserver(() => { if (this.pageTranslating) return; if (this.domChangeTimer) clearTimeout(this.domChangeTimer); this.domChangeTimer = setTimeout(() => { this.checkAndTranslate(); }, 800); }); } try { this.domObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); this.isObserving = true; console.log('微软翻译DOM监听已启动'); } catch (e) { console.error('启动DOM监听失败', e); } } stopDOMObservation() { if (this.domObserver) { this.domObserver.disconnect(); this.isObserving = false; } if (this.domChangeTimer) { clearTimeout(this.domChangeTimer); this.domChangeTimer = null; } } startContinuousCheck() { if (this.continuousTimer) clearInterval(this.continuousTimer); this.continuousTimer = setInterval(() => { if (!this.pageTranslating && this.active && this.authToken) { const now = Date.now(); if (now - this.lastTranslationTime > 30000) { this.checkAndTranslate(); } } }, 30000); } stopContinuousCheck() { if (this.continuousTimer) { clearInterval(this.continuousTimer); this.continuousTimer = null; } } async enable() { const success = await this.init(); if (!success) return false; await this.translateWholePage(); this.startDOMObservation(); this.startContinuousCheck(); return true; } disable() { this.stopDOMObservation(); this.stopContinuousCheck(); document.querySelectorAll('.microsoft-translated').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); this.active = false; this.authToken = null; console.log('微软翻译已禁用'); } async onLanguageChange() { if (!this.active) return; this.translationCache.clear(); await this.translateWholePage(); } async onIgnoreChange() { if (!this.active) return; document.querySelectorAll('.microsoft-translated').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); await this.translateWholePage(); } async onModeColorChange() { this.mode = this.configManager.get('microsoftMode') || 'replace'; this.textColor = this.configManager.get('microsoftTextColor') || '#0066cc'; this.bgColor = this.configManager.get('microsoftBgColor') || 'rgba(0,102,204,0.1)'; if (!this.active) return; document.querySelectorAll('.microsoft-translated').forEach(el => { const originalId = el.dataset.originalId; if (originalId && this.originalTextMap.has(originalId)) { const originalText = this.originalTextMap.get(originalId); const textNode = document.createTextNode(originalText); el.parentNode.replaceChild(textNode, el); } }); this.originalTextMap.clear(); this.translationCache.clear(); await this.translateWholePage(); } } // ==================== 配置管理器 ==================== class ConfigManager { constructor() { this.defaultConfig = { enabled: true, localLanguage: 'english', targetLanguage: 'chinese_simplified', autoTranslate: false, translateService: 'client.edge', customServiceUrls: '', panelPosition: { x: 100, y: 100 }, panelWidth: 380, panelHeight: 500, panelOpacity: 1, ignoredClasses: '', ignoredIds: '', customTerms: '', enableListener: true, enableCache: true, translateAttributes: ['title', 'alt', 'placeholder'], whitelist: [], siteSpecificServices: {}, enableSelectionTranslate: true, microsoftMode: 'replace', microsoftTextColor: '#0066cc', microsoftBgColor: 'rgba(0,102,204,0.1)' }; this.config = this.loadConfig(); } loadConfig() { const saved = GM_getValue('translateConfig', null); if (saved) { if (!saved.panelWidth) saved.panelWidth = this.defaultConfig.panelWidth; if (!saved.panelHeight) saved.panelHeight = this.defaultConfig.panelHeight; if (!saved.localLanguage) saved.localLanguage = this.defaultConfig.localLanguage; if (saved.enableSelectionTranslate === undefined) saved.enableSelectionTranslate = this.defaultConfig.enableSelectionTranslate; if (!saved.microsoftMode) saved.microsoftMode = this.defaultConfig.microsoftMode; if (!saved.microsoftTextColor) saved.microsoftTextColor = this.defaultConfig.microsoftTextColor; if (!saved.microsoftBgColor) saved.microsoftBgColor = this.defaultConfig.microsoftBgColor; return { ...this.defaultConfig, ...saved }; } return { ...this.defaultConfig }; } saveConfig() { GM_setValue('translateConfig', this.config); } get(key) { return this.config[key]; } set(key, value) { this.config[key] = value; this.saveConfig(); } reset() { GM_deleteValue('translateConfig'); this.config = { ...this.defaultConfig }; this.saveConfig(); } clearCache() { if (typeof translate !== 'undefined') { try { translate.storage.clear(); } catch (e) { console.log('清除translate.js缓存失败:', e); } } const keys = Object.keys(localStorage); let cleared = 0; keys.forEach(key => { if (key.includes('translate_') || key.includes('hash_')) { localStorage.removeItem(key); cleared++; } }); return cleared; } isInWhitelist(domain) { return this.config.whitelist.includes(domain); } addToWhitelist(domain) { if (!this.isInWhitelist(domain)) { this.config.whitelist.push(domain); this.saveConfig(); return true; } return false; } removeFromWhitelist(domain) { const index = this.config.whitelist.indexOf(domain); if (index !== -1) { this.config.whitelist.splice(index, 1); this.saveConfig(); return true; } return false; } clearWhitelist() { this.config.whitelist = []; this.saveConfig(); } toggleWhitelistDomain(domain) { if (this.isInWhitelist(domain)) { this.removeFromWhitelist(domain); return false; } else { this.addToWhitelist(domain); return true; } } shouldTranslate() { if (this.isInWhitelist(getCurrentDomain())) return true; return this.config.enabled; } getSiteService(domain) { return this.config.siteSpecificServices[domain] || null; } setSiteService(domain, service) { if (service && service !== this.config.translateService) { this.config.siteSpecificServices[domain] = service; } else { delete this.config.siteSpecificServices[domain]; } this.saveConfig(); } getEffectiveService(domain) { const siteService = this.getSiteService(domain); return siteService || this.config.translateService; } } // ==================== 翻译管理器 ==================== class TranslateManager { constructor(configManager) { this.configManager = configManager; this.initialized = false; this.listenerStarted = false; this.currentLanguage = null; this.isTranslating = false; this.microsoftTranslator = null; } init() { if (this.initialized) return; const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft') { if (!this.microsoftTranslator) { this.microsoftTranslator = new MicrosoftTranslator(this.configManager); } this.initialized = true; console.log('翻译管理器初始化完成,使用微软API引擎'); return; } if (typeof translate === 'undefined') return; try { translate.language.setLocal(this.configManager.get('localLanguage')); translate.service.use(service); if (service === 'translate.service') { const customUrls = this.configManager.get('customServiceUrls'); if (customUrls) { const urls = customUrls.split(',').map(url => url.trim()).filter(url => url); if (urls.length > 0) { translate.request.api.host = urls.length === 1 ? urls[0] : urls; } } } translate.selectLanguageTag.show = false; this.applyIgnoreSettings(); this.applyCustomTerms(); const translateAttributes = this.configManager.get('translateAttributes'); if (translateAttributes.length > 0) { translate.translateAttributes = translateAttributes; } translate.showOrigin = false; if (this.configManager.get('enableCache')) { translate.storage.enable(); } else { translate.storage.disable(); } this.initialized = true; console.log('翻译管理器初始化完成,使用translate.js引擎'); } catch (error) { console.error('翻译初始化失败:', error); } } applyIgnoreSettings() { if (typeof translate === 'undefined') return; translate.ignore.class = []; translate.ignore.id = []; const ignoredClasses = this.configManager.get('ignoredClasses'); if (ignoredClasses) { ignoredClasses.split(',').map(c => c.trim()).filter(c => c).forEach(cls => translate.ignore.class.push(cls)); } const ignoredIds = this.configManager.get('ignoredIds'); if (ignoredIds) { ignoredIds.split(',').map(id => id.trim()).filter(id => id).forEach(id => translate.ignore.id.push(id)); } } applyCustomTerms() { if (typeof translate === 'undefined') return; const customTerms = this.configManager.get('customTerms'); if (!customTerms) return; const localLang = this.configManager.get('localLanguage'); const targetLang = this.configManager.get('targetLanguage'); translate.nomenclature.append(localLang, targetLang, customTerms); } startListener() { if (!this.listenerStarted && typeof translate !== 'undefined') { try { if (this.configManager.get('enableListener')) { translate.listener.start(); this.listenerStarted = true; console.log('动态内容监听已启动'); } } catch (error) { if (!error.message?.includes('已经启动')) { console.error('启动监听失败:', error); } } } } async changeLanguage(targetLang, force = false) { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft') { if (!this.microsoftTranslator) { this.microsoftTranslator = new MicrosoftTranslator(this.configManager); } if (force || this.currentLanguage !== targetLang) { this.currentLanguage = targetLang; await this.microsoftTranslator.enable(); } return; } if (!this.initialized) this.init(); if (typeof translate === 'undefined') return; if (!force && this.currentLanguage === targetLang && this.isTranslating) return; this.isTranslating = true; this.init(); this.applyIgnoreSettings(); this.applyCustomTerms(); this.startListener(); translate.changeLanguage(targetLang); setTimeout(() => { this.isTranslating = false; this.currentLanguage = targetLang; }, 1000); } forceTranslate() { const targetLang = this.configManager.get('targetLanguage'); this.changeLanguage(targetLang, true); } refresh() { const should = this.configManager.shouldTranslate(); if (should) { this.forceTranslate(); } else { this.changeLanguage(null, true); } } // 新增:禁用当前翻译并恢复原文 async disableCurrentTranslation() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft') { if (this.microsoftTranslator && this.microsoftTranslator.active) { this.microsoftTranslator.disable(); } } else { // 使用 translate.js 的 changeLanguage(null) 清除翻译 if (typeof translate !== 'undefined') { translate.changeLanguage(null); } } } startAutoTranslate() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft') { if (!this.microsoftTranslator) { this.microsoftTranslator = new MicrosoftTranslator(this.configManager); } const should = this.configManager.shouldTranslate(); if (should) { setTimeout(() => this.microsoftTranslator.enable(), 100); } return; } if (!this.initialized) this.init(); this.startListener(); const should = this.configManager.shouldTranslate(); const auto = this.configManager.get('autoTranslate'); if (should && (auto || this.configManager.isInWhitelist(getCurrentDomain()))) { setTimeout(() => this.forceTranslate(), 100); } else if (!should && !auto) { setTimeout(() => this.changeLanguage(null, true), 100); } } startSelectionTranslate() { if (typeof translate !== 'undefined') { try { translate.language.setDefaultTo(this.configManager.get('targetLanguage')); translate.selectionTranslate.start(); } catch (error) { console.error('启动划词翻译失败:', error); } } } stopSelectionTranslate() { if (typeof translate !== 'undefined' && translate.selectionTranslate) { try { translate.selectionTranslate.stop(); } catch (error) { console.error('停止划词翻译失败:', error); } } } async onIgnoreChange() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft' && this.microsoftTranslator && this.microsoftTranslator.active) { await this.microsoftTranslator.onIgnoreChange(); } else { this.refresh(); } } async onLanguageChange() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft' && this.microsoftTranslator && this.microsoftTranslator.active) { await this.microsoftTranslator.onLanguageChange(); } else { this.refresh(); } } async onMicrosoftModeColorChange() { if (this.microsoftTranslator && this.microsoftTranslator.active) { await this.microsoftTranslator.onModeColorChange(); } } } // ==================== 提示管理器 ==================== class ToastManager { constructor() { this.container = null; this.init(); } init() { this.container = document.createElement('div'); this.container.id = 'translate-toast-container'; this.container.style.cssText = ` position: fixed !important; top: 20px !important; right: 20px !important; z-index: 2147483647 !important; pointer-events: none !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; `; document.body.appendChild(this.container); } show(message, type = 'success', duration = 2000) { const toast = document.createElement('div'); const bgColor = type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'; toast.style.cssText = ` background: ${bgColor} !important; color: white !important; padding: 12px 20px !important; border-radius: 12px !important; margin-bottom: 10px !important; box-shadow: 0 10px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.02) !important; animation: slideInRight 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; pointer-events: auto !important; font-size: 14px !important; font-weight: 500 !important; backdrop-filter: blur(8px) !important; `; toast.textContent = message; this.container.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease !important'; setTimeout(() => { if (this.container.contains(toast)) this.container.removeChild(toast); }, 300); }, duration); } } // ==================== UI管理器 ==================== class UIManager { constructor(configManager, translateManager) { this.configManager = configManager; this.translateManager = translateManager; this.panel = null; this.toast = new ToastManager(); this.isDragging = false; this.isResizing = false; this.init(); } init() { this.injectStyles(); this.createPanel(); this.bindEvents(); setTimeout(() => this.translateManager.startAutoTranslate(), 500); if (!isMobile()) setTimeout(() => this.translateManager.startSelectionTranslate(), 1500); } injectStyles() { const darkMode = isDarkMode(); GM_addStyle(` @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } @keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } ${darkMode ? ` #translate-panel { background: rgba(30, 30, 35, 0.95) !important; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); } .translate-panel-header { background: rgba(0, 0, 0, 0.3) !important; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .translate-control-label { color: #e5e7eb !important; } .translate-select, .translate-input, .translate-textarea { background: rgba(50, 50, 55, 0.8) !important; color: #e5e7eb !important; border-color: rgba(255, 255, 255, 0.1) !important; } .translate-select:focus, .translate-input:focus, .translate-textarea:focus { border-color: #8b5cf6 !important; box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); } .translate-slider { background: rgba(255, 255, 255, 0.2) !important; } .translate-slider-value { color: #9ca3af !important; } .translate-section-title { color: #e5e7eb !important; border-bottom-color: rgba(255, 255, 255, 0.1) !important; } .translate-info { background: rgba(0, 0, 0, 0.3) !important; color: #9ca3af !important; } .translate-description { color: #9ca3af !important; } .translate-button { background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%) !important; } .translate-button:hover { filter: brightness(1.1); } .resize-handle { background: rgba(255,255,255,0.2); } ` : ` #translate-panel { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.3); } .translate-panel-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .resize-handle { background: rgba(0,0,0,0.1); } `} #translate-panel { position: fixed !important; border-radius: 12px !important; box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.2) !important; z-index: 2147483646 !important; display: none !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; overflow: hidden !important; touch-action: none !important; user-select: none !important; -webkit-user-select: none !important; transition: none !important; min-width: 280px !important; min-height: 300px !important; max-width: 90vw !important; max-height: 80vh !important; } #translate-panel.show { display: flex !important; flex-direction: column !important; animation: fadeIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) !important; } #translate-panel.dragging { transition: none !important; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.3) !important; cursor: grabbing !important; } #translate-panel.resizing { transition: none !important; cursor: nw-resize !important; } .translate-panel-header { padding: 12px 16px !important; display: flex !important; justify-content: space-between !important; align-items: center !important; cursor: move !important; border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important; flex-shrink: 0 !important; } .translate-panel-title { font-size: 16px !important; font-weight: 600 !important; user-select: none !important; letter-spacing: -0.01em !important; display: flex !important; align-items: center !important; gap: 6px !important; color: white !important; } .translate-panel-close { width: 28px !important; height: 28px !important; border-radius: 50% !important; background: rgba(0, 0, 0, 0.1) !important; border: none !important; cursor: pointer !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.2s !important; font-size: 16px !important; color: white !important; } .translate-panel-close:hover { background: rgba(0, 0, 0, 0.2) !important; transform: scale(1.05) !important; } .translate-panel-close:active { transform: scale(0.95) !important; } .translate-panel-body { padding: 16px !important; overflow-y: auto !important; -webkit-overflow-scrolling: touch !important; cursor: default !important; flex: 1 !important; } .translate-panel-body::-webkit-scrollbar { width: 4px !important; } .translate-panel-body::-webkit-scrollbar-track { background: transparent !important; } .translate-panel-body::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2) !important; border-radius: 10px !important; } .translate-control-group { margin-bottom: 16px !important; } .translate-control-label { display: flex !important; align-items: center !important; gap: 6px !important; margin-bottom: 6px !important; font-size: 13px !important; font-weight: 500 !important; ${darkMode ? `color: #e5e7eb !important;` : `color: #1f2937 !important;`} } .translate-description { font-size: 11px !important; ${darkMode ? `color: #9ca3af !important;` : `color: #6b7280 !important;`} margin-top: 2px !important; font-weight: normal !important; } .translate-switch { position: relative !important; display: inline-block !important; width: 40px !important; height: 22px !important; } .translate-switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; } .translate-switch-slider { position: absolute !important; cursor: pointer !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background-color: #cbd5e1 !important; transition: .2s !important; border-radius: 34px !important; } .translate-switch-slider:before { position: absolute !important; content: "" !important; height: 16px !important; width: 16px !important; left: 3px !important; bottom: 3px !important; background-color: white !important; transition: .2s !important; border-radius: 50% !important; box-shadow: 0 1px 2px rgba(0,0,0,0.1) !important; } .translate-switch input:checked + .translate-switch-slider { background-color: #8b5cf6 !important; } .translate-switch input:checked + .translate-switch-slider:before { transform: translateX(18px) !important; } .translate-select, .translate-input { width: 100% !important; padding: 8px 12px !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; font-size: 13px !important; transition: all 0.2s !important; box-sizing: border-box !important; font-family: inherit !important; ${darkMode ? ` background: rgba(50, 50, 55, 0.8) !important; color: #e5e7eb !important; border-color: rgba(255, 255, 255, 0.1) !important; ` : ` background: rgba(255, 255, 255, 0.8) !important; color: #1f2937 !important; `} } .translate-select:focus, .translate-input:focus { outline: none !important; border-color: #8b5cf6 !important; box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.1) !important; } .translate-textarea { width: 100% !important; min-height: 80px !important; padding: 8px 12px !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; font-size: 12px !important; transition: all 0.2s !important; box-sizing: border-box !important; resize: vertical !important; font-family: monospace !important; line-height: 1.4 !important; ${darkMode ? ` background: rgba(50, 50, 55, 0.8) !important; color: #e5e7eb !important; border-color: rgba(255, 255, 255, 0.1) !important; ` : ` background: rgba(255, 255, 255, 0.8) !important; color: #1f2937 !important; `} } .translate-textarea:focus { outline: none !important; border-color: #8b5cf6 !important; box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.1) !important; } .translate-button { width: 100% !important; padding: 8px !important; background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-size: 13px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.2s !important; -webkit-tap-highlight-color: transparent !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; } .translate-button:hover { transform: translateY(-1px) !important; box-shadow: 0 4px 10px rgba(139, 92, 246, 0.2) !important; } .translate-button:active { transform: scale(0.98) !important; } .translate-button-group { display: flex !important; gap: 10px !important; margin-bottom: 16px !important; } .translate-button-group .translate-button { flex: 1 !important; } .translate-section-title { font-size: 15px !important; font-weight: 600 !important; ${darkMode ? `color: #e5e7eb !important;` : `color: #1f2937 !important;`} margin: 20px 0 12px !important; padding-bottom: 6px !important; border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important; letter-spacing: -0.2px !important; } .translate-section-title:first-of-type { margin-top: 0 !important; } .translate-info { background: rgba(0, 0, 0, 0.03) !important; padding: 12px !important; border-radius: 8px !important; font-size: 11px !important; ${darkMode ? `color: #9ca3af !important;` : `color: #6b7280 !important;`} margin-top: 12px !important; line-height: 1.4 !important; } details { margin-top: 10px !important; } summary { cursor: pointer !important; font-size: 13px !important; font-weight: 500 !important; color: #8b5cf6 !important; padding: 6px 0 !important; user-select: none !important; } summary:hover { color: #6366f1 !important; } .advanced-content { margin-top: 12px !important; } .resize-handle { position: absolute !important; bottom: 0 !important; right: 0 !important; width: 16px !important; height: 16px !important; cursor: nw-resize !important; background: rgba(0,0,0,0.1) !important; border-radius: 0 0 12px 0 !important; z-index: 10 !important; } .resize-handle:hover { background: rgba(139, 92, 246, 0.3) !important; } .size-slider-container { margin-top: 8px !important; display: flex !important; align-items: center !important; gap: 10px !important; } .size-slider-container .translate-slider { flex: 1 !important; } /* 微软API双语模式样式 - 无删除线,原文颜色较暗 */ .microsoft-translated.dual-mode { display: inline-flex; flex-direction: column; line-height: 1.4; } .microsoft-translated.dual-mode .original-text { font-size: 0.85em; color: #888 !important; /* 无删除线 */ } .microsoft-translated.dual-mode .translated-text { font-weight: 500; } /* 白名单列表样式 */ .whitelist-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 8px; margin: 2px 0; background: ${darkMode ? 'rgba(50,50,55,0.6)' : '#f9f9f9'}; border-radius: 6px; font-size: 13px; } .whitelist-item:hover { background: ${darkMode ? 'rgba(70,70,75,0.6)' : '#f0f0f0'}; } .whitelist-domain { word-break: break-all; flex: 1; } .whitelist-delete { background: #ef4444; color: white; border: none; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; margin-left: 8px; transition: background 0.2s; } .whitelist-delete:hover { background: #dc2626; } .whitelist-collapsible { max-height: 200px; overflow-y: auto; transition: max-height 0.3s ease; } .whitelist-collapsible.collapsed { max-height: 0; overflow: hidden; } .whitelist-toggle { display: flex; justify-content: center; margin-top: 8px; margin-bottom: 8px; } .toggle-btn { background: transparent; border: none; color: #8b5cf6; cursor: pointer; font-size: 12px; padding: 2px 8px; border-radius: 12px; } .toggle-btn:hover { background: rgba(139,92,246,0.1); } `); } createPanel() { const panel = document.createElement('div'); panel.id = 'translate-panel'; const languages = this.getSupportedLanguages(); const services = [ { value: 'client.edge', name: '微软 Edge' }, { value: 'microsoft', name: '微软 API' }, { value: 'giteeai', name: 'gitee AI' }, { value: 'siliconflow', name: 'SiliconFlow AI' }, { value: 'translate.service', name: '自定义服务' } ]; const config = this.configManager.config; const currentDomain = getCurrentDomain(); const inWhitelist = this.configManager.isInWhitelist(currentDomain); const siteService = this.configManager.getSiteService(currentDomain); const currentEffectiveService = this.configManager.getEffectiveService(currentDomain); const mobile = isMobile(); const whitelist = this.configManager.get('whitelist'); panel.innerHTML = `
自动翻译助手
基础设置
当前生效: ${services.find(s => s.value === currentEffectiveService)?.name || currentEffectiveService}
白名单设置
当前白名单网站数: ${whitelist.length}
微软 API 专用设置
${this.getColorPresets().map(preset => `
`).join('')}
点击色块或输入颜色代码
💡 提示:以上设置仅在使用“微软 API”引擎时生效。
⚙️ 高级设置
${!mobile ? `
` : ''}
${mobile ? `
📐 窗口大小调整
${this.panel ? this.panel.offsetWidth : config.panelWidth}px
${this.panel ? this.panel.offsetHeight : config.panelHeight}px
` : ''}
💡 提示:拖动标题栏可移动窗口,右下角可调整大小(PC端)
📌 白名单网站始终翻译,不受全局开关影响
🔵 微软 API 引擎支持单显/双显及文字颜色自定义
`; document.body.appendChild(panel); this.panel = panel; this.panel.style.width = `${config.panelWidth}px`; this.panel.style.height = `${config.panelHeight}px`; if (config.panelPosition?.x !== undefined) { this.panel.style.left = `${config.panelPosition.x}px`; this.panel.style.top = `${config.panelPosition.y}px`; } else { this.positionPanel(); } this.panel.style.opacity = config.panelOpacity; if (mobile) { const widthSlider = document.getElementById('mobile-width-slider'); const heightSlider = document.getElementById('mobile-height-slider'); const widthVal = document.getElementById('mobile-width-value'); const heightVal = document.getElementById('mobile-height-value'); if (widthSlider) { widthSlider.addEventListener('input', (e) => { const w = parseInt(e.target.value); this.panel.style.width = `${w}px`; widthVal.textContent = `${w}px`; this.configManager.set('panelWidth', w); this.ensureInViewport(); }); } if (heightSlider) { heightSlider.addEventListener('input', (e) => { const h = parseInt(e.target.value); this.panel.style.height = `${h}px`; heightVal.textContent = `${h}px`; this.configManager.set('panelHeight', h); this.ensureInViewport(); }); } } // 初始化白名单列表 this.refreshWhitelistDisplay(); } getSupportedLanguages() { return [ { value: 'chinese_simplified', name: '简体中文' }, { value: 'chinese_traditional', name: '繁體中文' }, { value: 'english', name: 'English' }, { value: 'spanish', name: 'Español' }, { value: 'french', name: 'Français' }, { value: 'german', name: 'Deutsch' }, { value: 'japanese', name: '日本語' }, { value: 'korean', name: '한국어' }, { value: 'russian', name: 'Русский' }, { value: 'portuguese', name: 'Português' }, { value: 'italian', name: 'Italiano' }, { value: 'dutch', name: 'Nederlands' }, { value: 'polish', name: 'Polski' }, { value: 'turkish', name: 'Türkçe' }, { value: 'vietnamese', name: 'Tiếng Việt' }, { value: 'thai', name: 'ไทย' }, { value: 'indonesian', name: 'Bahasa Indonesia' }, { value: 'arabic', name: 'العربية' }, { value: 'hindi', name: 'हिन्दी' } ]; } getColorPresets() { return [ { name: '默认蓝', value: '#0066cc', bg: 'rgba(0,102,204,0.1)' }, { name: '绿色', value: '#00b894', bg: 'rgba(0,184,148,0.1)' }, { name: '红色', value: '#ff4757', bg: 'rgba(255,71,87,0.1)' }, { name: '紫色', value: '#6c5ce7', bg: 'rgba(108,92,231,0.1)' }, { name: '橙色', value: '#ff9f43', bg: 'rgba(255,159,67,0.1)' }, { name: '深灰', value: '#2d3436', bg: 'rgba(45,52,54,0.1)' } ]; } refreshWhitelistDisplay() { const container = document.getElementById('whitelist-container'); if (!container) return; const whitelist = this.configManager.get('whitelist'); const count = whitelist.length; const countSpan = document.getElementById('whitelist-count'); if (countSpan) countSpan.textContent = `当前白名单网站数: ${count}`; if (count === 0) { container.innerHTML = '
暂无白名单网站
'; return; } // 构建列表项 const itemsHtml = whitelist.map(domain => `
${this.escapeHtml(domain)}
`).join(''); const showCollapse = count > 3; const collapsedClass = showCollapse ? 'collapsed' : ''; container.innerHTML = `
${itemsHtml}
${showCollapse ? `
` : ''} `; // 绑定删除按钮事件 const deleteBtns = container.querySelectorAll('.whitelist-delete'); deleteBtns.forEach(btn => { btn.addEventListener('click', (e) => { const domain = btn.getAttribute('data-domain'); if (domain && confirm(`确定从白名单中删除 "${domain}" 吗?`)) { this.configManager.removeFromWhitelist(domain); this.refreshWhitelistDisplay(); this.translateManager.refresh(); window.updateMenuItems?.(); this.toast.show(`已删除 ${domain}`, 'success', 1500); } }); }); // 折叠/展开按钮 const toggleBtn = document.getElementById('whitelist-toggle-btn'); if (toggleBtn) { const listDiv = document.getElementById('whitelist-list'); toggleBtn.addEventListener('click', () => { const isCollapsed = listDiv.classList.contains('collapsed'); if (isCollapsed) { listDiv.classList.remove('collapsed'); toggleBtn.textContent = `收起 (${count})`; } else { listDiv.classList.add('collapsed'); toggleBtn.textContent = `展开全部 (${count})`; } }); } } escapeHtml(str) { return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } bindEvents() { // 关闭面板 document.getElementById('translate-panel-close')?.addEventListener('click', () => this.togglePanel()); // 源语言 document.getElementById('translate-local-lang')?.addEventListener('change', (e) => { const val = e.target.value; this.configManager.set('localLanguage', val); const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service !== 'microsoft' && typeof translate !== 'undefined') { translate.language.setLocal(val); } this.translateManager.refresh(); this.toast.show('✅ 源语言已更新'); }); // 目标语言 document.getElementById('translate-target-lang')?.addEventListener('change', async (e) => { this.configManager.set('targetLanguage', e.target.value); await this.translateManager.onLanguageChange(); this.toast.show('✅ 目标语言已更新'); }); // 清空白名单 document.getElementById('translate-clear-whitelist')?.addEventListener('click', () => { if (confirm('确定清空所有白名单网站?')) { this.configManager.clearWhitelist(); this.toast.show('✅ 白名单已清空'); this.refreshWhitelistDisplay(); this.translateManager.refresh(); window.updateMenuItems?.(); } }); // 保存专用引擎 document.getElementById('save-site-service')?.addEventListener('click', () => { const service = document.getElementById('site-specific-service').value; const domain = getCurrentDomain(); this.configManager.setSiteService(domain, service || null); this.toast.show(`✅ 已为 ${domain} ${service ? '设置专用引擎' : '清除专用引擎'}`); const effective = this.configManager.getEffectiveService(domain); const desc = document.querySelector('#site-specific-service')?.closest('.translate-control-group')?.querySelector('.translate-description:last-child'); if (desc) { const map = { 'client.edge':'微软 Edge', 'microsoft':'微软 API', 'giteeai':'gitee AI', 'siliconflow':'SiliconFlow AI', 'translate.service':'自定义服务' }; desc.textContent = `当前生效: ${map[effective] || effective}`; } this.translateManager.refresh(); }); // 全局翻译引擎 - 立即刷新翻译 document.getElementById('translate-service')?.addEventListener('change', async (e) => { const newService = e.target.value; const oldService = this.configManager.get('translateService'); // 如果引擎没有改变,则不处理 if (newService === oldService) return; // 先禁用当前翻译,恢复原文 await this.translateManager.disableCurrentTranslation(); // 更新配置 this.configManager.set('translateService', newService); const domain = getCurrentDomain(); const effective = this.configManager.getEffectiveService(domain); // 重置翻译管理器状态,强制重新初始化 this.translateManager.initialized = false; this.translateManager.listenerStarted = false; if (this.translateManager.microsoftTranslator) { // 确保微软翻译器实例已清除(如果之前存在) this.translateManager.microsoftTranslator = null; } // 立即开始新翻译 this.translateManager.refresh(); this.toast.show(`✅ 已切换至 ${e.target.options[e.target.selectedIndex].text} 引擎,正在翻译...`, 'success', 2000); // 更新专用引擎描述 if (!this.configManager.getSiteService(domain)) { const desc = document.querySelector('#site-specific-service')?.closest('.translate-control-group')?.querySelector('.translate-description:last-child'); if (desc) { const map = { 'client.edge':'微软 Edge', 'microsoft':'微软 API', 'giteeai':'gitee AI', 'siliconflow':'SiliconFlow AI', 'translate.service':'自定义服务' }; desc.textContent = `当前生效: ${map[effective] || effective}`; } } }); // 忽略Class document.getElementById('save-ignore-class')?.addEventListener('click', async () => { this.configManager.set('ignoredClasses', document.getElementById('ignore-class-input').value.trim()); await this.translateManager.onIgnoreChange(); this.toast.show('✅ 忽略Class已保存'); }); document.getElementById('save-ignore-id')?.addEventListener('click', async () => { this.configManager.set('ignoredIds', document.getElementById('ignore-id-input').value.trim()); await this.translateManager.onIgnoreChange(); this.toast.show('✅ 忽略ID已保存'); }); document.getElementById('save-custom-terms')?.addEventListener('click', () => { this.configManager.set('customTerms', document.getElementById('custom-terms-input').value.trim()); this.translateManager.applyCustomTerms(); this.toast.show('✅ 自定义术语已保存'); }); // 划词翻译 const selectionToggle = document.getElementById('translate-selection'); if (selectionToggle) { selectionToggle.addEventListener('change', (e) => { this.configManager.set('enableSelectionTranslate', e.target.checked); e.target.checked ? this.translateManager.startSelectionTranslate() : this.translateManager.stopSelectionTranslate(); this.toast.show(e.target.checked ? '✅ 划词翻译已启用' : '❌ 划词翻译已禁用'); }); } // 动态监听 document.getElementById('translate-listener')?.addEventListener('change', (e) => { this.configManager.set('enableListener', e.target.checked); const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service !== 'microsoft') { if (e.target.checked && !this.translateManager.listenerStarted) this.translateManager.startListener(); this.toast.show(e.target.checked ? '✅ 动态监听已启用' : '❌ 动态监听已禁用'); } else { this.toast.show('微软 API 引擎自带动态监听,无需额外设置'); } }); // 缓存 document.getElementById('translate-cache')?.addEventListener('change', (e) => { this.configManager.set('enableCache', e.target.checked); if (typeof translate !== 'undefined' && translate.storage) { e.target.checked ? translate.storage.enable() : translate.storage.disable(); } this.toast.show(e.target.checked ? '✅ 缓存已启用' : '❌ 缓存已禁用'); }); document.getElementById('translate-clear-cache')?.addEventListener('click', () => { const cleared = this.configManager.clearCache(); this.toast.show(`✅ 已清除 ${cleared} 个缓存项`); }); document.getElementById('translate-reset-all')?.addEventListener('click', () => { if (confirm('确定重置所有设置?此操作不可撤销。')) { this.configManager.reset(); this.toast.show('✅ 已重置,页面将刷新', 'success', 1500); setTimeout(() => location.reload(), 1500); } }); // 微软API专用设置事件 const modeReplace = document.getElementById('ms-mode-replace'); const modeDual = document.getElementById('ms-mode-dual'); if (modeReplace) { modeReplace.addEventListener('click', async () => { if (this.configManager.get('microsoftMode') !== 'replace') { this.configManager.set('microsoftMode', 'replace'); await this.translateManager.onMicrosoftModeColorChange(); this.toast.show('✅ 微软API已切换为单显模式'); modeReplace.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)'; modeReplace.style.color = 'white'; modeDual.style.background = '#e5e7eb'; modeDual.style.color = '#333'; } }); } if (modeDual) { modeDual.addEventListener('click', async () => { if (this.configManager.get('microsoftMode') !== 'dual') { this.configManager.set('microsoftMode', 'dual'); await this.translateManager.onMicrosoftModeColorChange(); this.toast.show('✅ 微软API已切换为双显模式'); modeDual.style.background = 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)'; modeDual.style.color = 'white'; modeReplace.style.background = '#e5e7eb'; modeReplace.style.color = '#333'; } }); } // 颜色预设点击 const colorPresets = document.querySelectorAll('.color-preset'); colorPresets.forEach(preset => { preset.addEventListener('click', async () => { const color = preset.dataset.color; const bg = preset.dataset.bg; this.configManager.set('microsoftTextColor', color); this.configManager.set('microsoftBgColor', bg); await this.translateManager.onMicrosoftModeColorChange(); this.toast.show(`✅ 译文颜色已更新`); // 更新预设边框样式 colorPresets.forEach(p => p.style.border = '2px solid transparent'); preset.style.border = `2px solid white`; // 同步输入框值 const colorText = document.getElementById('ms-color-text'); if (colorText) colorText.value = color; const colorPicker = document.getElementById('ms-color-custom'); if (colorPicker) colorPicker.value = color; }); }); const colorCustom = document.getElementById('ms-color-custom'); const colorText = document.getElementById('ms-color-text'); const colorApply = document.getElementById('ms-color-apply'); if (colorCustom) { colorCustom.addEventListener('change', async (e) => { const color = e.target.value; if (colorText) colorText.value = color; this.configManager.set('microsoftTextColor', color); this.configManager.set('microsoftBgColor', color.replace('#', 'rgba(') + ',0.1)'); await this.translateManager.onMicrosoftModeColorChange(); this.toast.show(`✅ 自定义颜色已应用`); // 更新预设高亮 const presets = document.querySelectorAll('.color-preset'); presets.forEach(p => p.style.border = '2px solid transparent'); }); } if (colorApply) { colorApply.addEventListener('click', async () => { let color = colorText.value.trim(); if (!/^#[0-9A-Fa-f]{6}$/.test(color) && !/^#[0-9A-Fa-f]{3}$/.test(color)) { this.toast.show('颜色格式错误,请使用#RRGGBB格式', 'error'); return; } if (color.length === 4) { color = '#' + color[1]+color[1]+color[2]+color[2]+color[3]+color[3]; } this.configManager.set('microsoftTextColor', color); this.configManager.set('microsoftBgColor', color.replace('#', 'rgba(') + ',0.1)'); await this.translateManager.onMicrosoftModeColorChange(); this.toast.show(`✅ 颜色已应用`); const colorPicker = document.getElementById('ms-color-custom'); if (colorPicker) colorPicker.value = color; const presets = document.querySelectorAll('.color-preset'); presets.forEach(p => p.style.border = '2px solid transparent'); }); } window.addEventListener('resize', () => this.ensureInViewport()); window.addEventListener('orientationchange', () => setTimeout(() => this.ensureInViewport(), 300)); this.bindPanelDragEvents(); this.bindResizeEvents(); } bindPanelDragEvents() { const header = this.panel.querySelector('.translate-panel-header'); let startX, startY, startLeft, startTop; const onMouseMove = (e) => { if (!this.isDragging) return; let newLeft = startLeft + (e.clientX - startX); let newTop = startTop + (e.clientY - startY); newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - this.panel.offsetWidth)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - this.panel.offsetHeight)); this.panel.style.left = `${newLeft}px`; this.panel.style.top = `${newTop}px`; }; const onMouseUp = () => { if (this.isDragging) { this.isDragging = false; this.panel.classList.remove('dragging'); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); this.configManager.set('panelPosition', { x: parseInt(this.panel.style.left), y: parseInt(this.panel.style.top) }); } }; header.addEventListener('touchstart', (e) => { if (e.target.id === 'translate-panel-close') return; const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; startLeft = this.panel.offsetLeft; startTop = this.panel.offsetTop; this.isDragging = true; this.panel.classList.add('dragging'); e.preventDefault(); }); header.addEventListener('touchmove', (e) => { if (!this.isDragging) return; const touch = e.touches[0]; let newLeft = startLeft + (touch.clientX - startX); let newTop = startTop + (touch.clientY - startY); newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - this.panel.offsetWidth)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - this.panel.offsetHeight)); this.panel.style.left = `${newLeft}px`; this.panel.style.top = `${newTop}px`; e.preventDefault(); }); header.addEventListener('touchend', () => { if (this.isDragging) { this.isDragging = false; this.panel.classList.remove('dragging'); this.configManager.set('panelPosition', { x: parseInt(this.panel.style.left), y: parseInt(this.panel.style.top) }); } }); header.addEventListener('mousedown', (e) => { if (e.target.id === 'translate-panel-close') return; this.isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = this.panel.offsetLeft; startTop = this.panel.offsetTop; this.panel.classList.add('dragging'); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); e.preventDefault(); }); } bindResizeEvents() { const handle = document.getElementById('resize-handle'); if (!handle || isMobile()) return; let startX, startY, startWidth, startHeight; const onMouseMove = (e) => { if (!this.isResizing) return; let newWidth = startWidth + (e.clientX - startX); let newHeight = startHeight + (e.clientY - startY); newWidth = Math.max(280, Math.min(newWidth, window.innerWidth - this.panel.offsetLeft - 10)); newHeight = Math.max(300, Math.min(newHeight, window.innerHeight - this.panel.offsetTop - 10)); this.panel.style.width = `${newWidth}px`; this.panel.style.height = `${newHeight}px`; }; const onMouseUp = () => { if (this.isResizing) { this.isResizing = false; this.panel.classList.remove('resizing'); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); this.configManager.set('panelWidth', parseInt(this.panel.style.width)); this.configManager.set('panelHeight', parseInt(this.panel.style.height)); } }; handle.addEventListener('mousedown', (e) => { this.isResizing = true; startX = e.clientX; startY = e.clientY; startWidth = this.panel.offsetWidth; startHeight = this.panel.offsetHeight; this.panel.classList.add('resizing'); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); e.preventDefault(); e.stopPropagation(); }); } togglePanel() { if (this.panel.classList.contains('show')) { this.panel.classList.remove('show'); } else { this.panel.classList.add('show'); this.ensureInViewport(); this.refreshWhitelistDisplay(); } } positionPanel() { const w = this.panel.offsetWidth, h = this.panel.offsetHeight; let left = (window.innerWidth - w) / 2; let top = (window.innerHeight - h) / 2; left = Math.max(10, Math.min(left, window.innerWidth - w - 10)); top = Math.max(10, Math.min(top, window.innerHeight - h - 10)); this.panel.style.left = `${left}px`; this.panel.style.top = `${top}px`; this.configManager.set('panelPosition', { x: left, y: top }); } ensureInViewport() { if (!this.panel.classList.contains('show')) return; let left = this.panel.offsetLeft, top = this.panel.offsetTop; const w = this.panel.offsetWidth, h = this.panel.offsetHeight; const maxX = window.innerWidth - w, maxY = window.innerHeight - h; let changed = false; if (left < 0) { left = 0; changed = true; } if (top < 0) { top = 0; changed = true; } if (left > maxX) { left = maxX; changed = true; } if (top > maxY) { top = maxY; changed = true; } if (changed) { this.panel.style.left = `${left}px`; this.panel.style.top = `${top}px`; this.configManager.set('panelPosition', { x: left, y: top }); } } } // ==================== 菜单管理 ==================== let menuManager = null; function setupMenu(configManager, translateManager, uiManager) { function updateMenuItems() { if (menuManager) { if (menuManager.toggleEnabledId) GM_unregisterMenuCommand(menuManager.toggleEnabledId); if (menuManager.toggleWhitelistId) GM_unregisterMenuCommand(menuManager.toggleWhitelistId); if (menuManager.openSettingsId) GM_unregisterMenuCommand(menuManager.openSettingsId); } const enabled = configManager.get('enabled'); const domain = getCurrentDomain(); const inWhitelist = configManager.isInWhitelist(domain); const toggleEnabledText = enabled ? '❌ 关闭自动翻译' : '✅ 开启自动翻译'; const toggleWhitelistText = inWhitelist ? '⭐ 从白名单中移除' : '☆ 添加到白名单'; const toggleEnabledId = GM_registerMenuCommand(toggleEnabledText, () => { const newState = !configManager.get('enabled'); configManager.set('enabled', newState); configManager.set('autoTranslate', newState); translateManager.refresh(); uiManager.toast.show(newState ? '✅ 全局翻译已开启' : '❌ 全局翻译已关闭'); updateMenuItems(); }); const toggleWhitelistId = GM_registerMenuCommand(toggleWhitelistText, () => { const nowInWhitelist = configManager.toggleWhitelistDomain(domain); uiManager.toast.show(nowInWhitelist ? `✅ 已添加 ${domain} 到白名单` : `❌ 已从白名单移除 ${domain}`); uiManager.refreshWhitelistDisplay(); translateManager.refresh(); updateMenuItems(); }); const openSettingsId = GM_registerMenuCommand('⚙️ 翻译设置', () => { uiManager.togglePanel(); }); menuManager = { toggleEnabledId, toggleWhitelistId, openSettingsId }; } updateMenuItems(); window.updateMenuItems = updateMenuItems; } async function init() { if (document.readyState === 'loading') await new Promise(r => document.addEventListener('DOMContentLoaded', r)); try { if (typeof translate === 'undefined') { console.error('translate.js未加载,请检查@require配置'); } const configManager = new ConfigManager(); const translateManager = new TranslateManager(configManager); const uiManager = new UIManager(configManager, translateManager); setupMenu(configManager, translateManager, uiManager); window.translateHelper = { config: configManager, translate: translateManager, ui: uiManager }; console.log('自动翻译助手已加载完成'); } catch (error) { console.error('翻译脚本初始化失败:', error); } } setTimeout(init, 100); })();