您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通过HTTP API连接LunaTranslator实现浏览器上的原文的分词、翻译、朗读和查词功能
// ==UserScript== // @name LunaLens // @namespace http://tampermonkey.net/ // @license MIT // @version 0.2.7 // @description 通过HTTP API连接LunaTranslator实现浏览器上的原文的分词、翻译、朗读和查词功能 // @description:en Split, translate, read and query words on the browser through the HTTP API of LunaTranslator // @description:ja LunaTranslatorのHTTP APIを通じてブラウザ上で分詞、翻訳、読み上げ、辞書検索機能を実現 // @author Raindrop213 // @match *://*/* // @grant GM_xmlhttpRequest // @connect * // ==/UserScript== (function() { 'use strict'; // 默认配置 const DEFAULT_CONFIG = { // ※重点:填写你的服务器地址,同步设置@connect如: @connect 192.168.6.229 API_URL: 'http://127.0.0.1:2333', // 分句设置 // 分句:断句的符号基准 SENTENCE_DELIMITERS: '。..!?!?…', // 分句:句子字数阈值 SENTENCE_LENGTH: 50, // 最小内容长度 MIN_CONTENT_LENGTH: 2, // 最大内容长度 MAX_CONTENT_LENGTH: 1000, // 是否移除注音 // 选择器设置 // 选择的标签 INCLUDE_TAGS: 'p, h1, h2, h3, h4, h5, h6', // 排除的标签 EXCLUDE_TAGS: '', // 包含的class id INCLUDE_CLASS_IDS: '', // 排除的class id EXCLUDE_CLASS_IDS: '', // 停止容器 STOP_CONTAINERS: 'article, main, section, div.content, div.main-content', // 顶栏常用设置 // 是否使用句子模式 DISPLAY_SENTENCE_MODE: false, // 是否打开翻译 TRANSLATION_ENABLED: false, // 是否激活标签就自动朗读 TTS_AUTO: false, // 是否打开设置栏 SETTING_DISPLAY: false, BUTTON_TEXT: { TTS: {true: 'TTS:auto', false: 'TTS'}, TRANSLATION: {true: '翻译:auto', false: '翻译'}, DISPLAY: {true: '句子', false: '段落'} }, // 其他设置 // API请求等待的最大时限 TIMEOUT: 5000 }; // 当前配置,初始化为默认配置的副本 const CONFIG = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); // 面板样式 const STYLE = ` .lunalens-highlighted { outline: 2px solid rgba(59, 122, 83, 0.52) !important; } .lunalens-word { display: inline-block; position: relative; margin: 1px; cursor: pointer; } .lunalens-word.flash { animation: word-flash 0.3s ease-out; } @keyframes word-flash { 0% { background-color: rgba(255, 206, 30, 0.5); } 100% { background-color: transparent; } } /* 仅在词典窗内使用的高亮样式 */ .lunalens-dict-context .lunalens-word.active-word { background-color:rgba(255, 206, 30, 0.5); } .lunalens-word rt { font-size: 0.7em; color: #b91a1a; } /* LunaLens面板 */ .lunalens-panel { position: fixed; bottom: 0; left: 0; right: 0; height: 92%; background-color: #fff; border-top: 1px solid #ccc; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); z-index: 9999; display: flex; flex-direction: column; font-family: sans-serif; transition: transform 0.2s ease; transform: translateY(100%); writing-mode: horizontal-tb !important; -webkit-writing-mode: horizontal-tb !important; } .lunalens-panel.visible { transform: translateY(0); } .lunalens-panel input::placeholder { color: #aaa; opacity: 0.8; } .lunalens-header { display: flex; justify-content: space-between; background: #ffffff; border-bottom: 1px solid #ddd; height: 40px; } .lunalens-title { font-size: 16px; font-weight: bold; padding: 8px 10px; margin: 0; line-height: 20px; cursor: pointer; transition: color 0.2s ease; } .lunalens-title:hover { color: #007acc; } .lunalens-header-buttons { display: flex; align-items: stretch; height: 100%; } .lunalens-header-button { cursor: pointer; color: #666; font-size: 14px; padding: 0 12px; margin: 0; border: none; border-radius: 0; border-left: 1px solid #ddd; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .lunalens-square-button { width: 40px; color: white; border: none; border-radius: 0; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 20px; margin: 0; } .lunalens-setting-toggle { background-color: #929292; } .lunalens-setting-toggle.active { background-color: #1288ab; } .lunalens-close { background-color: #000000; } .lunalens-dict-context-wrapper { position: relative; background: #f9f9f9; border-bottom: 1px solid #eee; max-height: 20vh; overflow: hidden; } .lunalens-context-buttons { position: absolute; bottom: 4px; right: 4px; display: flex; gap: 4px; z-index: 10; } .lunalens-dict-context { padding: 8px 10px 30px 10px; font-size: 15px; line-height: 1.5; overflow-y: auto; height: 100%; box-sizing: border-box; } .lunalens-dict-query-box { display: flex; padding: 0; border-bottom: 1px solid #eee; height: 40px; } .lunalens-dict-query-input { flex: 1; padding: 0 10px; border: 1px solid transparent; border-right: 1px solid #ddd; border-radius: 0; margin: 0; height: 100%; outline: none; box-sizing: border-box; } .lunalens-dict-query-input:focus { border: 1px solid #000000; } .lunalens-dict-query-button { background: #23ab12; } .lunalens-tts-button { background: #a51dd1; } .lunalens-context-copy-button { width: 32px; height: 32px; color: #4a90e2; background: rgba(74, 144, 226, 0.1); border: 1px solid rgba(74, 144, 226, 0.3); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; transition: all 0.2s ease; } .lunalens-context-translate-button { width: 32px; height: 32px; color: #50c878; background: rgba(80, 200, 120, 0.1); border: 1px solid rgba(80, 200, 120, 0.3); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; transition: all 0.2s ease; } .lunalens-context-tts-button { width: 32px; height: 32px; color: #a51dd1; background:rgba(165, 29, 209, 0.1); border: 1px solid rgba(165, 29, 209, 0.3); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all 0.2s ease; } .lunalens-dict-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .lunalens-dict-tabs { display: flex; flex-direction: row; background: #f9f9f9; overflow-x: auto; white-space: nowrap; } .lunalens-dict-tab { padding: 8px 15px; cursor: pointer; border-right: 1px solid #eee; font-size: 13px; } .lunalens-dict-tab.active { background: #fff; font-weight: bold; border-top: 3px solid #b91a1a; border-left: none; } .lunalens-dict-entries { flex: 1; overflow-y: auto; padding-top: 10px; } .lunalens-dict-entry { display: none; } .lunalens-dict-entry.active { display: block; } .lunalens-dict-loading { text-align: center; padding: 20px; color: #666; } /* 翻译区域样式 */ .lunalens-translation { padding: 8px 10px; border-bottom: 1px solid #e0e6f5; font-size: 13px; line-height: 1.5; max-height: 15vh; overflow-y: auto; } .lunalens-translator-item { position: relative; padding-right: 40px; } .lunalens-translator-name { position: absolute; right: 0; top: 0; font-size: 10px; color: #bbb; font-style: italic; } .lunalens-hr { border: none; border-top: 1px dashed #c1c1c17d; margin: 5px 0; } /* 设置窗口样式 */ .lunalens-setting-window { display: none; flex-direction: column; width: 100%; height: 100%; overflow: hidden; padding: 10px; box-sizing: border-box; overflow-y: auto; } .lunalens-setting-window.visible { display: flex; } .lunalens-dict-window { display: none; flex-direction: column; width: 100%; height: 100%; overflow: hidden; } .lunalens-dict-window.visible { display: flex; } .lunalens-setting-api-url { padding: 8px 10px; border-bottom: 1px solid #eee; } .lunalens-setting-api-url label { display: block; margin-bottom: 5px; font-weight: bold; font-size: 13px; color: #333; } .lunalens-setting-api-url input { width: 100%; padding: 6px 10px; border: 1px solid #ddd; border-radius: 0; font-size: 14px; outline: none; box-sizing: border-box; } .lunalens-setting-api-url input:focus { border: 1px solid #000000; } .lunalens-setting-tabs { display: flex; flex-direction: row; background: #f9f9f9; overflow-x: auto; white-space: nowrap; } .lunalens-setting-tab { padding: 8px 15px; cursor: pointer; border-right: 1px solid #eee; font-size: 13px; } .lunalens-setting-tab.active { background: #fff; font-weight: bold; border-top: 3px solid #b91a1a; border-bottom: none; border-left: none; } .lunalens-setting-contents { flex: 1; overflow-y: auto; } .lunalens-setting-content { display: none; padding: 10px; border-top: none; } .lunalens-setting-content.active { display: block; } .lunalens-setting-item { margin-bottom: 12px; padding-bottom: 8px; } .lunalens-setting-item:last-child { border-bottom: none; } .lunalens-setting-item label { display: block; margin-bottom: 5px; font-weight: bold; font-size: 13px; } .lunalens-setting-item input[type="text"] { width: 100%; padding: 6px 10px; border: 1px solid #ddd; border-radius: 0; outline: none; box-sizing: border-box; } .lunalens-setting-item input[type="text"]:focus { border: 1px solid #000000; } .lunalens-setting-item input[type="checkbox"] { margin-right: 8px; vertical-align: middle; } .lunalens-setting-description { font-size: 12px; color: #666; margin-top: 5px; } .lunalens-setting-description kbd { background-color:rgba(223, 223, 223, 0.5); padding: 0px 3px; border-radius: 3px; } .lunalens-setting-button { background-color: #23ab12; color: white; border: none; padding: 8px 16px; cursor: pointer; border-radius: 0; margin-top: 10px; font-size: 14px; display: flex; align-items: center; justify-content: center; align-self: flex-start; margin-right: 10px; } .lunalens-reset-settings { background-color: #cc3333; } .lunalens-setting-buttons { display: flex; flex-direction: row; } .lunalens-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: white; padding: 10px 15px; border-radius: 4px; z-index: 10001; display: none; animation: fadeInOut 2s ease-in-out; box-shadow: 0 2px 8px rgba(0,0,0,0.2); font-size: 14px; max-width: 80%; text-align: center; writing-mode: horizontal-tb; } @keyframes fadeInOut { 0% { opacity: 0; transform: translate(-50%, -10px); } 10% { opacity: 1; transform: translate(-50%, 0); } 90% { opacity: 1; transform: translate(-50%, 0); } 100% { opacity: 0; transform: translate(-50%, -10px); } } ` // 全局变量 let activeElement = null; let originalContent = null; let dictionaryPanel = null; let currentWord = ''; let activeWordElements = []; // 改为数组存储所有激活的单词元素 let contextSentence = null; let translationArea = null; // 翻译区域 let lastTranslatedText = ''; // 上次翻译的文本,用于避免重复翻译 let currentAudio = null; // 当前播放的音频对象 let pendingTTSRequests = []; // 待处理的TTS请求数组 // 添加样式到文档 function addStyle(doc) { if (!doc.querySelector('#anon-simple-style')) { const style = doc.createElement('style'); style.id = 'anon-simple-style'; style.textContent = STYLE; doc.head.appendChild(style); } } // 复制文本到剪贴板 function copyToClipboard(text) { navigator.clipboard.writeText(text) .then(() => console.log(`Copy: ${text}`)) .catch(err => console.log('Failed to copy text: ', err)); } // 获取纯文本(移除rp、rt) function getPlainText(html) { const div = document.createElement('div'); div.innerHTML = html; // 先找出所有ruby元素 div.querySelectorAll('ruby').forEach(ruby => { // 创建文本节点,只包含主要内容(不包含rt中的注音) const mainText = Array.from(ruby.childNodes) .filter(node => node.nodeName !== 'RT' && node.nodeName !== 'RP') .map(node => node.textContent) .join(''); // 替换整个ruby标签为纯文本 const textNode = document.createTextNode(mainText); ruby.parentNode.replaceChild(textNode, ruby); }); return div.textContent || ''; } // 辅助函数:判断片假名和平假名是否等价 function isKanaEquivalent(word, kana) { if (!word || !kana) return false; // 简单转换为片假名进行比较 const toKatakana = str => { const hiraRange = [0x3041, 0x3096]; return str.split('') .map(char => { const code = char.charCodeAt(0); if (code >= hiraRange[0] && code <= hiraRange[1]) { return String.fromCharCode(code + 0x60); } return char; }) .join(''); }; return toKatakana(word) === toKatakana(kana); } // 片假名转平假名 function katakanaToHiragana(text) { if (!text) return ''; // 片假名的Unicode范围是:U+30A0 to U+30FF return text.replace(/[\u30A0-\u30FF]/g, function(match) { const code = match.charCodeAt(0) - 0x60; return String.fromCharCode(code); }); } // 处理分词API请求 function processMecabAPI(text, callback) { GM_xmlhttpRequest({ method: 'GET', url: `${CONFIG.API_URL}/api/mecab?text=${encodeURIComponent(text)}`, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); callback(data); } catch (e) { console.error('解析API响应失败:', e); showToast('分词API请求失败,请检查API连接', true); callback(null); } } else { console.error('API请求失败:', response.status); showToast('API请求失败,HTTP状态码: ' + response.status, true); callback(null); } }, onerror: function(error) { console.error('API请求错误:', error); showToast('分词API请求失败,请检查API连接', true); callback(null); }, ontimeout: function() { console.error('API请求超时'); showToast('分词API请求超时', true); callback(null); }, timeout: CONFIG.TIMEOUT }); } // 显示设置面板的辅助函数 function showSettingsPanel() { // 先显示词典面板 showPanel(); // 然后切换到设置页面 if (dictionaryPanel) { dictionaryPanel.querySelector('.lunalens-dict-window').classList.remove('visible'); dictionaryPanel.querySelector('.lunalens-setting-window').classList.add('visible'); dictionaryPanel.querySelector('.lunalens-setting-toggle').classList.add('active'); } } // 生成带注音的HTML标记,添加唯一索引 function generateRubyHTML(word, index) { const indexAttr = `data-original-index="${index}"`; if (word.isdeli || !word.kana || isKanaEquivalent(word.word, word.kana)) { return `<span class="lunalens-word" data-word="${word.word}" ${indexAttr}>${word.word}</span>`; } else { // 将片假名转换为平假名 const hiragana = katakanaToHiragana(word.kana); return `<span class="lunalens-word" data-word="${word.word}" ${indexAttr}><ruby>${word.word}<rt>${hiragana}</rt></ruby></span>`; } } // 处理文本并生成带注音的分句HTML function processTextWithTokenization(html, callback) { // 1. 获取纯文本 const plainText = getPlainText(html); // 2. 一次性请求API进行分词 processMecabAPI(plainText, function(words) { if (!words) { callback(html); // 如果API失败,返回原HTML showSettingsPanel(); return; } // 3. 按句子分隔符构建句子数组 const sentences = []; let currentSentence = []; let currentSentenceLength = 0; let wordIndex = 0; // 遍历所有词汇,构建句子 words.forEach(word => { // 生成当前词的HTML,添加唯一索引 const wordHTML = generateRubyHTML(word, wordIndex++); currentSentence.push(wordHTML); currentSentenceLength += word.word.length; // 如果当前词是句子分隔符,检查是否应结束当前句子 if (CONFIG.SENTENCE_DELIMITERS.indexOf(word.word) !== -1 && currentSentenceLength >= CONFIG.SENTENCE_LENGTH) { sentences.push(`<span class="lunalens-sentence">${currentSentence.join('')}</span>`); currentSentence = []; currentSentenceLength = 0; } }); // 处理剩余的词 if (currentSentence.length > 0) { sentences.push(`<span class="lunalens-sentence">${currentSentence.join('')}</span>`); } callback(sentences.join('')); }); } // 查询按钮点击事件(添加下一个单词) function setupQueryButton(queryButton, queryInput) { queryButton.addEventListener('click', function() { if (activeWordElements.length > 0) { // 获取最后一个激活的单词元素的索引 const lastActiveIndex = activeWordElements[activeWordElements.length - 1]; let lastActiveElement; // 在主页和词典上下文中查找元素 const allWords = document.querySelectorAll('.lunalens-word'); for (const word of allWords) { if (word.dataset.originalIndex === lastActiveIndex || (typeof lastActiveIndex === 'object' && lastActiveIndex.dataset && lastActiveIndex.dataset.originalIndex === word.dataset.originalIndex)) { lastActiveElement = word; // 一旦在主页上找到匹配的元素,优先使用 if (activeElement.contains(word)) { break; } } } if (!lastActiveElement) return; // 寻找下一个单词元素 let nextWord = null; if (activeElement.contains(lastActiveElement)) { // 如果最后激活的单词在主页上,直接找下一个相邻元素 nextWord = lastActiveElement.nextElementSibling; while (nextWord && !nextWord.classList.contains('lunalens-word')) { nextWord = nextWord.nextElementSibling; } } else { // 如果最后激活的单词在词典上下文中,需要找到主页上对应的单词,再找下一个 const mainPageWords = activeElement.querySelectorAll('.lunalens-word'); for (let i = 0; i < mainPageWords.length; i++) { if (mainPageWords[i].dataset.originalIndex === lastActiveIndex) { if (i < mainPageWords.length - 1) { nextWord = mainPageWords[i + 1]; } break; } } } if (nextWord && nextWord.classList.contains('lunalens-word')) { // 记录下一个单词,保留之前的记录 activeWordElements.push(nextWord.dataset.originalIndex || nextWord); // 获取当前查询框内容 const currentQuery = queryInput.value.trim(); // 将下一个单词添加到查询框 queryInput.value = currentQuery + nextWord.dataset.word; // 更新上下文 updateContext(); // 触发查询 lookupWord(queryInput.value); } } }); } // 上下文区域点击事件处理 function setupContextAreaEvents(contextArea, queryInput) { // 使用事件委托,避免在每次更新上下文时都要重新绑定事件 contextArea.addEventListener('click', function(e) { // 阻止事件冒泡到document,防止触发deactivateElement e.stopPropagation(); // 寻找被点击的单词元素,包括临时容器中的元素 let targetWord = null; if (e.target.classList && e.target.classList.contains('lunalens-word')) { targetWord = e.target; } else { targetWord = e.target.closest('.lunalens-word'); } if (targetWord) { // 清除所有已激活的高亮样式 contextArea.querySelectorAll('.lunalens-word.active-word').forEach(word => { word.classList.remove('active-word'); }); // 清空激活单词数组 activeWordElements = []; // 直接给当前单词添加高亮类 targetWord.classList.add('active-word'); // 记录当前激活的单词 activeWordElements.push(targetWord.dataset.originalIndex || targetWord); // 更新查询框内容 queryInput.value = targetWord.dataset.word; // 触发查询 lookupWord(targetWord.dataset.word); // 找到主页上对应的单词并添加临时闪烁效果 const originalIndex = targetWord.dataset.originalIndex; if (originalIndex && activeElement) { const mainPageWord = activeElement.querySelector(`.lunalens-word[data-original-index="${originalIndex}"]`); if (mainPageWord) { mainPageWord.classList.add('flash'); setTimeout(() => { mainPageWord.classList.remove('flash'); }, 1000); } } } }); } // 创建词典面板 function createDictionaryPanel() { // 检查是否已存在 if (document.querySelector('.lunalens-panel')) { return document.querySelector('.lunalens-panel'); } // 创建面板 const panel = document.createElement('div'); panel.className = 'lunalens-panel'; panel.innerHTML = ` <div class="lunalens-header"> <div class="lunalens-title">LunaLens</div> <div class="lunalens-header-buttons"> <span class="lunalens-header-button lunalens-auto-tts-toggle" title="自动朗读开关"> ${CONFIG.BUTTON_TEXT.TTS[CONFIG.TTS_AUTO]} </span> <span class="lunalens-header-button lunalens-translation-toggle" title="切换翻译功能"> ${CONFIG.BUTTON_TEXT.TRANSLATION[CONFIG.TRANSLATION_ENABLED]} </span> <span class="lunalens-header-button lunalens-context-toggle" title="切换句子/段落"> ${CONFIG.BUTTON_TEXT.DISPLAY[CONFIG.DISPLAY_SENTENCE_MODE]} </span> <button class="lunalens-square-button lunalens-setting-toggle" title="设置">⚙</button> <button class="lunalens-square-button lunalens-close" title="关闭面板">×</button> </div> </div> <div class="lunalens-dict-window"> <div class="lunalens-dict-context-wrapper"> <div class="lunalens-dict-context"></div> <div class="lunalens-context-buttons"> <button class="lunalens-context-copy-button" title="复制内容">⧉</button> <button class="lunalens-context-translate-button" title="翻译内容">翻</button> <button class="lunalens-context-tts-button" title="朗读上下文">♬</button> </div> </div> <div class="lunalens-translation"></div> <div class="lunalens-dict-query-box"> <input type="text" class="lunalens-dict-query-input" placeholder="输入要查询的单词"> <button class="lunalens-square-button lunalens-dict-query-button">+</button> <button class="lunalens-square-button lunalens-tts-button" title="朗读单词">♬</button> </div> <div class="lunalens-dict-content"> <div class="lunalens-dict-tabs"></div> <div class="lunalens-dict-entries"> <div class="lunalens-dict-loading">请点击任意单词或输入要查询的词</div> </div> </div> </div> <div class="lunalens-setting-window"> <div class="lunalens-setting-api-url"> <label for="lunalens-api-url">API URL:</label> <input type="text" id="lunalens-api-url" value="${CONFIG.API_URL}" placeholder="例如:http://127.0.0.1:2333"> <div class="lunalens-setting-description">通常为LunaTranslator的网络服务地址</div> </div> <div class="lunalens-setting-tabs"> <div class="lunalens-setting-tab active" data-tab="sentence-settings">分句设置</div> <div class="lunalens-setting-tab" data-tab="selector-settings">选择器设置</div> <div class="lunalens-setting-tab" data-tab="other-settings">其他设置</div> </div> <div class="lunalens-setting-contents"> <div class="lunalens-setting-content active" id="sentence-settings"> <div class="lunalens-setting-item"> <label for="sentence-delimiters">分句断句符号</label> <input type="text" id="sentence-delimiters" value="${CONFIG.SENTENCE_DELIMITERS}" placeholder="切分为多个句子单元的符合"> </div> <div class="lunalens-setting-item"> <label for="sentence-length">句子字数阈值</label> <input type="text" id="sentence-length" value="${CONFIG.SENTENCE_LENGTH}" placeholder="防止句子过短而设置的最小句子长度"> </div> <div class="lunalens-setting-item"> <label for="min-content-length">最小内容长度</label> <input type="text" id="min-content-length" value="${CONFIG.MIN_CONTENT_LENGTH}" placeholder="过短的文本不会被选中"> </div> <div class="lunalens-setting-item"> <label for="max-content-length">最大内容长度</label> <input type="text" id="max-content-length" value="${CONFIG.MAX_CONTENT_LENGTH}" placeholder="过长的文本不会被选中"> </div> <div class="lunalens-setting-description">※注意:默认去除原句的振假名注音(ruby 中的 rt 和 rp)</div> </div> <div class="lunalens-setting-content" id="selector-settings"> <div class="lunalens-setting-item"> <label for="include-tags">包含的标签</label> <input type="text" id="include-tags" value="${CONFIG.INCLUDE_TAGS}" placeholder="例如:p, h1, h2, h3, h4, h5, h6, div"> </div> <div class="lunalens-setting-item"> <label for="exclude-tags">排除的标签</label> <input type="text" id="exclude-tags" value="${CONFIG.EXCLUDE_TAGS}" placeholder="例如: a, img, em, dd, code, button"> </div> <div class="lunalens-setting-item"> <label for="include-class-ids">包含的class/id</label> <input type="text" id="include-class-ids" value="${CONFIG.INCLUDE_CLASS_IDS}" placeholder="例如:.article-content, #main-content"> </div> <div class="lunalens-setting-item"> <label for="exclude-class-ids">排除的class/id</label> <input type="text" id="exclude-class-ids" value="${CONFIG.EXCLUDE_CLASS_IDS}" placeholder="例如:.sidebar, #ads, .popup, #comments"> </div> <div class="lunalens-setting-item"> <label for="stop-containers">停止容器</label> <input type="text" id="stop-containers" value="${CONFIG.STOP_CONTAINERS}" placeholder="例如:article, main, section, div.content, div.main-content"> </div> <div class="lunalens-setting-description">※注意class和id的写法:<br><kbd>class</kbd> 前加 <kbd>.</kbd><br><kbd>id</kbd> 前加 <kbd>#</kbd><br>用逗号分隔</div> <div class="lunalens-setting-description">如果你看不懂又选不中文本的时候,请试试在第一栏加多个加上 <kbd>div</kbd> 提高选中率</div> </div> <div class="lunalens-setting-content" id="other-settings"> <div class="lunalens-setting-item"> <label for="timeout">API请求超时(ms)</label> <input type="text" id="timeout" value="${CONFIG.TIMEOUT}"> </div> </div> </div> <div class="lunalens-setting-buttons"> <button class="lunalens-setting-button lunalens-save-settings">保存设置</button> <button class="lunalens-setting-button lunalens-reset-settings">重置设置</button> </div> </div> `; document.body.appendChild(panel); // 保存翻译区域引用 translationArea = panel.querySelector('.lunalens-translation'); // 添加事件处理 const titleElement = panel.querySelector('.lunalens-title'); const closeButton = panel.querySelector('.lunalens-close'); const contextToggle = panel.querySelector('.lunalens-context-toggle'); const translationToggle = panel.querySelector('.lunalens-translation-toggle'); const ttsToggle = panel.querySelector('.lunalens-auto-tts-toggle'); const settingToggle = panel.querySelector('.lunalens-setting-toggle'); const queryInput = panel.querySelector('.lunalens-dict-query-input'); const queryButton = panel.querySelector('.lunalens-dict-query-button'); const contextArea = panel.querySelector('.lunalens-dict-context'); const ttsButton = panel.querySelector('.lunalens-tts-button'); const contextTtsButton = panel.querySelector('.lunalens-context-tts-button'); const contextCopyButton = panel.querySelector('.lunalens-context-copy-button'); const contextTranslateButton = panel.querySelector('.lunalens-context-translate-button'); const settingTabs = panel.querySelectorAll('.lunalens-setting-tab'); const saveButton = panel.querySelector('.lunalens-save-settings'); const dictWindow = panel.querySelector('.lunalens-dict-window'); const settingWindow = panel.querySelector('.lunalens-setting-window'); // 切换到设置面板 settingToggle.addEventListener('click', function() { const isSettingVisible = this.classList.contains('active'); this.classList.toggle('active'); if (isSettingVisible) { // 从设置切换到词典 dictWindow.classList.add('visible'); settingWindow.classList.remove('visible'); } else { // 从词典切换到设置 dictWindow.classList.remove('visible'); settingWindow.classList.add('visible'); // 加载保存的设置 loadSettings(); } }); // 设置面板中的标签切换 settingTabs.forEach(tab => { tab.addEventListener('click', function() { // 移除所有标签的活动状态 settingTabs.forEach(t => t.classList.remove('active')); // 添加当前标签的活动状态 this.classList.add('active'); // 隐藏所有内容 panel.querySelectorAll('.lunalens-setting-content').forEach(content => { content.classList.remove('active'); }); // 显示当前标签对应的内容 const tabId = this.getAttribute('data-tab'); const tabContent = panel.querySelector(`#${tabId}`); if (tabContent) { tabContent.classList.add('active'); } }); }); // 保存设置 saveButton.addEventListener('click', function() { // 收集所有设置输入 const apiUrl = panel.querySelector('#lunalens-api-url').value.trim(); const sentenceDelimiters = panel.querySelector('#sentence-delimiters').value; const sentenceLength = parseInt(panel.querySelector('#sentence-length').value) || 50; const minContentLength = parseInt(panel.querySelector('#min-content-length').value) || 2; const maxContentLength = parseInt(panel.querySelector('#max-content-length').value) || 1000; const includeSelectors = panel.querySelector('#include-tags').value; const excludeSelectors = panel.querySelector('#exclude-tags').value; const includeClassIds = panel.querySelector('#include-class-ids').value; const excludeClassIds = panel.querySelector('#exclude-class-ids').value; const stopContainers = panel.querySelector('#stop-containers').value; const timeout = parseInt(panel.querySelector('#timeout').value) || 5000; // 更新配置 CONFIG.API_URL = apiUrl; CONFIG.SENTENCE_DELIMITERS = sentenceDelimiters; CONFIG.SENTENCE_LENGTH = sentenceLength; CONFIG.MIN_CONTENT_LENGTH = minContentLength; CONFIG.MAX_CONTENT_LENGTH = maxContentLength; CONFIG.INCLUDE_TAGS = includeSelectors; CONFIG.EXCLUDE_TAGS = excludeSelectors; CONFIG.INCLUDE_CLASS_IDS = includeClassIds; CONFIG.EXCLUDE_CLASS_IDS = excludeClassIds; CONFIG.STOP_CONTAINERS = stopContainers; CONFIG.TIMEOUT = timeout; // 保存到本地存储 saveSettings(); // 显示通知 showToast('设置已保存', false); }); // 标题点击关闭面板 titleElement.addEventListener('click', function() { // 停止当前播放的语音 stopReading(); hidePanel(); }); // 关闭词典面板 closeButton.addEventListener('click', function() { // 停止当前播放的语音 stopReading(); hidePanel(); }); // 切换句子/段落 contextToggle.addEventListener('click', function() { CONFIG.DISPLAY_SENTENCE_MODE = !CONFIG.DISPLAY_SENTENCE_MODE; this.textContent = CONFIG.BUTTON_TEXT.DISPLAY[CONFIG.DISPLAY_SENTENCE_MODE]; // 更新上下文 updateContext(); // 保存设置到本地存储 saveSettings(); }); // 切换翻译功能 translationToggle.addEventListener('click', function() { CONFIG.TRANSLATION_ENABLED = !CONFIG.TRANSLATION_ENABLED; this.textContent = CONFIG.BUTTON_TEXT.TRANSLATION[CONFIG.TRANSLATION_ENABLED]; this.classList.toggle('active', CONFIG.TRANSLATION_ENABLED); if (CONFIG.TRANSLATION_ENABLED) { translationArea.style.display = 'block'; // 如果有上下文内容且不同于上次翻译的内容,立即翻译 const currentContextText = getContextText(); if (currentContextText && currentContextText !== lastTranslatedText) { translateContextText(currentContextText); } } else { translationArea.style.display = 'none'; } // 保存设置到本地存储 saveSettings(); }); // 切换TTS功能 ttsToggle.addEventListener('click', function() { CONFIG.TTS_AUTO = !CONFIG.TTS_AUTO; this.textContent = CONFIG.BUTTON_TEXT.TTS[CONFIG.TTS_AUTO]; this.classList.toggle('active', CONFIG.TTS_AUTO); // 保存设置到本地存储 saveSettings(); }); // 单词发音按钮 ttsButton.addEventListener('click', function() { const word = queryInput.value.trim(); if (word) { readText(word); } }); // 上下文朗读按钮 contextTtsButton.addEventListener('click', function() { const contextText = getContextText(); if (contextText) { readText(contextText); } }); // 上下文复制按钮 contextCopyButton.addEventListener('click', function() { const contextText = getContextText(); copyToClipboard(contextText); }); // 上下文翻译按钮 contextTranslateButton.addEventListener('click', function() { const contextText = getContextText(); if (contextText) { // 临时显示翻译区域 translationArea.style.display = 'block'; // 手动触发翻译(即使总开关关闭也能工作) translateContextText(contextText, true); } }); // 查询框输入事件 queryInput.addEventListener('input', function() { const word = this.value.trim(); if (word) { lookupWord(word); } }); // 查询按钮点击事件(添加下一个单词) setupQueryButton(queryButton, queryInput); // 上下文区域点击事件:点击上下文中的单词 setupContextAreaEvents(contextArea, queryInput); // 初始设置翻译区域显示状态 translationArea.style.display = CONFIG.TRANSLATION_ENABLED ? 'block' : 'none'; // 添加重置设置按钮事件处理 const resetButton = panel.querySelector('.lunalens-reset-settings'); resetButton.addEventListener('click', function() { resetSettings(); showToast('设置已重置为默认值', false); }); // 阻止滚动事件冒泡到主页 panel.addEventListener('wheel', function(e) { e.stopPropagation(); }); panel.addEventListener('touchmove', function(e) { e.stopPropagation(); }); return panel; } // 显示词典面板 function showPanel() { if (dictionaryPanel) { dictionaryPanel.classList.add('visible'); dictionaryPanel.querySelector('.lunalens-dict-window').classList.add('visible'); dictionaryPanel.querySelector('.lunalens-setting-window').classList.remove('visible'); dictionaryPanel.querySelector('.lunalens-setting-toggle').classList.remove('active'); } document.documentElement.style.overflow = 'hidden'; } // 隐藏词典面板 function hidePanel() { if (dictionaryPanel) { dictionaryPanel.classList.remove('visible'); } document.documentElement.style.overflow = 'auto'; } // 更新词典面板上下文(句子或段落) function updateContext() { if (!dictionaryPanel || !activeElement || !contextSentence) return; const contextArea = dictionaryPanel.querySelector('.lunalens-dict-context'); // 保存当前滚动位置 const scrollTop = contextArea.scrollTop; // 清空上下文区域 contextArea.innerHTML = ''; // 创建临时容器 const tempContainer = document.createElement('div'); if (!CONFIG.DISPLAY_SENTENCE_MODE) { // 段落模式:显示整个段落的内容 tempContainer.innerHTML = activeElement.innerHTML; } else { // 句子模式:只显示当前句子的内容 tempContainer.innerHTML = contextSentence.innerHTML; } // 将内容添加到上下文区域,不带原始事件监听器 contextArea.appendChild(tempContainer); // 重新标记激活的单词 if (activeWordElements.length > 0) { contextArea.querySelectorAll('.lunalens-word').forEach(word => { const isActive = activeWordElements.some(activeWord => activeWord === word.dataset.originalIndex || // 通过索引匹配 (typeof activeWord === 'object' && activeWord.dataset && activeWord.dataset.originalIndex === word.dataset.originalIndex) ); if (isActive) { word.classList.add('active-word'); } else { word.classList.remove('active-word'); } }); } // 恢复滚动位置 contextArea.scrollTop = scrollTop; // 检查上下文内容是否变化,如果变化且翻译功能开启,才翻译上下文内容 if (CONFIG.TRANSLATION_ENABLED) { const currentContextText = getContextText(); if (currentContextText !== lastTranslatedText) { translateContextText(currentContextText); } } } // 获取上下文区域的纯文本 function getContextText() { if (!dictionaryPanel) return ''; const contextArea = dictionaryPanel.querySelector('.lunalens-dict-context'); if (!contextArea) return ''; // 去除振假名 return getPlainText(contextArea.innerHTML).trim(); } // 翻译上下文内容 function translateContextText(text, forceTranslate = false) { if (!translationArea || (!CONFIG.TRANSLATION_ENABLED && !forceTranslate)) return; // 如果未提供文本参数,则获取当前上下文文本 if (!text) { text = getContextText(); } if (!text) { translationArea.innerHTML = '<div class="lunalens-translator-item">没有可翻译的文本</div>'; return; } // 避免重复翻译相同的文本 if (text === lastTranslatedText) { return; } // 更新上次翻译的文本 lastTranslatedText = text; // 清空翻译区域,显示加载信息 translationArea.innerHTML = '<div class="lunalens-translator-item">正在获取翻译...</div>'; // 获取翻译器列表并发起请求 GM_xmlhttpRequest({ method: 'GET', url: `${CONFIG.API_URL}/api/list/translator`, timeout: CONFIG.TIMEOUT, onload: (response) => { try { const translators = JSON.parse(response.responseText); if (Array.isArray(translators) && translators.length > 0) { // 清空翻译容器 translationArea.innerHTML = ''; // 对每个翻译器发起请求 translators.forEach((translator, index) => { // 为每个翻译器创建一个项,先显示加载中 const translatorItem = document.createElement('div'); translatorItem.className = 'lunalens-translator-item'; translatorItem.innerHTML = `加载中...<span class="lunalens-translator-name">${translator.name}</span>`; translationArea.appendChild(translatorItem); // 如果不是最后一个翻译器,添加分隔线 if (index < translators.length - 1) { const hr = document.createElement('hr'); hr.className = 'lunalens-hr'; translationArea.appendChild(hr); } // 发起翻译请求 GM_xmlhttpRequest({ method: 'GET', url: `${CONFIG.API_URL}/api/translate?text=${encodeURIComponent(text)}&id=${encodeURIComponent(translator.id)}`, timeout: CONFIG.TIMEOUT, onload: (response) => { try { const data = JSON.parse(response.responseText); translatorItem.innerHTML = `${data.result || '翻译失败'}<span class="lunalens-translator-name">${translator.name}</span>`; } catch (error) { translatorItem.innerHTML = `翻译结果解析失败<span class="lunalens-translator-name">${translator.name}</span>`; } }, onerror: () => { translatorItem.innerHTML = `翻译请求失败<span class="lunalens-translator-name">${translator.name}</span>`; }, ontimeout: () => { translatorItem.innerHTML = `翻译请求超时<span class="lunalens-translator-name">${translator.name}</span>`; } }); }); } else { translationArea.innerHTML = '<div class="lunalens-translator-item">未找到可用的翻译器</div>'; } } catch (error) { translationArea.innerHTML = '<div class="lunalens-translator-item">获取翻译器列表失败</div>'; } }, onerror: () => { translationArea.innerHTML = '<div class="lunalens-translator-item">获取翻译器列表失败</div>'; }, ontimeout: () => { translationArea.innerHTML = '<div class="lunalens-translator-item">获取翻译器列表超时</div>'; } }); } // 激活元素 function activateElement(element) { // 取消激活当前元素 deactivateElement(); // 设置新的激活元素 activeElement = element; // 保存原始内容以便后续恢复 originalContent = element.innerHTML; element.classList.add('lunalens-highlighted'); // 重置上次翻译的文本 lastTranslatedText = ''; // 创建一个取消标志,用于标识当前处理是否仍然有效 const requestId = Date.now(); element.dataset.requestId = requestId; // 如果TTS功能开启,尝试朗读文本 if (CONFIG.TTS_AUTO) { const plainText = getPlainText(originalContent); setTimeout(() => { // 使用重构后的TTS朗读功能 readText(plainText); }, 50); } // 处理文本并添加分词标记 processTextWithTokenization(originalContent, html => { // 如果元素已经不是当前激活的元素或者请求ID不匹配,则不更新内容 if (element !== activeElement || element.dataset.requestId != requestId) { return; } element.innerHTML = html; // 为所有单词添加点击事件 element.querySelectorAll('.lunalens-word').forEach(word => { word.addEventListener('click', e => { e.stopPropagation(); // 获取单词文本 const wordText = word.dataset.word; // 获取所在句子 contextSentence = word.closest('.lunalens-sentence'); if (!contextSentence) contextSentence = element; // 给单词添加临时闪烁效果 word.classList.add('flash'); setTimeout(() => { word.classList.remove('flash'); }, 1000); // 首先清空之前的激活状态 const contextArea = dictionaryPanel.querySelector('.lunalens-dict-context'); if (contextArea) { contextArea.querySelectorAll('.lunalens-word.active-word').forEach(w => { w.classList.remove('active-word'); }); } activeWordElements = []; // 记录当前活跃词 activeWordElements.push(word.dataset.originalIndex || word); // 显示词典面板 showPanel(); // 更新面板上下文 updateContext(); // 设置查词框内容 const queryInput = dictionaryPanel.querySelector('.lunalens-dict-query-input'); queryInput.value = wordText; // 查询单词 lookupWord(wordText); }); }); }); } // 取消激活元素 function deactivateElement() { if (activeElement && originalContent) { activeElement.innerHTML = originalContent; activeElement.classList.remove('lunalens-highlighted'); activeElement = null; originalContent = null; // 清除激活单词记录 activeWordElements = []; // 清除上次翻译的文本记录 lastTranslatedText = ''; } } // 处理点击事件 function handleClick(e) { // 如果点击发生在词典面板内,不处理 if (dictionaryPanel && dictionaryPanel.contains(e.target)) { return; } if (activeElement && activeElement.contains(e.target)) { if (e.target.classList.contains('lunalens-sentence') || e.target.classList.contains('lunalens-word') || e.target.closest('.lunalens-word')) { e.preventDefault(); return; } } else if (isElementSelectable(e.target)) { // 检查内容是否为空或只有空白字符 const textContent = e.target.textContent.trim(); if (textContent.length < CONFIG.MIN_CONTENT_LENGTH || textContent.length > CONFIG.MAX_CONTENT_LENGTH) { return; } e.preventDefault(); activateElement(e.target); } else if (!e.target.closest('.lunalens-panel')) { // 只有当点击不在词典面板内时才停用元素 deactivateElement(); } } // 判断元素是否符合选择器设置 function isElementSelectable(element) { if (!element) return false; const tagName = element.tagName.toLowerCase(); // 处理逗号分隔的配置字符串并返回有效的数组 function parseConfigList(configString) { return configString.split(',').map(item => item.trim()).filter(Boolean); } // 1. 检查标签是否在包含列表中 const includeTags = parseConfigList(CONFIG.INCLUDE_TAGS); if (includeTags.length > 0 && !includeTags.includes(tagName)) { return false; } // 2. 检查标签是否在排除列表中 const excludeTags = parseConfigList(CONFIG.EXCLUDE_TAGS); if (excludeTags.length > 0 && excludeTags.includes(tagName)) { return false; } // 3. 检查class和id是否在包含列表中 const includeClassIds = parseConfigList(CONFIG.INCLUDE_CLASS_IDS); if (includeClassIds.length > 0) { let matched = false; for (const selector of includeClassIds) { if (selector.startsWith('.') && element.classList.contains(selector.substring(1))) { matched = true; break; } else if (selector.startsWith('#') && element.id === selector.substring(1)) { matched = true; break; } } if (!matched) return false; } // 4. 检查class和id是否在排除列表中 const excludeClassIds = parseConfigList(CONFIG.EXCLUDE_CLASS_IDS); if (excludeClassIds.length > 0) { for (const selector of excludeClassIds) { if (selector.startsWith('.') && element.classList.contains(selector.substring(1))) { return false; } else if (selector.startsWith('#') && element.id === selector.substring(1)) { return false; } } } // 5. 检查是否在停止容器内 const stopContainers = parseConfigList(CONFIG.STOP_CONTAINERS); if (stopContainers.length > 0) { for (const selector of stopContainers) { let closestContainer = null; try { closestContainer = element.closest(selector); } catch (e) { console.error(`无效的选择器: ${selector}`, e); } if (closestContainer) { // 如果元素本身就是停止容器,允许选择 if (closestContainer === element) { return true; } // 如果元素在停止容器内,需要确保它不是深层嵌套的 const parentContainer = element.parentElement.closest(selector); if (!parentContainer || parentContainer === closestContainer) { return true; } return false; } } } return true; } // 处理iframe function handleIframe(iframe) { try { function setupIframe() { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; addStyle(iframeDoc); iframeDoc.addEventListener('click', handleClick); } if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') { setupIframe(); } else { iframe.addEventListener('load', setupIframe); } } catch (e) { console.log('无法访问iframe:', e); } } // 处理所有iframe function handleAllIframes() { document.querySelectorAll('iframe').forEach(handleIframe); } // 初始化函数 function init() { addStyle(document); document.addEventListener('click', handleClick); // 创建常驻词典面板 dictionaryPanel = createDictionaryPanel(); // 处理现有iframe handleAllIframes(); // 监听新添加的iframe new MutationObserver(handleAllIframes).observe(document.body, { childList: true, subtree: true }); // 加载保存的设置 loadSettings(); } setTimeout(init, 500); // 显示通知 function showToast(message, isError = false) { // 创建通知元素 const toast = document.createElement('div'); toast.className = 'lunalens-toast'; toast.textContent = message; // 错误消息使用红色背景 if (isError) { toast.style.backgroundColor = 'rgba(220, 53, 69, 0.9)'; } else { toast.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; } // 添加到文档中并显示 document.body.appendChild(toast); // 强制回流以触发动画 void toast.offsetWidth; toast.style.display = 'block'; // 动画结束后移除元素 setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); }, 2000); } // 保存设置到本地存储 function saveSettings() { const settings = { API_URL: CONFIG.API_URL, SENTENCE_DELIMITERS: CONFIG.SENTENCE_DELIMITERS, SENTENCE_LENGTH: CONFIG.SENTENCE_LENGTH, MIN_CONTENT_LENGTH: CONFIG.MIN_CONTENT_LENGTH, MAX_CONTENT_LENGTH: CONFIG.MAX_CONTENT_LENGTH, INCLUDE_TAGS: CONFIG.INCLUDE_TAGS, EXCLUDE_TAGS: CONFIG.EXCLUDE_TAGS, INCLUDE_CLASS_IDS: CONFIG.INCLUDE_CLASS_IDS, EXCLUDE_CLASS_IDS: CONFIG.EXCLUDE_CLASS_IDS, STOP_CONTAINERS: CONFIG.STOP_CONTAINERS, TIMEOUT: CONFIG.TIMEOUT, DISPLAY_SENTENCE_MODE: CONFIG.DISPLAY_SENTENCE_MODE, TRANSLATION_ENABLED: CONFIG.TRANSLATION_ENABLED, TTS_AUTO: CONFIG.TTS_AUTO }; try { localStorage.setItem('lunalens_settings', JSON.stringify(settings)); } catch (e) { console.error('保存设置失败:', e); } } // 从本地存储加载设置 function loadSettings() { try { const savedSettings = localStorage.getItem('lunalens_settings'); if (savedSettings) { const settings = JSON.parse(savedSettings); // 更新CONFIG对象 Object.keys(settings).forEach(key => { if (typeof CONFIG[key] !== 'undefined') { CONFIG[key] = settings[key]; } }); // 更新设置表单值 if (dictionaryPanel) { const panel = dictionaryPanel; // 更新基本设置 const updateField = (id, value) => { const field = panel.querySelector(`#${id}`); if (field) field.value = value; }; updateField('lunalens-api-url', CONFIG.API_URL); updateField('sentence-delimiters', CONFIG.SENTENCE_DELIMITERS); updateField('sentence-length', CONFIG.SENTENCE_LENGTH); updateField('min-content-length', CONFIG.MIN_CONTENT_LENGTH); updateField('max-content-length', CONFIG.MAX_CONTENT_LENGTH); updateField('include-tags', CONFIG.INCLUDE_TAGS); updateField('exclude-tags', CONFIG.EXCLUDE_TAGS); updateField('include-class-ids', CONFIG.INCLUDE_CLASS_IDS); updateField('exclude-class-ids', CONFIG.EXCLUDE_CLASS_IDS); updateField('stop-containers', CONFIG.STOP_CONTAINERS); updateField('timeout', CONFIG.TIMEOUT); // 更新顶栏按钮状态 panel.querySelector('.lunalens-context-toggle').textContent = CONFIG.BUTTON_TEXT.DISPLAY[CONFIG.DISPLAY_SENTENCE_MODE]; panel.querySelector('.lunalens-translation-toggle').textContent = CONFIG.BUTTON_TEXT.TRANSLATION[CONFIG.TRANSLATION_ENABLED]; panel.querySelector('.lunalens-translation-toggle').classList.toggle('active', CONFIG.TRANSLATION_ENABLED); panel.querySelector('.lunalens-auto-tts-toggle').textContent = CONFIG.BUTTON_TEXT.TTS[CONFIG.TTS_AUTO]; panel.querySelector('.lunalens-auto-tts-toggle').classList.toggle('active', CONFIG.TTS_AUTO); // 显示/隐藏翻译区域 if (translationArea) { translationArea.style.display = CONFIG.TRANSLATION_ENABLED ? 'block' : 'none'; } } } } catch (e) { console.error('加载设置失败:', e); } } // 重置设置为默认值 function resetSettings() { // 从DEFAULT_CONFIG复制所有属性到CONFIG Object.keys(DEFAULT_CONFIG).forEach(key => { CONFIG[key] = DEFAULT_CONFIG[key]; }); // 更新UI上的设置值 if (dictionaryPanel) { const panel = dictionaryPanel; // 更新基本设置 const updateField = (id, value) => { const field = panel.querySelector(`#${id}`); if (field) field.value = value; }; updateField('lunalens-api-url', CONFIG.API_URL); updateField('sentence-delimiters', CONFIG.SENTENCE_DELIMITERS); updateField('sentence-length', CONFIG.SENTENCE_LENGTH); updateField('min-content-length', CONFIG.MIN_CONTENT_LENGTH); updateField('max-content-length', CONFIG.MAX_CONTENT_LENGTH); updateField('include-tags', CONFIG.INCLUDE_TAGS); updateField('exclude-tags', CONFIG.EXCLUDE_TAGS); updateField('include-class-ids', CONFIG.INCLUDE_CLASS_IDS); updateField('exclude-class-ids', CONFIG.EXCLUDE_CLASS_IDS); updateField('stop-containers', CONFIG.STOP_CONTAINERS); updateField('timeout', CONFIG.TIMEOUT); // 更新顶栏按钮状态 panel.querySelector('.lunalens-context-toggle').textContent = CONFIG.BUTTON_TEXT.DISPLAY[CONFIG.DISPLAY_SENTENCE_MODE]; panel.querySelector('.lunalens-translation-toggle').textContent = CONFIG.BUTTON_TEXT.TRANSLATION[CONFIG.TRANSLATION_ENABLED]; panel.querySelector('.lunalens-translation-toggle').classList.toggle('active', CONFIG.TRANSLATION_ENABLED); panel.querySelector('.lunalens-auto-tts-toggle').textContent = CONFIG.BUTTON_TEXT.TTS[CONFIG.TTS_AUTO]; panel.querySelector('.lunalens-auto-tts-toggle').classList.toggle('active', CONFIG.TTS_AUTO); // 显示/隐藏翻译区域 if (translationArea) { translationArea.style.display = CONFIG.TRANSLATION_ENABLED ? 'block' : 'none'; } } // 从本地存储中移除保存的设置 localStorage.removeItem('lunalens_settings'); } // 查词典并显示结果 function lookupWord(word) { if (!word) return; if (word === currentWord) return; currentWord = word; // 获取词典面板 const panel = document.querySelector('.lunalens-panel'); if (!panel) return; const tabsContainer = panel.querySelector('.lunalens-dict-tabs'); const entriesContainer = panel.querySelector('.lunalens-dict-entries'); // 清空现有内容 tabsContainer.innerHTML = ''; entriesContainer.innerHTML = `<div class="lunalens-dict-loading">正在查询${word}</div>`; // 使用兼容模式查询 fetchDictionaryByGM(word, tabsContainer, entriesContainer); } // 使用GM_xmlhttpRequest并行获取多个词典数据 function fetchDictionaryByGM(word, tabsContainer, entriesContainer, dictIds = []) { // 生成唯一请求ID const requestId = Date.now().toString(); dictionaryPanel.setAttribute('data-request-id', requestId); // 如果没有提供词典ID列表,则先获取可用词典列表 if (!dictIds || dictIds.length === 0) { GM_xmlhttpRequest({ method: 'GET', url: `${CONFIG.API_URL}/api/list/dictionary`, onload: (response) => { // 检查响应是否匹配当前请求 if (dictionaryPanel.getAttribute('data-request-id') !== requestId) return; try { const dictList = JSON.parse(response.responseText); if (Array.isArray(dictList) && dictList.length > 0) { // 提取词典ID列表 const ids = dictList.map(dict => dict.id); // 递归调用,使用获取的词典ID列表 fetchDictionaryByGM(word, tabsContainer, entriesContainer, ids); } else { // 没有找到词典 showDictionaryStatus(entriesContainer, ''); } } catch (error) { console.error('获取词典列表失败:', error); showDictionaryStatus(entriesContainer, ''); } }, onerror: () => { // 检查响应是否匹配当前请求 if (dictionaryPanel.getAttribute('data-request-id') !== requestId) return; showDictionaryStatus(entriesContainer, ''); } }); return; } // 记录未完成的请求数 let pendingRequests = dictIds.length; // 添加MDICT内部标签切换函数 if (entriesContainer.parentNode && !entriesContainer.parentNode.querySelector('script[data-mdict-function]')) { const script = document.createElement('script'); script.setAttribute('data-mdict-function', 'true'); script.textContent = ` function onclickbtn_mdict_internal(_id) { tabPanes = document.querySelectorAll('.tab-widget_mdict_internal .tab-pane_mdict_internal'); tabButtons = document.querySelectorAll('.tab-widget_mdict_internal .tab-button_mdict_internal'); for (i = 0; i < tabButtons.length; i++) tabButtons[i].classList.remove('active'); for (i = 0; i < tabPanes.length; i++) tabPanes[i].classList.remove('active'); document.getElementById(_id).classList.add('active'); tabId = document.getElementById(_id).getAttribute('data-tab'); tabPane = document.getElementById(tabId); tabPane.classList.add('active'); } `; entriesContainer.parentNode.appendChild(script); } // 并行请求每个词典 dictIds.forEach(dictId => { GM_xmlhttpRequest({ method: 'GET', url: `${CONFIG.API_URL}/api/dictionary?id=${dictId}&word=${encodeURIComponent(word)}`, onload: (response) => { // 检查响应是否匹配当前请求 if (dictionaryPanel.getAttribute('data-request-id') !== requestId) return; try { const data = JSON.parse(response.responseText); // 隐藏加载提示 const loadingIndicator = entriesContainer.querySelector('.lunalens-dict-loading'); if (loadingIndicator) loadingIndicator.style.display = 'none'; // 添加词典条目 addDictionaryEntry(tabsContainer, entriesContainer, data); } catch (error) { console.error(`获取词典 ${dictId} 失败:`, error); } // 减少未完成请求计数 pendingRequests--; // 如果所有请求都完成了,但没有词典结果,则显示提示 if (pendingRequests === 0 && tabsContainer.children.length === 0) { showDictionaryStatus(entriesContainer, ''); } }, onerror: () => { // 检查响应是否匹配当前请求 if (dictionaryPanel.getAttribute('data-request-id') !== requestId) return; console.error(`获取词典 ${dictId} 失败`); // 减少未完成请求计数 pendingRequests--; // 如果所有请求都完成了,但没有词典结果,则显示提示 if (pendingRequests === 0 && tabsContainer.children.length === 0) { showDictionaryStatus(entriesContainer, ''); } } }); }); } // 显示词典状态信息 function showDictionaryStatus(container, message) { const loadingIndicator = container.querySelector('.lunalens-dict-loading'); if (loadingIndicator) { loadingIndicator.textContent = message; } else { container.innerHTML = `<div class="lunalens-dict-loading">${message}</div>`; } } // 添加词典条目 function addDictionaryEntry(tabsContainer, entriesContainer, data, isFirst) { // 如果没有词典名称,直接跳过 if (!data.name) return; const dictName = data.name; const dictId = `dict-${dictName.replace(/\s+/g, '-')}`; // 检查是否已有该词典结果 let entryDiv = document.getElementById(dictId); if (!entryDiv) { // 创建新词典条目 entryDiv = document.createElement('div'); entryDiv.className = 'lunalens-dict-entry'; if (isFirst || tabsContainer.children.length === 0) entryDiv.classList.add('active'); entryDiv.id = dictId; entryDiv.setAttribute('data-dict', dictName); entryDiv.innerHTML = `<div class="lunalens-dict-content">${data.result || ''}</div>`; entriesContainer.appendChild(entryDiv); // 添加词典标签 const tab = document.createElement('div'); tab.className = 'lunalens-dict-tab'; if (isFirst || tabsContainer.children.length === 0) tab.classList.add('active'); tab.textContent = dictName; tab.setAttribute('data-dict', dictName); tab.addEventListener('click', function() { // 更新标签状态 document.querySelectorAll('.lunalens-dict-tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); // 更新词典显示 const dictName = this.getAttribute('data-dict'); document.querySelectorAll('.lunalens-dict-entry').forEach(entry => { if (entry.getAttribute('data-dict') === dictName) { entry.classList.add('active'); } else { entry.classList.remove('active'); } }); }); tabsContainer.appendChild(tab); } else { // 更新现有词典内容 const contentDiv = entryDiv.querySelector('.lunalens-dict-content'); if (contentDiv) contentDiv.innerHTML = data.result || ''; } } // TTS朗读功能 function readText(text, element = null) { if (!text || !text.trim()) return false; stopReading(); // 检测设备类型并使用适当的播放方法 if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { // 在移动设备上直接使用Audio元素和原始URL playWithDirectUrl(text); } else { // 在桌面设备上使用ArrayBuffer加音频解码 fetchAndPlayTTS(text); } return true; } // 停止朗读 function stopReading() { // 取消所有待处理的TTS请求 pendingTTSRequests.forEach(request => { if (request && request.abort) { try { request.abort(); } catch(e) { console.log('取消TTS请求失败:', e); } } }); pendingTTSRequests = []; // 停止当前播放的音频 if (currentAudio) { try { currentAudio.pause(); if (currentAudio.src) { URL.revokeObjectURL(currentAudio.src); } // 释放资源 if (currentAudio.source && currentAudio.context) { try { currentAudio.source.stop(); currentAudio.context.close(); } catch(e) {} } } catch(e) { console.error('停止播放时出错:', e); } finally { currentAudio = null; } } } // 直接通过URL播放(适用于移动设备) function playWithDirectUrl(text) { const audio = new Audio(); audio.src = `${CONFIG.API_URL}/api/tts?text=${encodeURIComponent(text)}`; currentAudio = { element: audio, pause: function() { try { this.element.pause(); } catch(e) { console.error('停止播放失败:', e); } } }; audio.onended = () => { currentAudio = null; }; audio.onerror = (e) => { console.error('音频播放失败:', e); currentAudio = null; }; audio.play().catch(e => fetchAndPlayTTS(text)); } // 获取并播放TTS(适用于桌面设备) function fetchAndPlayTTS(text) { const request = GM_xmlhttpRequest({ method: 'GET', url: `${CONFIG.API_URL}/api/tts?text=${encodeURIComponent(text)}`, responseType: 'arraybuffer', timeout: CONFIG.TIMEOUT, onload: response => { // 从待处理列表中移除该请求 const index = pendingTTSRequests.indexOf(request); if (index > -1) { pendingTTSRequests.splice(index, 1); } if (response.status >= 200 && response.status < 300) { playAudioBlob(response.response); } else { console.error('TTS请求失败! 状态:', response.status); } }, onerror: error => { // 从待处理列表中移除该请求 const index = pendingTTSRequests.indexOf(request); if (index > -1) { pendingTTSRequests.splice(index, 1); } console.error('TTS请求失败:', error); }, ontimeout: () => { // 从待处理列表中移除该请求 const index = pendingTTSRequests.indexOf(request); if (index > -1) { pendingTTSRequests.splice(index, 1); } console.error('TTS请求超时'); } }); // 将请求添加到待处理列表 pendingTTSRequests.push(request); } // 播放音频数据(仅用于桌面设备) function playAudioBlob(arrayBuffer) { // 如果已经有音频在播放或者系统已停止,则不播放新音频 if (currentAudio) { return; } try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioContext.decodeAudioData(arrayBuffer, (buffer) => { const source = audioContext.createBufferSource(); source.buffer = buffer; source.connect(audioContext.destination); currentAudio = { source: source, context: audioContext, pause: function() { try { this.source.stop(); this.context.close(); } catch(e) {} } }; source.onended = () => { audioContext.close().catch(() => {}); currentAudio = null; }; source.start(0); }, () => console.error('音频播放失败') ); } catch (e) { console.error('音频播放失败:', e); playWithDirectUrl(text); } } })();