// ==UserScript== // @name 自动翻译助手 // @name:zh-CN 自动翻译助手 // @name:zh-TW 自動翻譯助手 // @name:zh-HK 自動翻譯助手 // @name:en Auto Translation Assistant // @namespace https://github.com/eraycc // @version 2.7.1 // @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 // @connect transmart.qq.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 gmFetch(opts) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 15000, ...opts, onload: resolve, onerror: reject, ontimeout: reject }); }); } function delay(ms) { return new Promise(r => setTimeout(r, ms)); } // ==================== 语言映射 ==================== 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; } function mapToTencentLang(langCode) { const map = { 'chinese_simplified': 'zh', 'chinese_traditional': 'zh-TW', 'english': 'en', 'japanese': 'ja', 'korean': 'ko', 'french': 'fr', 'german': 'de', 'spanish': 'es', '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 TencentTranslator { constructor(configManager) { this.configManager = configManager; this.clientKey = 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)'; } getClientKey() { if (this.clientKey) return this.clientKey; this.clientKey = 'browser-chrome-120.0-Windows_10-' + crypto.randomUUID() + '-' + Date.now(); return this.clientKey; } langCode(l) { const m = { 'zh': 'zh', 'zh-CN': 'zh', 'zh-TW': 'zh-TW' }; return m[l] || l; } async translateBatch(texts) { if (!texts.length) return []; let toLang = this.configManager.get('targetLanguage'); toLang = mapToTencentLang(toLang); toLang = this.langCode(toLang); const requestBody = { header: { fn: 'auto_translation', session: '', client_key: this.getClientKey(), user: '' }, type: 'plain', model_category: 'normal', text_domain: 'general', source: { lang: 'auto', text_list: texts }, target: { lang: toLang } }; try { const r = await gmFetch({ method: 'POST', url: 'https://transmart.qq.com/api/imt', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(requestBody) }); if (r.status !== 200) throw new Error(`腾讯API错误: ${r.status}`); const data = JSON.parse(r.responseText); if (data && data.auto_translation && Array.isArray(data.auto_translation)) { return data.auto_translation; } throw new Error('腾讯响应格式异常'); } catch (err) { console.error('腾讯批量翻译失败:', err); return texts.map(() => null); } } collectTextNodes() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; if (parent.closest('.microsoft-translated, [data-no-translate], script, style, noscript, code, pre')) 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 (!/[a-zA-Z\u4e00-\u9fff\u0400-\u04FF]/.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) { if (!translation || translation === nodeInfo.text) return; 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 { 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; this.pageTranslating = true; console.log('腾讯翻译开始...'); let textNodes = this.collectTextNodes(); if (textNodes.length === 0) { this.pageTranslating = false; return; } const BATCH_SIZE = 100; const CONCURRENT_BATCHES = 3; let batches = []; for (let i = 0; i < textNodes.length; i += BATCH_SIZE) { batches.push(textNodes.slice(i, i + BATCH_SIZE)); } let translatedCount = 0; for (let i = 0; i < batches.length; i += CONCURRENT_BATCHES) { const currentBatches = batches.slice(i, i + CONCURRENT_BATCHES); await Promise.all(currentBatches.map(async (batch) => { const textsToTranslate = []; const nodeIndices = []; batch.forEach((nodeInfo, idx) => { const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; if (this.translationCache.has(cacheKey)) { this.applyTranslation(nodeInfo, this.translationCache.get(cacheKey)); translatedCount++; } else { textsToTranslate.push(nodeInfo.text); nodeIndices.push(idx); } }); if (textsToTranslate.length) { try { const translations = await this.translateBatch(textsToTranslate); translations.forEach((trans, ti) => { if (trans && trans !== textsToTranslate[ti]) { const nodeInfo = batch[nodeIndices[ti]]; this.applyTranslation(nodeInfo, trans); const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; this.translationCache.set(cacheKey, trans); translatedCount++; } }); } catch (err) { console.error('腾讯批次失败:', err); } } })); await delay(10); } this.pageTranslating = false; this.lastTranslationTime = Date.now(); console.log(`腾讯翻译完成,翻译 ${translatedCount} 项`); } async checkAndTranslate() { if (this.pageTranslating) return; const untranslated = this.collectTextNodes(); if (untranslated.length > 0) { 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(), 500); }); } try { this.domObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); this.isObserving = true; } catch (e) {} } stopDOMObservation() { if (this.domObserver) { this.domObserver.disconnect(); this.isObserving = false; } if (this.domChangeTimer) clearTimeout(this.domChangeTimer); } startContinuousCheck() { if (this.continuousTimer) clearInterval(this.continuousTimer); this.continuousTimer = setInterval(() => { if (!this.pageTranslating && this.active) { this.checkAndTranslate(); } }, 20000); } stopContinuousCheck() { if (this.continuousTimer) clearInterval(this.continuousTimer); } async enable() { this.active = true; 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; } async onLanguageChange() { if (!this.active) return; this.translationCache.clear(); await this.translateWholePage(); } async onIgnoreChange() { if (!this.active) return; this.disable(); await this.enable(); } 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; this.disable(); await this.enable(); } } // ==================== 微软 API 引擎(同样优化速度)==================== 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 getAuthToken() { if (this.authToken && Date.now() - this.tokenTime < 480000) return this.authToken; const r = await gmFetch({ method: 'GET', url: 'https://edge.microsoft.com/translate/auth' }); if (r.status !== 200) throw new Error('MS auth'); this.authToken = r.responseText; this.tokenTime = Date.now(); return this.authToken; } langCode(l) { const m = { 'zh': 'zh-Hans', 'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant', 'no': 'nb', 'sr': 'sr-Cyrl', 'pt-PT': 'pt-pt', 'fr-CA': 'fr-ca' }; return m[l] || l; } async translateBatch(texts) { if (!texts.length) return []; const token = await this.getAuthToken(); let toLang = this.configManager.get('targetLanguage'); toLang = mapToMicrosoftLang(toLang); toLang = this.langCode(toLang); const r = await gmFetch({ method: 'POST', url: `https://api-edge.cognitive.microsofttranslator.com/translate?to=${toLang}&api-version=3.0`, headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, data: JSON.stringify(texts.map(t => ({ Text: t }))) }); if (r.status !== 200) throw new Error('MS batch error'); const data = JSON.parse(r.responseText); return data.map(item => item.translations[0].text); } collectTextNodes() { const textNodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; if (parent.closest('.microsoft-translated, [data-no-translate], script, style, noscript, code, pre')) 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 (!/[a-zA-Z\u4e00-\u9fff\u0400-\u04FF]/.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, text, parent: node.parentElement, originalText: node.textContent }); } return textNodes; } applyTranslation(nodeInfo, translation) { if (!translation || translation === nodeInfo.text) return; 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 { 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; this.pageTranslating = true; let textNodes = this.collectTextNodes(); if (textNodes.length === 0) { this.pageTranslating = false; return; } const BATCH_SIZE = 100; const CONCURRENT_BATCHES = 3; let batches = []; for (let i = 0; i < textNodes.length; i += BATCH_SIZE) { batches.push(textNodes.slice(i, i + BATCH_SIZE)); } let translatedCount = 0; for (let i = 0; i < batches.length; i += CONCURRENT_BATCHES) { const currentBatches = batches.slice(i, i + CONCURRENT_BATCHES); await Promise.all(currentBatches.map(async (batch) => { const textsToTranslate = []; const nodeIndices = []; batch.forEach((nodeInfo, idx) => { const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; if (this.translationCache.has(cacheKey)) { this.applyTranslation(nodeInfo, this.translationCache.get(cacheKey)); translatedCount++; } else { textsToTranslate.push(nodeInfo.text); nodeIndices.push(idx); } }); if (textsToTranslate.length) { try { const translations = await this.translateBatch(textsToTranslate); translations.forEach((trans, ti) => { if (trans && trans !== textsToTranslate[ti]) { const nodeInfo = batch[nodeIndices[ti]]; this.applyTranslation(nodeInfo, trans); const cacheKey = `${nodeInfo.text}|${this.configManager.get('targetLanguage')}|${this.mode}`; this.translationCache.set(cacheKey, trans); translatedCount++; } }); } catch (err) { console.error('微软批次失败:', err); } } })); await delay(10); } this.pageTranslating = false; this.lastTranslationTime = Date.now(); console.log(`微软翻译完成,翻译 ${translatedCount} 项`); } async checkAndTranslate() { if (this.pageTranslating) return; const untranslated = this.collectTextNodes(); if (untranslated.length > 0) 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(), 500); }); } try { this.domObserver.observe(document.body, { childList: true, subtree: true, characterData: true }); this.isObserving = true; } catch (e) {} } stopDOMObservation() { if (this.domObserver) this.domObserver.disconnect(); this.isObserving = false; if (this.domChangeTimer) clearTimeout(this.domChangeTimer); } startContinuousCheck() { if (this.continuousTimer) clearInterval(this.continuousTimer); this.continuousTimer = setInterval(() => { if (!this.pageTranslating && this.active && this.authToken) this.checkAndTranslate(); }, 20000); } stopContinuousCheck() { if (this.continuousTimer) clearInterval(this.continuousTimer); } async enable() { try { await this.getAuthToken(); this.active = true; await this.translateWholePage(); this.startDOMObservation(); this.startContinuousCheck(); return true; } catch (err) { console.error('微软引擎启用失败', err); return false; } } 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; } async onLanguageChange() { if (!this.active) return; this.translationCache.clear(); await this.translateWholePage(); } async onIgnoreChange() { if (!this.active) return; this.disable(); await this.enable(); } 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; this.disable(); await this.enable(); } } // ==================== 配置管理器 ==================== 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) {} let cleared = 0; Object.keys(localStorage).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 idx = this.config.whitelist.indexOf(domain); if (idx !== -1) { this.config.whitelist.splice(idx,1); this.saveConfig(); return true; } return false; } clearWhitelist() { this.config.whitelist = []; this.saveConfig(); } toggleWhitelistDomain(domain) { return this.isInWhitelist(domain) ? this.removeFromWhitelist(domain) : this.addToWhitelist(domain); } shouldTranslate() { return this.isInWhitelist(getCurrentDomain()) || 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) { return this.getSiteService(domain) || 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; this.tencentTranslator = 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; return; } if (service === 'tencent') { if (!this.tencentTranslator) this.tencentTranslator = new TencentTranslator(this.configManager); this.initialized = true; 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(u=>u.trim()).filter(u=>u); if (urls.length) translate.request.api.host = urls.length===1?urls[0]:urls; } } translate.selectLanguageTag.show = false; this.applyIgnoreSettings(); this.applyCustomTerms(); const attrs = this.configManager.get('translateAttributes'); if (attrs.length) translate.translateAttributes = attrs; translate.showOrigin = false; if (this.configManager.get('enableCache')) translate.storage.enable(); else translate.storage.disable(); this.initialized = true; } catch(e) { console.error(e); } } 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) translate.nomenclature.append(this.configManager.get('localLanguage'), this.configManager.get('targetLanguage'), customTerms); } startListener() { if (!this.listenerStarted && typeof translate !== 'undefined' && this.configManager.get('enableListener')) { try { translate.listener.start(); this.listenerStarted = true; } catch(e) {} } } 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 (service === 'tencent') { if (!this.tencentTranslator) this.tencentTranslator = new TencentTranslator(this.configManager); if (force || this.currentLanguage !== targetLang) { this.currentLanguage = targetLang; await this.tencentTranslator.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() { this.changeLanguage(this.configManager.get('targetLanguage'), true); } refresh() { this.configManager.shouldTranslate() ? this.forceTranslate() : this.changeLanguage(null, true); } async disableCurrentTranslation() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft' && this.microsoftTranslator?.active) this.microsoftTranslator.disable(); else if (service === 'tencent' && this.tencentTranslator?.active) this.tencentTranslator.disable(); else if (typeof translate !== 'undefined') translate.changeLanguage(null); } startAutoTranslate() { const service = this.configManager.getEffectiveService(getCurrentDomain()); const should = this.configManager.shouldTranslate(); if (service === 'microsoft') { if (!this.microsoftTranslator) this.microsoftTranslator = new MicrosoftTranslator(this.configManager); if (should) setTimeout(()=>this.microsoftTranslator.enable(), 100); return; } if (service === 'tencent') { if (!this.tencentTranslator) this.tencentTranslator = new TencentTranslator(this.configManager); if (should) setTimeout(()=>this.tencentTranslator.enable(), 100); return; } if (!this.initialized) this.init(); this.startListener(); 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(e) {} } stopSelectionTranslate() { if (typeof translate !== 'undefined' && translate.selectionTranslate) try { translate.selectionTranslate.stop(); } catch(e) {} } async onIgnoreChange() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft' && this.microsoftTranslator?.active) await this.microsoftTranslator.onIgnoreChange(); else if (service === 'tencent' && this.tencentTranslator?.active) await this.tencentTranslator.onIgnoreChange(); else this.refresh(); } async onLanguageChange() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft' && this.microsoftTranslator?.active) await this.microsoftTranslator.onLanguageChange(); else if (service === 'tencent' && this.tencentTranslator?.active) await this.tencentTranslator.onLanguageChange(); else this.refresh(); } async onMicrosoftModeColorChange() { const service = this.configManager.getEffectiveService(getCurrentDomain()); if (service === 'microsoft' && this.microsoftTranslator?.active) await this.microsoftTranslator.onModeColorChange(); else if (service === 'tencent' && this.tencentTranslator?.active) await this.tencentTranslator.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;top:20px;right:20px;z-index:2147483647;pointer-events:none;font-family:system-ui;'; document.body.appendChild(this.container); } show(msg, type='success', dur=2000) { const toast = document.createElement('div'); const bg = type==='success'?'#10b981':type==='error'?'#ef4444':'#3b82f6'; toast.style.cssText = `background:${bg};color:white;padding:12px 20px;border-radius:12px;margin-bottom:10px;box-shadow:0 10px 25px -5px rgba(0,0,0,0.1);animation:slideInRight 0.3s;pointer-events:auto;font-size:14px;font-weight:500;backdrop-filter:blur(8px);`; toast.textContent = msg; this.container.appendChild(toast); setTimeout(()=>{ toast.style.animation='slideOutRight 0.3s'; setTimeout(()=>{ if(this.container.contains(toast)) this.container.removeChild(toast); },300); },dur); } } // ==================== 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 dark = 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)}} #translate-panel{position:fixed;border-radius:12px;box-shadow:0 20px 40px -12px rgba(0,0,0,0.2);z-index:2147483646;display:none;font-family:system-ui;overflow:hidden;min-width:280px;min-height:300px;max-width:90vw;max-height:80vh;background:${dark?'rgba(30,30,35,0.95)':'rgba(255,255,255,0.95)'};backdrop-filter:blur(20px);border:1px solid ${dark?'rgba(255,255,255,0.1)':'rgba(255,255,255,0.3)'}} #translate-panel.show{display:flex;flex-direction:column;animation:fadeIn 0.2s} .translate-panel-header{padding:12px 16px;display:flex;justify-content:space-between;align-items:center;cursor:move;border-bottom:1px solid rgba(0,0,0,0.05);background:${dark?'rgba(0,0,0,0.3)':'linear-gradient(135deg,#667eea,#764ba2)'};flex-shrink:0} .translate-panel-title{font-size:16px;font-weight:600;color:white;display:flex;align-items:center;gap:6px} .translate-panel-close{width:28px;height:28px;border-radius:50%;background:rgba(0,0,0,0.1);border:none;cursor:pointer;color:white;font-size:16px} .translate-panel-body{padding:16px;overflow-y:auto;flex:1} .translate-control-group{margin-bottom:16px} .translate-control-label{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:13px;font-weight:500;color:${dark?'#e5e7eb':'#1f2937'}} .translate-select,.translate-input{width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;background:${dark?'rgba(50,50,55,0.8)':'rgba(255,255,255,0.8)'};color:${dark?'#e5e7eb':'#1f2937'}} .translate-button{width:100%;padding:8px;background:linear-gradient(135deg,#8b5cf6,#6366f1);color:white;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer} .translate-button-group{display:flex;gap:10px;margin-bottom:16px} .translate-button-group .translate-button{flex:1} .translate-section-title{font-size:15px;font-weight:600;margin:20px 0 12px;padding-bottom:6px;border-bottom:1px solid rgba(0,0,0,0.05);color:${dark?'#e5e7eb':'#1f2937'}} .translate-info{background:rgba(0,0,0,0.03);padding:12px;border-radius:8px;font-size:11px;margin-top:12px;color:${dark?'#9ca3af':'#6b7280'}} .resize-handle{position:absolute;bottom:0;right:0;width:16px;height:16px;cursor:nw-resize;background:rgba(0,0,0,0.1);border-radius:0 0 12px 0} .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} .microsoft-translated.dual-mode .translated-text{font-weight:500} /* 白名单容器:固定高度,滚动 */ #whitelist-container { max-height: 150px; overflow-y: auto; border: 1px solid ${dark?'rgba(255,255,255,0.1)':'#e5e7eb'}; border-radius: 8px; margin-bottom: 8px; } .whitelist-item{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid ${dark?'rgba(255,255,255,0.05)':'#f0f0f0'}} .whitelist-item:last-child{border-bottom:none} .whitelist-delete{background:#ef4444;color:white;border:none;border-radius:4px;padding:2px 8px;cursor:pointer;font-size:12px} .whitelist-delete:hover{background:#dc2626} `); } createPanel() { const panel = document.createElement('div'); panel.id = 'translate-panel'; const languages = [ {value:'chinese_simplified',name:'简体中文'},{value:'chinese_traditional',name:'繁體中文'}, {value:'english',name:'English'},{value:'japanese',name:'日本語'},{value:'korean',name:'한국어'}, {value:'french',name:'Français'},{value:'german',name:'Deutsch'},{value:'spanish',name:'Español'}, {value:'russian',name:'Русский'},{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:'arabic',name:'العربية'},{value:'hindi',name:'हिन्दी'} ]; const services = [ {value:'client.edge',name:'微软 Edge'},{value:'microsoft',name:'微软 API'}, {value:'tencent',name:'腾讯'},{value:'giteeai',name:'gitee AI'}, {value:'siliconflow',name:'SiliconFlow AI'},{value:'translate.service',name:'自定义服务'} ]; const config = this.configManager.config; const domain = getCurrentDomain(); const siteService = this.configManager.getSiteService(domain); const effective = this.configManager.getEffectiveService(domain); const whitelist = this.configManager.get('whitelist'); panel.innerHTML = `
✨ 自动翻译助手
基础设置
当前生效: ${services.find(s=>s.value===effective)?.name||effective}
白名单设置
当前白名单网站数: ${whitelist.length}
翻译显示设置
${this.getColorPresets().map(p=>`
`).join('')}
点击色块或输入颜色代码
💡 提示:以上设置对“微软 API”和“腾讯”引擎均生效。
⚙️ 高级设置
${!isMobile()?`
`:''}
💡 提示:拖动标题栏可移动窗口,右下角可调整大小(PC端)
📌 白名单网站始终翻译,不受全局开关影响
🔵 微软 API 和腾讯引擎均支持单显/双显及文字颜色自定义
`; document.body.appendChild(panel); this.panel = panel; this.panel.setAttribute('data-no-translate', 'true'); 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.refreshWhitelistDisplay(); } 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; let whitelist = [...this.configManager.get('whitelist')]; const currentDomain = getCurrentDomain(); // 如果当前域名在白名单中,将其移到第一位 const idx = whitelist.indexOf(currentDomain); if (idx !== -1 && idx !== 0) { whitelist.splice(idx, 1); whitelist.unshift(currentDomain); } 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(''); container.innerHTML = itemsHtml; // 绑定删除按钮事件 container.querySelectorAll('.whitelist-delete').forEach(btn => { btn.addEventListener('click', (e) => { const domain = btn.dataset.domain; if(confirm(`确定从白名单中删除 "${domain}" 吗?`)){ this.configManager.removeFromWhitelist(domain); this.refreshWhitelistDisplay(); this.translateManager.refresh(); window.updateMenuItems?.(); this.toast.show(`已删除 ${domain}`); } }); }); } escapeHtml(str){ return str.replace(/[&<>]/g, m=>({ '&':'&', '<':'<', '>':'>' }[m])); } bindEvents() { document.getElementById('translate-panel-close')?.addEventListener('click',()=>this.togglePanel()); document.getElementById('translate-local-lang')?.addEventListener('change',(e)=>{ this.configManager.set('localLanguage',e.target.value); const svc=this.configManager.getEffectiveService(getCurrentDomain()); if(!['microsoft','tencent'].includes(svc) && typeof translate!=='undefined') translate.language.setLocal(e.target.value); 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.refreshWhitelistDisplay(); this.translateManager.refresh(); window.updateMenuItems?.(); this.toast.show('✅ 白名单已清空'); } }); document.getElementById('save-site-service')?.addEventListener('click',async()=>{ const svc=document.getElementById('site-specific-service').value; const domain=getCurrentDomain(); await this.translateManager.disableCurrentTranslation(); this.configManager.setSiteService(domain,svc||null); this.toast.show(`✅ 已为 ${domain} ${svc?'设置专用引擎':'清除专用引擎'}`); 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','tencent':'腾讯','giteeai':'gitee AI','siliconflow':'SiliconFlow AI','translate.service':'自定义服务'}; desc.textContent=`当前生效: ${map[effective]||effective}`; } this.translateManager.initialized=false; this.translateManager.listenerStarted=false; this.translateManager.microsoftTranslator=null; this.translateManager.tencentTranslator=null; this.translateManager.refresh(); }); document.getElementById('translate-service')?.addEventListener('change',async(e)=>{ const newSvc=e.target.value; const oldSvc=this.configManager.get('translateService'); if(newSvc===oldSvc) return; await this.translateManager.disableCurrentTranslation(); this.configManager.set('translateService',newSvc); const domain=getCurrentDomain(); const effective=this.configManager.getEffectiveService(domain); this.translateManager.initialized=false; this.translateManager.listenerStarted=false; this.translateManager.microsoftTranslator=null; this.translateManager.tencentTranslator=null; this.translateManager.refresh(); this.toast.show(`✅ 已切换至 ${e.target.options[e.target.selectedIndex].text} 引擎,正在翻译...`); 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','tencent':'腾讯','giteeai':'gitee AI','siliconflow':'SiliconFlow AI','translate.service':'自定义服务'}; desc.textContent=`当前生效: ${map[effective]||effective}`; } } }); 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 selToggle=document.getElementById('translate-selection'); if(selToggle) selToggle.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 svc=this.configManager.getEffectiveService(getCurrentDomain()); if(!['microsoft','tencent'].includes(svc)){ if(e.target.checked && !this.translateManager.listenerStarted) this.translateManager.startListener(); this.toast.show(e.target.checked?'✅ 动态监听已启用':'❌ 动态监听已禁用'); } else this.toast.show('该引擎自带动态监听,无需额外设置'); }); 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('✅ 已重置,页面将刷新'); setTimeout(()=>location.reload(),1500); } }); const modeReplace=document.getElementById('ms-mode-replace'), 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('✅ 已切换为单显模式'); modeReplace.style.background='linear-gradient(135deg,#8b5cf6,#6366f1)'; 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('✅ 已切换为双显模式'); modeDual.style.background='linear-gradient(135deg,#8b5cf6,#6366f1)'; 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, 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'), colorText=document.getElementById('ms-color-text'), colorApply=document.getElementById('ms-color-apply'); if(colorCustom) colorCustom.addEventListener('change',async(e)=>{ const col=e.target.value; if(colorText) colorText.value=col; this.configManager.set('microsoftTextColor',col); this.configManager.set('microsoftBgColor',col.replace('#','rgba(')+',0.1)'); await this.translateManager.onMicrosoftModeColorChange(); this.toast.show('✅ 自定义颜色已应用'); colorPresets.forEach(p=>p.style.border='2px solid transparent'); }); if(colorApply) colorApply.addEventListener('click',async()=>{ let col=colorText.value.trim(); if(!/^#[0-9A-Fa-f]{6}$/.test(col) && !/^#[0-9A-Fa-f]{3}$/.test(col)){ this.toast.show('颜色格式错误,请使用#RRGGBB格式','error'); return; } if(col.length===4) col='#'+col[1]+col[1]+col[2]+col[2]+col[3]+col[3]; this.configManager.set('microsoftTextColor',col); this.configManager.set('microsoftBgColor',col.replace('#','rgba(')+',0.1)'); await this.translateManager.onMicrosoftModeColorChange(); this.toast.show('✅ 颜色已应用'); if(colorCustom) colorCustom.value=col; colorPresets.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 onMove=(e)=>{ if(!this.isDragging) return; let left=startLeft+(e.clientX-startX), top=startTop+(e.clientY-startY); left=Math.max(0,Math.min(left,window.innerWidth-this.panel.offsetWidth)); top=Math.max(0,Math.min(top,window.innerHeight-this.panel.offsetHeight)); this.panel.style.left=`${left}px`; this.panel.style.top=`${top}px`; }; const onUp=()=>{ if(this.isDragging){ this.isDragging=false; this.panel.classList.remove('dragging'); document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); 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',onMove); document.addEventListener('mouseup',onUp); e.preventDefault(); }); } bindResizeEvents() { const handle=document.getElementById('resize-handle'); if(!handle || isMobile()) return; let startX,startY,startW,startH; const onMove=(e)=>{ if(!this.isResizing) return; let w=startW+(e.clientX-startX), h=startH+(e.clientY-startY); w=Math.max(280,Math.min(w,window.innerWidth-this.panel.offsetLeft-10)); h=Math.max(300,Math.min(h,window.innerHeight-this.panel.offsetTop-10)); this.panel.style.width=`${w}px`; this.panel.style.height=`${h}px`; }; const onUp=()=>{ if(this.isResizing){ this.isResizing=false; this.panel.classList.remove('resizing'); document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp); 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; startW=this.panel.offsetWidth; startH=this.panel.offsetHeight; this.panel.classList.add('resizing'); document.addEventListener('mousemove',onMove); document.addEventListener('mouseup',onUp); e.preventDefault(); e.stopPropagation(); }); } togglePanel(){ this.panel.classList.toggle('show'); if(this.panel.classList.contains('show')) this.ensureInViewport(); } positionPanel(){ let left=(window.innerWidth-this.panel.offsetWidth)/2, top=(window.innerHeight-this.panel.offsetHeight)/2; left=Math.max(10,Math.min(left,window.innerWidth-this.panel.offsetWidth-10)); top=Math.max(10,Math.min(top,window.innerHeight-this.panel.offsetHeight-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 maxX=window.innerWidth-this.panel.offsetWidth, maxY=window.innerHeight-this.panel.offsetHeight; 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 update() { 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 toggleEnabledId = GM_registerMenuCommand(enabled?'❌ 关闭自动翻译':'✅ 开启自动翻译',()=>{ const newState=!configManager.get('enabled'); configManager.set('enabled',newState); configManager.set('autoTranslate',newState); translateManager.refresh(); uiManager.toast.show(newState?'✅ 全局翻译已开启':'❌ 全局翻译已关闭'); update(); }); const toggleWhitelistId = GM_registerMenuCommand(inWhitelist?'⭐ 从白名单中移除':'☆ 添加到白名单',()=>{ const nowIn=configManager.toggleWhitelistDomain(domain); uiManager.toast.show(nowIn?`✅ 已添加 ${domain} 到白名单`:`❌ 已从白名单移除 ${domain}`); uiManager.refreshWhitelistDisplay(); translateManager.refresh(); update(); }); const openSettingsId = GM_registerMenuCommand('⚙️ 翻译设置',()=>uiManager.togglePanel()); menuManager = { toggleEnabledId, toggleWhitelistId, openSettingsId }; } update(); window.updateMenuItems = update; } async function init() { if(document.readyState==='loading') await new Promise(r=>document.addEventListener('DOMContentLoaded',r)); try { 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(e) { console.error('初始化失败:', e); } } setTimeout(init, 100); })();