// ==UserScript== // @name 划词翻译发音:专注英语学习 // @namespace http://tampermonkey.net/ // @version 2.5.2 // @description 支持有道词典、微软翻译、谷歌翻译与 DeepSeek AI 翻译;集成 Edge TTS 发音引擎;内置笔记本和单词本(支持回收站与云端同步);兼容 PC 端和移动端。 // @author mini_soso // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @connect youdao.com // @connect dict.youdao.com // @connect translate.googleapis.com // @connect translate.google.com // @connect translate-pa.googleapis.com // @connect bing.com // @connect www.bing.com // @connect edge.microsoft.com // @connect api-edge.cognitive.microsofttranslator.com // @connect api.cognitive.microsofttranslator.com // @connect api.deepseek.com // @connect api.cloudflare.com // @connect iliili.qzz.io // @connect upstash.io // @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js // @license MIT // ==/UserScript== (function() { 'use strict'; const ICONS = { youdao: "data:image/png;base64,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", microsoft: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAAAsTAAALEwEAmpwYAAANeUlEQVR4nO1caVAUdx6dL1ugguAxUYQAasxWJZvsJpVNvmzigkRRBDUKgqAgzIpropVkk2iya5RDjFwCgyAw3IccAwKiKBJB5PYEAiiIRhQVCCo4KJf8trqHnum5u3umG9R+Ve8D5cyUvPfm1+//6xk4HBYsWLBgwYIFCxYsWLBgwYIFCxYstMRbqa4zF+XwXCyFvDhLoVe9ZS6vxzLXa2Sh0AsQWuKZg9FTymxPsJDhVrDIUqR5lgeYZyrhMYTu8CaeGRi3SJm+BcxwNE13GzFLdesxTdtcb5bqFmuW7uY8R+BpyHlZsDCP97ZlLi9+odBrcGEuD1AKecREz1EiuhLhzRGqEd1cqejqhN8sZpoiTVPdwDTVddA01VVgmu6yhDNVYZb9zTTLXF6wpdBrVCq87tJuTkPaVYouEV6WC1JcR0xTNgVaJLrrc6YSFgm9l1gKeU2vRtrdZJmC0RXlghRXMEneVGOe6mrCmQqwyPH8AJnvRNNu8ZKk3VROdJTJUpokudw1TXJ+fwok36vnVU77AhnhN0mZtAlMEl3uchMd50+K+MgctMjxuvY6pH0BTnRUeAldwCTB+ZJZtuM0xg2wzPEMfp3SvkBe+EQcE5x9GBV/4THe2xbZnqM6Ez3zJUl7onLOT3B+yugosszeGv/apj1RknpEeAnnJThHMXbCtcjaOji10u7GZNoVGe8M8wUbRXMEDvSfmC2ytrq8jGn/Z/4PcP5+E/hfyoB3MrdplXaJ6Cg3SjgvYeNGJgyIo5L2qJaiSU37u5nb4MaTe4BgcHQIIhoL4M8ZPGppx4uOUIDRKYYJA+qppB1BVHMR/WlPVj3b3z22DdomTEBQ332DetplhEfFh3kCxxraDTDP8uilMtsxhDYdJ5X2RRlbdTrbPX8NlfxfHgz2aZF2THQnmBcn5htxTt20G/DmMY9hKrMdj9DG4wppN0/fAutL/CG2tRgu97bDw2ePYHhsFH38s9FhuD3wEIo7L8H+i2nwaf5/VIru/msw7KtPUTnbzZJcobyrAVoe3QHHYj8t0i4VfkJ84MY5DtFuANUmI4/QxjxUeMt0D9hdm4AKTAbI+Nj8a5BC2tufdKH/7njmAOXZ/l7GNqh92Ao/VSdqFB1lrKOE9BtAsckoQ0pbKbQ86iQl/MDIM8jpuAD/Kg+D97K2y4yZvwt3SR4XdCUH/pb5b1hdtBe8y8Lh57pkCL2aCwktZyCvowrK7zXA+a5GlBe7b0BDbwfK5r470PusH32NsfEXsLtKACbIO0BBeKnojBpAtcloiztPe2BPbQIsTvdQWR/9LqYDHUAMUpZ2GcZsAG7MBgYMoNjbqWL0xRgEXRXCwjR3tb3dpmAP+u7A8Mfzfqh+0AJnO69Awa0aSL1eCtFNRcBvKEAZdi0PNSz4ihD9OaW1FLLayuH52LDkNYbGRuD8vUb0XaBcdKnwXOYMINnbJ5oLFfQNDYDDqf2EDku3+h/IPBcZO1SajGjkOfr83JsX4O3krWrTLsOjYtJvAMXeThZPhkVglf8D4VNqZls59A8Pooes2N9OgUm8C6Ums6MsAjzPBhNKOyY69+h6CWk3gEjalfV2MhiHcXA9e0irU6pFkht8kr0LVhT8BM6nA8D7XDh8VxkLfvXpEHQ5B/jX8lFGXMuHtsf34L6oD7aWBJNKO174udFi0m8AqVOqq4RkL3om2uxkBBuh/bG4jpJFbvsFwmnHCy/mF8wZoC7tyk6pZBHZWKjVKfWvGd5QcKta8npIwgtv1YBPXRp4lARB8OUc9AKPH3nC9gp0/BBNOyY6yigxaTeASNqVnVKpgN9YQCjtCnMdN9s/ztwJ76d7o73948yvIOSyEHaWR8LIC/Epu+PJfdhZFglmAhfSaccLPzkGkNjJUAUfMYHiakC+t0c3npB57aSWEjCNc6acdoxzUK6DOUfWMWAAgbQrW4ZpA35DAaG0azqlHrqYJfO6mTfKFERfkbcbbHK/Jyi8WHQ8GTCA2t0lbcFvKCCVdoRvJbnDR8e+hOV5e8DppD+ktpbKvOb1R3ehoKNawqJbtfBiHOlg4xDdUAgLYpyUp11OdAkjGTCASNqV9XZdgN+QrzLtsU2noO7hdbRS9j57InOBpYqa+y1q046JPidyLcyeIAMGULmBrZt9+zw1aUdWBuow8mIMHoj6ZFpR6GUh/FARC1+diwDPkmDwPCOlx+lAsM39UW3a8cKj5DNlgK7vpQq0m+1Ib1+cuBkqcCb83v8QctoqYE+lAJ3pJrFOYC6QXouQcaNstuMvqERFl3IN/QYwn3ZHjRtIjIgR/8j6Bt5J9lLaZLjRG9D5jiCmoYjUbFct/BopIxgxYPLT/gaJ1QC+ySxOkG5ld52L1DrtGGdFSEm/AVMk7VwCwjsV+aE3WVKaS+Db8mj47nyMxICP0naoEH4t4bTjhZ8V4QCzwh0YMoDBtHMpph3hitzdMDA8qHBBbuq9rZO0o6JPCI+RdgN0kXbzRDewP7EX9temoPuZ5r7f0eqIdHAEfc8HoP7hdfCtS4MlSe5anVI/TNsO9Q+uyxiwozRcJ2mXp3G4PUMGUEi7pD7GOUKX6A/CXfzxkAjcig9qTLu6JjM/egPwrxyHJ0MiOHK1QGdpF4vuAMZh9hLSb4AOZvvVnptABmPjL8CxyJfYTkZlk9Fx2nGiM2rAPJJpVzbbT96ug0fPB+DYjTL4sSoeXRM4FvmBV0kI7KtOhgtdTZK6iKFzoEe8GtBxb9cm7RIeRrgaJf0GUG0yuPr4SeZO6RbyqPLZjiQeub2Ix6aTAVMq7XjhjSbIgAHk0q5Nk/m5OknGgMCL2VMq7ZjoRqFS0m8A1cMShSbzWdY3MgZEXSvUmHaf6hTQBcgJbych7QaoF56K6OtVNhmkQuJx5FoBobT7VCXrxgANaccLbxRiBzNDmDBAh2n/IG078EpCIORSDiQ3l6AUNJ1CL8QbT/jBZ5lfy4gScTmP8Gzfr6UJRNKOiI4JL+YqBg3QIu1fFO6DugetGkVAVsh4HL4kJHVB1cYEImnHCz8zWEz6DdBitnOj1kPEleMKFZMoguqzSDeZ/ZXUTCCSdkx0PGk3QJvZfqBO8cOzj4eeQnLzGfA8HQRLM7+FD1O84dNjX4N78SFIaDot89iDtRmqRVfTZPZXJlEwQFnaV6kU3jBoJUr6DdBiJyN/m7DwZjUsFmxW2du5kV/IPN6vOpVyb6+/30rOAAJpxwvPoAHUdjLhV/JkfsGzv1+GuZHrNF5QsW/JINhXmUSpt285EUD6HUAk7TIMFJN2Awh/Tkaur1/tlt3/2OR8T+iUil8n772QSPqUujJ7NwxOfOKZlAHywgepE95WQoYMIH8vFVkxY0BSjaafr/mC2jP4WPI8f2QEkTilepz8BZ7JrTPIGqAu7ZjoBggPiUm/AUQ3kBOHJWy2I192wPB8dBjmoslfo7HJ3OnvljwvpD6bkPALo10gpakEtAGRtOOFF3MFMwZQ2UB2PZW9B+CQ9z9CTeYm7lPOR67kq10NWEa5QEB1Grpp1RZE0o6JLuEvDBhAJO3KZntZ51WZXxD52MhHKduViv4Gfx3wioOhoadD5jkJjcUKwi+JcQPeqSAoaKuSeZcRQWN3BwiuFik3gEDaMdExzmDcABIf19hxNlzhlxweG4XcGxXw34p42FXKB5+qFChsr4b+IcX7uAhO3qxVWA04Ht8PNfeaSQnf/ugefHk6DIyD7dCUB1SlKTFAc9ox0cVcDjMOLmfAgMi1w1T27XP5a6HirvpPr8kj9loR/NZ7W/JzU88tlauBD+J5sKcsFjUQEVg08YU95EveXQO9UNfVCuH1Qlie8R0YyV9cAxVNIJJ2THQJAz6n/4vas/lre0nf5JgYM2ZRTpDWfBa9xagKL8bHofT2JViR9T2a9HN3pKMLuaerdjUg19tnh9qTajIBlVITiKRdntMP2ND/pwpm8dfUaxZe+QUV6+1/ifeEr0sj0e1mUtNpyGw9B8F1WeB9OgTeOupKegOp9JSqTHQCTWZvebzYAE1pl00+TBeT/j/WMZu/JpZI2nVxd8mIRNqp9nZlsz2gMlVj2nGiS+lvc5R2A2bxHZzJpF256MTuLhnRnHbCs10iugrhDyC0gWn+nzvSbgA30tHAOGKN6FVJuwGB2a5adLHwKP2XiTj7lhpwmIBxuIPgdU/79AlOQ5NvA/r+y2I5TMEwzG7JrDD7kdc27QfEoku5bFjv4LJFHCZhHGYf+CqlfTqJtOOEF9PX+iCHaVgkLtU3Ory6Zsql/Re6044T3g+hdTUn3FaPMxngBq6ab3TYrnMqpX0G3Wn3k1LPz7prmr+1KWcyYRxm//7MkNV3dZ922ymYdjH1EfF9rTr/5GvzHmcqwCDcljszxO68LtNuMMXSro/R1xoRv3r6vqWT82frVSLcVs8w2M7HMGSV6OVM+zI1wlujwuv7Wg3r+VgHTNrMJ4LpgavmGwatijIMXiVSKfpLlHZ9hD5WIn1f6xi9fQxXTW3AjVxqMOPQyo2GQSuPGgTa1hoGruw2DLQdnvpptxrW87Xq1vO1rtX3tYrW97VyYuyEy4IFCxYsWLBgwYIFCxYsWLBgwYLzKuP/ix2SJ0OdDUIAAAAASUVORK5CYII=", google: "data:image/png;base64,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", deepseek: "data:image/png;base64,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", speaker: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEJUlEQVR4nO2ZX2xTVRzHr5EENbwaHxQfNPGNB/TFB4kvGmcC9JyWgkgUeTBGEjQBA/jC3TltgQEz6QDFGTJDMlEWgk6JxAwHATXojH+iEQJjjEE2GV0ZWEdXfudr7sq9Pe1u73rJ3WxJv8kvaU57e36f/n7n/H7n1DDqqquuuu56mWE8Jhh1CE5XJaOvEovwkFErkiHMk4wGJVewTXA6ZNSC4hxPC04juvN5o4xR7YqF8ZxkdMNxOlIMYVSzRAiLJKMxx+GlCpvW1ghAI8cKySlnOxp/SaHJVEgIfwAyhHmNDMuTDZg9c788x2rJiGwnN7+isHuHQvI9he1bKgeIR/GwHUHBqW1GnJccG3QHm1YptO1R2Nuq8MGuPESlACbDE4KTmvgsIzI5npw2x80o5ghGLbpzzW8odLQD+9sV9rUVIPykkOB00Pk8o2/9OfUsZokQ1ktOPZJR9nYor0uGpfkvx/Pu26NCy9sKh78AOg8BBw8UQ/gBMK3ix+lmAQJPVe48p25X51bTpb5efLhnHZ11e7/1XYXuo0DXN8DXhydDlAMwo5gjOe20zHqtRaFNi8LuO8pl27YsVzjZCVzsB74/AmxdkR9PLFPY9ZbCl/uAUz8A350Ajne7Q5QDiHGscao0I+mMMzyjFb+0uRL3VQBAPfZDXe1AahgYSQHDV4ChQeDypTxE33mg9xxw5jTw5x/Ab78CP/d4Q5QDkByLNYAr9tYJA/dIXoi2CGHB1AC3c96y0TRwfRS4lg4GohzAgSjuFZzOFyDwovaDthaew4YKIlCYJJMB/rkRHITXIpacmrQotDjjIbympdHnvgDGxhAohBeACOMFrVs94fgTwXwN4BdfANksAoXwAohb1bew4wza47EQ5mrjA74AcjkECuEFkGzAbC0CN+3x5iju11LrX18At24hUAgvgOYyjpoL8YCvc0QpQJAQXgAmxyNuKWRG8agWmYu+AIgQKIQXQIyjwW0RW43cHS9iCyBICM9diNM2t220kWOVr7O0Xshy4wgUIr7ESZFsaSGTnPrcCplg9JFWiddPCSAY/WQ/cGw/MkFCdCQVYmHKCU6bp62VEBNt9ORmrpzFIpTd+SYN/XgE4xVF4i80ls4Z05o5yUlovizw3cxZ7bTkdMwPhG2fJCiTTk0BcXYygBlkO21DWJGYSCdtTVRiezfS6MiwB0TvZAA3xRgeL5o7gvnGTB0p319D6atD7hD9/ZUBCP1IyalrWpwvmpBhkw6RfJ1GhgaKIQYuAEc/xcd6mvzvh/rSxehMzBW2r6TU5b4CROs7Tj6frLprFVuS41X9Ymvry5S6cBpkQciwU5CUUY0XW+WuFhPL6Nq53zHuVYmr/nI3HtEuemsBoOav1++KPzhKbtk+k4z+lpw6E1E86LxZV1111WXUsv4DqNalNBhQdZAAAAAASUVORK5CYII=" }; const WECHAT_REWARD_QR_BASE64 = 'data:image/png;base64,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'; const DEFAULT_DEEPSEEK_PROMPT_WORD = `你是一位专业的电子词典与词汇辅导专家。请将用户输入的单词翻译为简体中文,并提供系统化的词汇解析以辅助高效记忆。 请严格按以下 **4 个部分** 输出结构化内容: --- ### 1. 简明释义 提供单词的标准发音(音标)、常见词性及精准中文释义。 ### 2. 常用搭配 提取 2-3 个高频固定搭配或习惯用语,并附带简短中文翻译。 ### 3. 词性拓展与近反义词 * **衍生词/同根词**:提供相关词性的拓展词汇(如动词变名词、形容词等) * **近义词 / 反义词**:各列举 1-2 个常用词汇,便于对比记忆 ### 4. 实用例句 提供 1-2 个地道、简短的例句,包含: * 英文原句 * 对应中文译文 --- **输出要求:** - 排版清晰,使用 Markdown 语法(标题、列表、表格、加粗等)增强可读性 - 直接输出结果,不要使用代码块(\`\`\`)包裹整个输出`; const DEFAULT_DEEPSEEK_PROMPT_SENTENCE = `你是一位专业的外语私教。请将用户输入的句子翻译为简体中文,并进行深度解析以辅助学习。 请严格按以下 **4 个部分** 输出结构化内容: --- ### 1. 准确翻译 提供自然、地道的译文,必要时可附上直译对照。 ### 2. 语法分析 - 拆解句子核心成分(主谓宾、从句、修饰关系等) - 解释时态、语态、虚拟语气等关键语法点 - 如有特殊句型(倒装、强调、省略等),单独说明 ### 3. 核心句型 提取可复用的句型或固定搭配,并给出: - 句型结构公式 - 一个同结构例句 ### 4. 核心单词 提取 1-3 个生词或重点词汇,每个词包含: - **词性**(如 v. / n. / adj.) - **释义**(中文解释) - **高频搭配**(1-2 个常用短语) - **例句**(简短实用) --- **输出要求:** - 排版清晰,使用 Markdown 语法(标题、列表、表格、加粗等)增强可读性 - 直接输出结果,不要使用代码块(\`\`\`)包裹整个输出 - 如用户输入非完整句子(如单词或短语),可适当调整分析深度`; const DEFAULT_DEEPSEEK_PROMPT_CHAT = `你是一位博学的通用 AI 知识助手,同时擅长语言学习辅导。 请针对用户的问题提供清晰、简明且全面的解答。 **输出要求:** - 如果问题涉及**外语学习、翻译、语法、词汇、写作**等语言相关主题,请主动提供: 1. **核心解答**(直接回答用户问题) 2. **语言点解析**(如有必要,解释相关语法、词义、搭配等) 3. **实用例句**(提供 1-2 个地道例句及中文翻译) 4. **扩展知识**(如近义词辨析、常见错误、文化背景等,可选) - 如果问题属于**一般知识类**(如历史、科学、技术等),按常规方式清晰作答,可适当列出要点或分步骤说明。 - **排版要求**: - 使用 Markdown 语法(标题 # ## ###、列表 - 1.、加粗 **、表格 | 等)增强可读性。 - 直接输出结果,**不要使用代码块(\`\`\`)包裹整个输出**,只对代码片段或公式可使用行内代码或单行代码块。 - 避免空泛的套话(如“好的,现在我来回答”),直接进入主题。 `; const ALL_ENGINES = [ { id: 'youdao', label: '有道词典' }, { id: 'microsoft', label: '微软翻译' }, { id: 'google', label: '谷歌翻译' }, { id: 'deepseek', label: 'DeepSeek 翻译' } ]; const LANG_MAP = { 'en': '英文', 'zh-Hans': '中文 (简体)', 'zh-CN': '中文 (简体)', 'zh-TW': '中文 (繁体)', 'zh-Hant': '中文 (繁体)', 'ja': '日语', 'ko': '韩语', 'fr': '法语', 'de': '德语', 'es': '西班牙语', 'ru': '俄语', 'it': '意大利语', 'pt': '葡萄牙语' }; const DICTIONARY_SOURCES = [ { id: 'oxford', label: '牛津词典', url: word => `https://www.oxfordlearnersdictionaries.com/definition/english/${encodeURIComponent(word)}` }, { id: 'merriam', label: '韦氏词典', url: word => `https://www.merriam-webster.com/dictionary/${encodeURIComponent(word)}` }, { id: 'cambridge', label: '剑桥词典', url: word => `https://dictionary.cambridge.org/dictionary/english/${encodeURIComponent(word)}` }, { id: 'collins', label: '柯林斯词典', url: word => `https://www.collinsdictionary.com/dictionary/english/${encodeURIComponent(word)}` }, { id: 'longman', label: '朗文词典', url: word => `https://www.ldoceonline.com/dictionary/${encodeURIComponent(word)}` } ]; const TYPEWRITER_SPEED_MAP = { 'none': { label: '无打字效果', value: null }, 'very_slow': { label: '很慢 (60ms)', value: 60 }, 'slow': { label: '慢速 (40ms)', value: 40 }, 'normal': { label: '正常 (20ms)', value: 20 }, 'fast': { label: '快速 (10ms)', value: 10 }, 'very_fast': { label: '很快 (5ms)', value: 5 }, 'super_fast': { label: '极速 (2ms)', value: 2 } }; const TTS_SPEED_MAP = { 'very_slow': { label: '很慢 (0.5x)', value: '0.5' }, 'slow': { label: '慢速 (0.75x)', value: '0.75' }, 'normal': { label: '正常 (1x)', value: '1' }, 'fast': { label: '快速 (1.25x)', value: '1.25' }, 'very_fast': { label: '很快 (1.5x)', value: '1.5' }, 'super_fast': { label: '极速 (2x)', value: '2' } }; const API_URL = 'https://youdao.com/result?word='; const DEFAULT_TYPEWRITER_SPEED_KEY = 'fast'; const DEFAULT_TTS_API = 'https://tts.iliili.qzz.io/v1/audio/speech'; const youdaoAudioCache = new Map(); const googleAudioCache = new Map(); const translationCache = new Map(); const MAX_CACHE_SIZE = 50; const wordDictCache = new Map(); const EDGE_TTS_CACHE_LIMIT = 15 const EDGE_TTS_CACHE_MAX_DURATION = 60; const edgeTtsCache = new Map(); const edgeTtsCacheOrder = []; const NOTES_STORAGE_KEY = 'notes'; const UPSTASH_NOTES_KEY = 'notes'; const MAX_RETRY = 3; const WORD_SYNC_STATUS_KEY = 'word_sync_success'; const NOTE_SYNC_STATUS_KEY = 'note_sync_success'; window.__translookup_confirm_showing = false; let currentPageType = 'idle'; let pendingRequests = new Map(); let webAudioCtx = null; let webAudioSource = null; let webAudioBuffer = null; let currentPreciseHighlightCleanup = null; function unwrapAllTtsSpans(container) { if (!container) return; const existing = container.querySelectorAll('.tts-read-token'); if (!existing.length) return; existing.forEach(sp => { if (!sp.parentNode) return; const parent = sp.parentNode; while (sp.firstChild) { parent.insertBefore(sp.firstChild, sp); } parent.removeChild(sp); }); if (container.normalize) container.normalize(); } function findTtsHighlightContainer(btn, text) { if (!btn || !text) return null; const scope = btn.closest('.gt-section') || btn.closest('.gt-card'); if (!scope) return null; const candidates = scope.querySelectorAll('.gt-text-source, .gt-text-target'); const needle = text.trim(); for (const el of candidates) { if (el.textContent.trim() === needle) return el; } return null; } function customConfirm(message) { return new Promise((resolve) => { window.__translookup_confirm_showing = true; const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2147483647; display: flex; align-items: center; justify-content: center; animation: fadeIn 0.15s ease; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: #ffffff; padding: 20px 24px; border-radius: 14px; max-width: 300px; width: 85%; box-shadow: 0 8px 30px rgba(0,0,0,0.2); font-family: system-ui, -apple-system, sans-serif; color: #0f1115; animation: scaleIn 0.15s ease; `; const msg = document.createElement('p'); msg.textContent = message; msg.style.cssText = 'margin: 0 0 18px 0; font-size: 15px; line-height: 1.5; text-align: center;'; const btnContainer = document.createElement('div'); btnContainer.style.cssText = 'display: flex; justify-content: center; gap: 12px;'; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.style.cssText = ` padding: 8px 20px; border: 1px solid #dadce0; border-radius: 8px; background: transparent; cursor: pointer; font-size: 14px; font-weight: 500; min-width: 60px; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; `; const confirmBtn = document.createElement('button'); confirmBtn.textContent = '确定'; confirmBtn.style.cssText = ` padding: 8px 20px; border: none; border-radius: 8px; background: #1a1d21; color: #ffffff; cursor: pointer; font-size: 14px; font-weight: 500; min-width: 60px; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; `; const style = document.createElement('style'); style.textContent = ` @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes scaleIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } } `; document.head.appendChild(style); btnContainer.appendChild(cancelBtn); btnContainer.appendChild(confirmBtn); dialog.appendChild(msg); dialog.appendChild(btnContainer); overlay.appendChild(dialog); document.body.appendChild(overlay); const cleanup = (result) => { if (overlay.parentNode) overlay.remove(); if (style.parentNode) style.remove(); window.__translookup_confirm_showing = false; resolve(result); }; cancelBtn.addEventListener('click', function(e) { e.stopPropagation(); cleanup(false); }); confirmBtn.addEventListener('click', function(e) { e.stopPropagation(); cleanup(true); }); }); } const GoogleService = (function() { const CONFIG = { transMinInterval: 400, maxTransRateLimitRetries: 3, audioMinInterval: 400, maxAudioRateLimitRetries: 3, cooldownDuration: 30000, timeout: 8000, audioCacheLimit: 30, ttsPrefetchCount: 2, ttsGap: 0, ttsMaxChars: 190, ttsTrimTail: true, ttsTrimHead: false, ttsSilenceWindowMs: 5, ttsSilenceFloor: 0.0005, ttsSilenceThresholdRatio: 0.02, ttsSilenceMinMs: 40, ttsSilenceSafetyMs: 120, ttsInitialScheduleMs: 30, ttsDecodedCacheLimit: 8, ttsDebug: true, }; const state = { transCache: new Map(), transPending: new Map(), transQueue: [], transProcessing: false, transLastTime: 0, transRateLimitCount: 0, transCooldownUntil: 0, audioCache: new Map(), audioPending: new Map(), audioQueue: [], audioProcessing: false, audioLastTime: 0, audioCooldownUntil: 0, audioCacheOrder: [], audioDecodedCache: new Map(), audioDecodedCacheOrder: [], audioDecodedPending: new Map(), }; function loadCooldown() { try { const audioCooldown = Number(GM_getValue('google_audio_cooldown_until', 0)); const transCooldown = Number(GM_getValue('google_trans_cooldown_until', 0)); state.audioCooldownUntil = audioCooldown || 0; state.transCooldownUntil = transCooldown || 0; } catch (e) { state.audioCooldownUntil = 0; state.transCooldownUntil = 0; } } function saveCooldown() { try { GM_setValue('google_audio_cooldown_until', state.audioCooldownUntil); GM_setValue('google_trans_cooldown_until', state.transCooldownUntil); } catch (e) {} } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function now() { return Date.now(); } function makeKey(text, lang, trimHead, trimTail) { return `${text || ''}|||${lang || ''}|||${!!trimHead}|||${!!trimTail}`; } function debug(...args) { if (CONFIG.ttsDebug) console.log('[Google TTS]', ...args); } loadCooldown(); function doTranslation(text, tl) { return new Promise((resolve, reject) => { const primaryUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${encodeURIComponent(tl)}&dt=t&dt=rm&q=${encodeURIComponent(text)}`; function fallbackRequest() { return new Promise((res, rej) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://translate-pa.googleapis.com/v1/translateHtml', headers: { 'Content-Type': 'application/json+protobuf', 'X-Goog-API-Key': 'AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', }, data: JSON.stringify([ [ [text], 'auto', tl ], 'wt_lib' ]), timeout: CONFIG.timeout, onload(resp) { if (resp.status >= 200 && resp.status < 300) { try { const data = JSON.parse(resp.responseText); let translation = null; if (data && typeof data === 'object') { if (data.translatedText) translation = data.translatedText; else if (data.data && data.data.translations && data.data.translations.length) translation = data.data.translations[0].translatedText; else if (data.translations && data.translations.length) translation = data.translations[0].translatedText; else if (Array.isArray(data)) { for (const item of data) { if (typeof item === 'string') { translation = item; break; } if (Array.isArray(item)) { for (const sub of item) { if (typeof sub === 'string') { translation = sub; break; } else if (Array.isArray(sub) && sub.length && typeof sub[0] === 'string') { translation = sub[0]; break; } } if (translation) break; } } } } if (translation) { let srcLang = 'auto'; if (Array.isArray(data) && data.length > 1) { const maybeLang = data[1]; if (Array.isArray(maybeLang) && maybeLang.length > 0 && typeof maybeLang[0] === 'string') { srcLang = maybeLang[0]; } else if (typeof maybeLang === 'string') { srcLang = maybeLang; } } res({ word: text, translation, pinyin: '', srcPinyin: '', srcLang: srcLang, apiSource: 'fallback' }); } else { rej({ code: 'PARSE_ERROR', source: 'fallback', message: '备用API解析失败' }); } } catch (e) { rej({ code: 'PARSE_ERROR', source: 'fallback', message: '备用API数据解析异常' }); } return; } if (resp.status === 429) { rej({ code: 'RATE_LIMIT', source: 'fallback', status: 429 }); return; } if (resp.status >= 500) { rej({ code: 'SERVER_ERROR', source: 'fallback', status: resp.status }); return; } rej({ code: 'HTTP_ERROR', source: 'fallback', status: resp.status }); }, ontimeout() { rej({ code: 'NETWORK_ERROR', source: 'fallback', message: '备用API超时' }); }, onerror() { rej({ code: 'NETWORK_ERROR', source: 'fallback', message: '备用API网络错误' }); } }); }); } GM_xmlhttpRequest({ method: 'GET', url: primaryUrl, timeout: CONFIG.timeout, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': 'https://translate.google.com/', 'Accept': 'application/json, text/plain, */*' }, onload(res) { if (res.status >= 200 && res.status < 300) { try { const data = JSON.parse(res.responseText); let translation = '', pinyin = '', srcPinyin = ''; if (data && data[0] && Array.isArray(data[0])) { translation = data[0].filter(item => Array.isArray(item) && typeof item[0] === 'string').map(item => item[0]).join(''); const rmChunk = data[0][data[0].length - 1]; if (rmChunk && Array.isArray(rmChunk)) { if (typeof rmChunk[2] === 'string') pinyin = rmChunk[2]; if (typeof rmChunk[3] === 'string') srcPinyin = rmChunk[3]; } } const srcLang = (data && data[2]) ? data[2] : 'auto'; if (translation) { resolve({ word: text, translation, pinyin, srcPinyin, srcLang, apiSource: 'primary' }); } else { fallbackRequest().then(resolve).catch(reject); } } catch (e) { fallbackRequest().then(resolve).catch(reject); } return; } if (res.status === 429) { fallbackRequest().then(resolve).catch(reject); return; } if (res.status >= 500) { fallbackRequest().then(resolve).catch(reject); return; } reject({ code: 'HTTP_ERROR', source: 'primary', status: res.status, message: `Primary 返回 ${res.status}` }); }, ontimeout() { fallbackRequest().then(resolve).catch(reject); }, onerror() { fallbackRequest().then(resolve).catch(reject); } }); }); } function processTranslationQueue() { if (state.transProcessing) return; if (!state.transQueue.length) return; if (Date.now() < state.transCooldownUntil) { const queue = state.transQueue; state.transQueue = []; const err = new Error('Google 翻译服务冷却中,请稍后再试'); err.code = 'COOLDOWN'; queue.forEach(item => item.reject(err)); return; } const now = Date.now(); const elapsed = now - state.transLastTime; if (elapsed < CONFIG.transMinInterval) { setTimeout(processTranslationQueue, CONFIG.transMinInterval - elapsed); return; } state.transProcessing = true; const item = state.transQueue.shift(); const key = `${item.text}:${item.tl}`; state.transLastTime = Date.now(); doTranslation(item.text, item.tl) .then(result => { state.transRateLimitCount = 0; state.transProcessing = false; state.transPending.delete(key); state.transCache.set(key, result); item.resolve(result); processTranslationQueue(); }) .catch(err => { if (err && err.code === 'RATE_LIMIT') { state.transRateLimitCount++; if (state.transRateLimitCount >= CONFIG.maxTransRateLimitRetries) { state.transPending.delete(key); state.transCooldownUntil = Date.now() + CONFIG.cooldownDuration; saveCooldown(); item.reject(new Error('Google 翻译服务已冷却,请 30 秒后重试')); while (state.transQueue.length) { const q = state.transQueue.shift(); q.reject(new Error('Google 翻译服务冷却中')); } state.transProcessing = false; return; } const delay = Math.min(1000 * Math.pow(2, state.transRateLimitCount), 5000); state.transQueue.unshift(item); state.transProcessing = false; setTimeout(processTranslationQueue, delay); return; } state.transPending.delete(key); state.transProcessing = false; let msg = err && err.message ? err.message : '翻译失败'; if (err && err.code === 'SERVER_ERROR') msg = `Google 服务异常 (${err.status})`; else if (err && err.code === 'NETWORK_ERROR') msg = '网络连接失败,请检查网络'; else if (err && err.code === 'PARSE_ERROR') msg = '翻译结果解析失败'; else if (err && err.code === 'HTTP_ERROR') msg = `请求失败 (${err.status})`; item.reject(new Error(msg)); processTranslationQueue(); }); } function translate(text, tl) { return new Promise((resolve, reject) => { text = String(text || '').trim(); tl = String(tl || '').trim(); if (!text) { reject(new Error('翻译文本为空')); return; } const key = `${text}:${tl}`; if (state.transCache.has(key)) { resolve(state.transCache.get(key)); return; } if (Date.now() < state.transCooldownUntil) { const err = new Error('Google 翻译服务冷却中,请稍后再试'); err.code = 'COOLDOWN'; reject(err); return; } if (state.transPending.has(key)) { state.transPending.get(key).then(resolve).catch(reject); return; } const promise = new Promise((res, rej) => { state.transQueue.push({ text, tl, resolve: res, reject: rej }); }); state.transPending.set(key, promise); processTranslationQueue(); promise.then(resolve).catch(reject); }); } function touchAudioCache(key) { const order = state.audioCacheOrder; const index = order.indexOf(key); if (index !== -1) order.splice(index, 1); order.push(key); } function setAudioCache(key, blob) { const order = state.audioCacheOrder; if (state.audioCache.has(key)) { state.audioCache.set(key, blob); touchAudioCache(key); return; } while (order.length >= CONFIG.audioCacheLimit) { const oldest = order.shift(); if (oldest) state.audioCache.delete(oldest); } order.push(key); state.audioCache.set(key, blob); } function getAudioCache(key) { if (!state.audioCache.has(key)) return null; const blob = state.audioCache.get(key); touchAudioCache(key); return blob; } function doAudio(text, lang) { return new Promise((resolve, reject) => { const url = `https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=${encodeURIComponent(lang)}&q=${encodeURIComponent(text)}`; GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', timeout: CONFIG.timeout, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Referer': 'https://translate.google.com/', 'Accept': 'application/json, text/plain, */*' }, onload(res) { if (res.status === 429) { reject({ code: 'RATE_LIMIT', status: 429 }); return; } if (res.status >= 200 && res.status < 300 && res.response && res.response.size > 0) { resolve(res.response); return; } if (res.status >= 500) { reject({ code: 'SERVER_ERROR', status: res.status, message: `Google TTS 服务异常 (${res.status})` }); return; } reject({ code: 'HTTP_ERROR', status: res.status, message: `Google TTS 请求失败 (${res.status})` }); }, ontimeout() { reject({ code: 'NETWORK_ERROR', message: 'Google TTS 请求超时' }); }, onerror() { reject({ code: 'NETWORK_ERROR', message: 'Google TTS 网络连接失败' }); } }); }); } function processAudioQueue() { if (state.audioProcessing) return; if (!state.audioQueue.length) return; if (Date.now() < state.audioCooldownUntil) { const queue = state.audioQueue; state.audioQueue = []; const err = new Error('Google TTS 服务冷却中,请稍后再试'); err.code = 'COOLDOWN'; queue.forEach(item => item.reject(err)); return; } const now = Date.now(); const elapsed = now - state.audioLastTime; if (elapsed < CONFIG.audioMinInterval) { setTimeout(processAudioQueue, CONFIG.audioMinInterval - elapsed); return; } state.audioProcessing = true; const item = state.audioQueue.shift(); state.audioLastTime = Date.now(); if (typeof item.retryCount !== 'number') item.retryCount = 0; doAudio(item.text, item.lang) .then(blob => { state.audioProcessing = false; item.resolve(blob); processAudioQueue(); }) .catch(err => { if (err && err.code === 'RATE_LIMIT') { item.retryCount++; if (item.retryCount >= CONFIG.maxAudioRateLimitRetries) { state.audioCooldownUntil = Date.now() + CONFIG.cooldownDuration; saveCooldown(); item.reject({ code: 'COOLDOWN', message: 'Google TTS 服务已冷却,请 30 秒后重试' }); while (state.audioQueue.length) { const q = state.audioQueue.shift(); q.reject({ code: 'COOLDOWN', message: 'Google TTS 服务冷却中' }); } state.audioProcessing = false; return; } const delay = Math.min(1000 * Math.pow(2, item.retryCount), 5000); state.audioQueue.unshift(item); state.audioProcessing = false; setTimeout(processAudioQueue, delay); return; } state.audioProcessing = false; item.reject(err); processAudioQueue(); }); } function _getAudioBlob(text, lang) { return new Promise((resolve, reject) => { if (!text || !String(text).trim()) { resolve(null); return; } text = String(text).trim(); lang = String(lang || 'en'); const key = `google_${lang}_${text}`; const cached = getAudioCache(key); if (cached) { resolve(cached); return; } if (state.audioPending.has(key)) { state.audioPending.get(key).then(resolve).catch(reject); return; } if (Date.now() < state.audioCooldownUntil) { const err = new Error('Google TTS 服务冷却中,请稍后重试'); err.code = 'COOLDOWN'; reject(err); return; } const promise = new Promise((res, rej) => { const item = { text, lang, retryCount: 0, resolve(blob) { state.audioPending.delete(key); setAudioCache(key, blob); res(blob); }, reject(err) { state.audioPending.delete(key); rej(err); } }; state.audioQueue.push(item); }); state.audioPending.set(key, promise); processAudioQueue(); promise.then(resolve).catch(reject); }); } let ttsAudioContext = null; let ttsScheduledSources = new Set(); let ttsPlaybackStartTime = 0; let ttsPlaybackEndTime = 0; function _getAudioContext() { if (!ttsAudioContext) { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!AudioContextClass) throw new Error('Web Audio API is not supported'); ttsAudioContext = new AudioContextClass(); } return ttsAudioContext; } function _stopScheduledSources() { for (const source of ttsScheduledSources) { try { source.onended = null; } catch (e) {} try { source.stop(0); } catch (e) {} try { source.disconnect(); } catch (e) {} } ttsScheduledSources.clear(); ttsPlaybackStartTime = 0; ttsPlaybackEndTime = 0; } let _stopCurrentAudioOriginal = null; let _stopCurrentAudioPatched = false; function _stopGoogleTts() { _stopScheduledSources(); try { if (typeof currentAudio !== 'undefined' && currentAudio) { try { currentAudio.pause(); } catch (e) {} try { currentAudio.currentTime = 0; } catch (e) {} try { if (currentAudio.blobUrl) URL.revokeObjectURL(currentAudio.blobUrl); } catch (e) {} try { currentAudio.src = ''; } catch (e) {} } } catch (e) {} try { window.currentAudio = null; } catch (e) {} try { window.currentAudioKey = null; } catch (e) {} if (typeof currentTtsCancel === 'function') { try { currentTtsCancel(); } catch (e) {} currentTtsCancel = null; } //debug('Google TTS stopped'); } function _patchStopCurrentAudio() { if (_stopCurrentAudioPatched) return; if (typeof window.stopCurrentAudio === 'function') { _stopCurrentAudioOriginal = window.stopCurrentAudio; window.stopCurrentAudio = function() { if (_stopCurrentAudioOriginal) { _stopCurrentAudioOriginal.apply(this, arguments); } _stopGoogleTts(); }; _stopCurrentAudioPatched = true; debug('stopCurrentAudio 已代理'); } else { setTimeout(_patchStopCurrentAudio, 200); } } _patchStopCurrentAudio(); function touchDecodedCache(key) { const index = state.audioDecodedCacheOrder.indexOf(key); if (index !== -1) state.audioDecodedCacheOrder.splice(index, 1); state.audioDecodedCacheOrder.push(key); } function setDecodedCache(key, value) { state.audioDecodedCache.set(key, value); touchDecodedCache(key); while (state.audioDecodedCacheOrder.length > CONFIG.ttsDecodedCacheLimit) { const oldKey = state.audioDecodedCacheOrder.shift(); if (oldKey !== undefined) state.audioDecodedCache.delete(oldKey); } } function getDecodedCache(key) { const value = state.audioDecodedCache.get(key); if (!value) return null; touchDecodedCache(key); return value; } function _analyzeSilence(audioBuffer, trimHead, trimTail) { const sampleRate = audioBuffer.sampleRate; const length = audioBuffer.length; const durationMs = audioBuffer.duration * 1000; if (!length) { return { startOffsetSec: 0, durationSec: audioBuffer.duration, leadingSilenceMs: 0, trailingSilenceMs: 0 }; } const numberOfChannels = audioBuffer.numberOfChannels; let peak = 0; for (let channel = 0; channel < numberOfChannels; channel++) { const data = audioBuffer.getChannelData(channel); const step = 32; for (let i = 0; i < data.length; i += step) { const value = Math.abs(data[i]); if (value > peak) peak = value; } } if (peak <= 0) { return { startOffsetSec: 0, durationSec: audioBuffer.duration, leadingSilenceMs: durationMs, trailingSilenceMs: 0 }; } const threshold = Math.max(CONFIG.ttsSilenceFloor, peak * CONFIG.ttsSilenceThresholdRatio); const windowSamples = Math.max(1, Math.floor(sampleRate * CONFIG.ttsSilenceWindowMs / 1000)); const minSilentWindows = Math.max(1, Math.ceil(CONFIG.ttsSilenceMinMs / CONFIG.ttsSilenceWindowMs)); let leadingSilenceMs = 0; let startOffsetSec = 0; if (trimHead) { let found = false; let firstNonSilentSample = 0; for (let i = 0; i < length; i += windowSamples) { const windowEnd = Math.min(length, i + windowSamples); let windowPeak = 0; for (let channel = 0; channel < numberOfChannels; channel++) { const data = audioBuffer.getChannelData(channel); for (let j = i; j < windowEnd; j++) { const value = Math.abs(data[j]); if (value > windowPeak) windowPeak = value; } if (windowPeak > threshold) break; } if (windowPeak > threshold) { firstNonSilentSample = i; found = true; break; } } if (found) { startOffsetSec = firstNonSilentSample / sampleRate; leadingSilenceMs = startOffsetSec * 1000; } } let trailingSilenceMs = 0; let effectiveEndSample = length; if (trimTail) { let silentWindows = 0; let lastNonSilentSample = length - 1; for (let windowEnd = length; windowEnd > 0; windowEnd -= windowSamples) { const windowStart = Math.max(0, windowEnd - windowSamples); let windowPeak = 0; for (let channel = 0; channel < numberOfChannels; channel++) { const data = audioBuffer.getChannelData(channel); for (let i = windowStart; i < windowEnd; i++) { const value = Math.abs(data[i]); if (value > windowPeak) windowPeak = value; if (windowPeak > threshold) break; } if (windowPeak > threshold) break; } if (windowPeak <= threshold) { silentWindows++; continue; } lastNonSilentSample = windowEnd - 1; break; } trailingSilenceMs = (length - 1 - lastNonSilentSample) / sampleRate * 1000; if (silentWindows >= minSilentWindows) { effectiveEndSample = Math.min(length, lastNonSilentSample + 1 + Math.ceil(sampleRate * CONFIG.ttsSilenceSafetyMs / 1000)); } } let effectiveDurationSec = effectiveEndSample / sampleRate; effectiveDurationSec = Math.max(0.05, Math.min(audioBuffer.duration, effectiveDurationSec)); if (trimHead) { effectiveDurationSec = Math.max(0.05, effectiveDurationSec - startOffsetSec); } return { startOffsetSec, durationSec: effectiveDurationSec, leadingSilenceMs, trailingSilenceMs, originalDurationSec: audioBuffer.duration }; } async function _decodeAudioBlob(blob) { const context = _getAudioContext(); const arrayBuffer = await blob.arrayBuffer(); let audioBuffer; try { audioBuffer = await context.decodeAudioData(arrayBuffer); } catch (error) { debug('decodeAudioData failed:', error); throw error; } return audioBuffer; } async function _getDecodedAudio(text, lang, trimHead = false, trimTail = true) { const key = makeKey(text, lang, trimHead, trimTail); const cached = getDecodedCache(key); if (cached) return cached; if (state.audioDecodedPending.has(key)) return state.audioDecodedPending.get(key); const promise = (async () => { const blob = await _getAudioBlob(text, lang); const audioBuffer = await _decodeAudioBlob(blob); const analysis = _analyzeSilence(audioBuffer, trimHead, trimTail); const result = { audioBuffer, ...analysis }; setDecodedCache(key, result); return result; })(); state.audioDecodedPending.set(key, promise); try { return await promise; } finally { state.audioDecodedPending.delete(key); } } function _splitText(text, maxLen = CONFIG.ttsMaxChars) { text = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim(); if (!text) return []; const abbreviations = new Set([ 'mr.', 'mrs.', 'ms.', 'dr.', 'prof.', 'rev.', 'hon.', 'capt.', 'lt.', 'col.', 'gen.', 'sgt.', 'cpl.', 'sr.', 'jr.', 'st.', 'mt.', 'vs.', 'ph.d.', 'm.d.', 'd.d.s.', 'd.v.m.', 'j.d.', 'm.a.', 'm.s.', 'b.a.', 'b.s.', 'co.', 'corp.', 'inc.', 'ltd.', 'llc.', 'etc.', 'e.g.', 'i.e.', 'et al.', 'viz.', 'cf.', 'u.s.', 'u.k.', 'u.n.', 'e.u.', 'u.a.e.', 'in.', 'ft.', 'yd.', 'oz.', 'lb.', 'pt.', 'qt.', 'no.', 'vol.', 'p.', 'pp.', 'approx.', 'a.m.', 'p.m.', 'jan.', 'feb.', 'mar.', 'apr.', 'may.', 'jun.', 'jul.', 'aug.', 'sep.', 'sept.', 'oct.', 'nov.', 'dec.', 'mon.', 'tue.', 'tues.', 'wed.', 'thu.', 'thurs.', 'fri.', 'sat.', 'sun.' ]); function isSentenceEndAt(index) { const char = text[index]; if (char !== '.' && char !== '?' && char !== '!' && char !== '。' && char !== '?' && char !== '!') return false; if (char !== '.') { return true; } let left = index - 1; while (left >= 0 && /[A-Za-z.]/.test(text[left])) left--; let right = index + 1; while (right < text.length && /[A-Za-z.]/.test(text[right])) right++; const token = text.slice(left + 1, right).trim(); if (!token) return true; if (abbreviations.has(token.toLowerCase())) { return false; } if (index + 1 < text.length && /\d/.test(text[index + 1])) { return false; } return true; } const sentences = []; let start = 0; for (let i = 0; i < text.length; i++) { if (!isSentenceEndAt(i)) continue; let end = i + 1; while (end < text.length && /\s/.test(text[end])) end++; const sentence = text.slice(start, end).trim(); if (sentence) sentences.push(sentence); start = end; i = end - 1; } if (start < text.length) { const rest = text.slice(start).trim(); if (rest) sentences.push(rest); } function isCompleteSegment(seg) { const trimmed = seg.trim(); if (!trimmed) return false; const lastChar = trimmed[trimmed.length - 1]; const isPunct = ['.', '?', '!', '。', '?', '!'].includes(lastChar); if (!isPunct) return false; const last5 = trimmed.slice(-5); for (const abbr of abbreviations) { if (last5.endsWith(abbr)) return false; } return true; } if (text.length <= maxLen) { return [{ text, isComplete: isCompleteSegment(text) }]; } const chunks = []; let current = ''; for (const sentence of sentences) { if (sentence.length > maxLen) { if (current) { chunks.push({ text: current.trim(), isComplete: isCompleteSegment(current) }); current = ''; } const parts = _splitLongSentence(sentence, maxLen, abbreviations); for (const part of parts) { chunks.push({ text: part.trim(), isComplete: false }); } continue; } const candidate = current ? current + ' ' + sentence : sentence; if (candidate.length <= maxLen) { current = candidate; } else { if (current) { chunks.push({ text: current.trim(), isComplete: isCompleteSegment(current) }); } current = sentence; } } if (current) { chunks.push({ text: current.trim(), isComplete: isCompleteSegment(current) }); } return chunks; } function _splitLongSentence(sentence, maxLen, abbreviations) { const unsafeEndWords = new Set([ 'a', 'an', 'the', 'and', 'or', 'but', 'if', 'of', 'to', 'for', 'with', 'from', 'in', 'on', 'at', 'by', 'as', 'than', 'that', 'which', 'who', 'whom', 'this', 'these', 'those', 'is', 'are', 'was', 'were', 'be', 'been', 'being' ]); const parts = []; let remaining = sentence.trim(); while (remaining.length > maxLen) { const limit = maxLen; let breakIndex = -1; const strongPunctuation = /[;;::—–]/g; let match; while ((match = strongPunctuation.exec(remaining.slice(0, limit + 1))) !== null) { breakIndex = match.index + 1; } if (breakIndex < 0) { const commaIndex = Math.max( remaining.lastIndexOf(',', limit), remaining.lastIndexOf(',', limit), remaining.lastIndexOf('、', limit) ); if (commaIndex >= Math.floor(maxLen * 0.45)) { breakIndex = commaIndex + 1; } } if (breakIndex < 0) { const whitespace = remaining.lastIndexOf(' ', limit); if (whitespace >= Math.floor(maxLen * 0.45)) { breakIndex = whitespace; } } if (breakIndex < 1) breakIndex = maxLen; let part = remaining.slice(0, breakIndex).trim(); let words = part.split(/\s+/); while (words.length > 1 && unsafeEndWords.has(words[words.length - 1].replace(/[.,!?;:]+$/, '').toLowerCase())) { words.pop(); const candidate = words.join(' ').trim(); if (candidate.length >= Math.floor(maxLen * 0.45)) { part = candidate; } else { break; } } if (!part) part = remaining.slice(0, maxLen).trim(); parts.push(part); remaining = remaining.slice(part.length).trim(); } if (remaining) parts.push(remaining); return parts; } function _prefetchSegments(segments, lang, startIndex, count, playId) { const end = Math.min(segments.length, startIndex + count); for (let i = startIndex; i < end; i++) { if (playId !== ttsPlayId) return; const seg = segments[i]; const trimHead = (i > 0); const trimTail = (i < segments.length - 1); _getDecodedAudio(seg.text, lang, false, true).catch(error => { if (playId === ttsPlayId) debug('Prefetch failed:', error); }); } } function _scheduleAudio(audioInfo, startTime, playId) { const context = _getAudioContext(); if (playId !== ttsPlayId) return null; const source = context.createBufferSource(); source.buffer = audioInfo.audioBuffer; source.connect(context.destination); ttsScheduledSources.add(source); source.onended = () => { ttsScheduledSources.delete(source); try { source.disconnect(); } catch (e) {} }; const offset = Math.max(0, audioInfo.startOffsetSec); const duration = Math.max(0.001, Math.min(audioInfo.durationSec, audioInfo.audioBuffer.duration - offset)); source.start(startTime, offset, duration); return { source, startTime, duration, endTime: startTime + duration }; } async function _playSegments(segments, lang, playId) { if (!segments || !segments.length) return; const context = _getAudioContext(); if (context.state === 'suspended') { try { await context.resume(); } catch (error) { debug('AudioContext resume failed:', error); throw error; } } if (playId !== ttsPlayId) return; const n = segments.length; const trimOptions = segments.map((seg, index) => { if (seg.isComplete) { return { trimHead: false, trimTail: false }; } else { const trimHead = index > 0; const trimTail = index < n - 1; return { trimHead, trimTail }; } }); const firstOpts = trimOptions[0]; const first = await _getDecodedAudio(segments[0].text, lang, firstOpts.trimHead, firstOpts.trimTail); if (playId !== ttsPlayId) return; let nextStartTime = context.currentTime + CONFIG.ttsInitialScheduleMs / 1000; ttsPlaybackStartTime = nextStartTime; for (let i = 0; i < n; i++) { if (playId !== ttsPlayId) return; let audioInfo; if (i === 0) { audioInfo = first; } else { const opts = trimOptions[i]; audioInfo = await _getDecodedAudio(segments[i].text, lang, opts.trimHead, opts.trimTail); } if (playId !== ttsPlayId) return; _prefetchSegments(segments, lang, i + 1, CONFIG.ttsPrefetchCount, playId); const minimumStartTime = context.currentTime + 0.005; if (nextStartTime < minimumStartTime) { debug(`Segment ${i} scheduling late:`, (minimumStartTime - nextStartTime).toFixed(3) + 's'); nextStartTime = minimumStartTime; } const scheduled = _scheduleAudio(audioInfo, nextStartTime, playId); if (!scheduled) return; nextStartTime = scheduled.endTime + CONFIG.ttsGap / 1000; ttsPlaybackEndTime = nextStartTime; } } let ttsPlayId = 0; let currentTtsCancel = null; function playAudio(text, lang) { _patchStopCurrentAudio(); _stopGoogleTts(); try { if (typeof stopCurrentAudio === 'function') stopCurrentAudio(); } catch (e) {} const playId = ++ttsPlayId; text = String(text || '').trim(); lang = String(lang || 'en'); if (!text) return; const segments = _splitText(text, CONFIG.ttsMaxChars); if (!segments.length) return; currentTtsCancel = () => { _stopScheduledSources(); }; (async () => { try { _prefetchSegments(segments, lang, 1, CONFIG.ttsPrefetchCount, playId); await _playSegments(segments, lang, playId); } catch (error) { if (playId !== ttsPlayId) return; console.error('[Google TTS] 播放失败:', error); try { if (typeof showToast === 'function') showToast('Google TTS 播放失败'); } catch (e) {} } finally { if (playId === ttsPlayId) currentTtsCancel = null; } })(); } function _reset() { ++ttsPlayId; _stopGoogleTts(); state.transQueue.length = 0; state.audioQueue.length = 0; state.transPending.clear(); state.audioPending.clear(); state.audioDecodedPending.clear(); state.transCache.clear(); state.audioCache.clear(); state.audioCacheOrder.length = 0; state.audioDecodedCache.clear(); state.audioDecodedCacheOrder.length = 0; state.transRateLimitCount = 0; state.audioRateLimitCount = 0; state.transCooldownUntil = 0; state.audioCooldownUntil = 0; saveCooldown(); if (ttsAudioContext) { try { _stopScheduledSources(); } catch (e) {} } debug('GoogleService reset'); } return { translate, playAudio, stop: _stopGoogleTts, _reset }; })(); function getCacheKey(engine, text, lang) { return `${engine}:${encodeURIComponent(text)}:${lang}`; } function setTranslationCache(key, value) { if (translationCache.size >= MAX_CACHE_SIZE) { const firstKey = translationCache.keys().next().value; translationCache.delete(firstKey); } translationCache.set(key, value); } function getAllNotes() { let data = GM_getValue(NOTES_STORAGE_KEY, []); if (!Array.isArray(data)) { console.warn('[笔记] 存储数据损坏,已重置为空数组'); data = []; GM_setValue(NOTES_STORAGE_KEY, data); } return data; } function getNotes() { return getAllNotes().filter(n => !n.deleted); } function getDeletedNotes() { return getAllNotes().filter(n => n.deleted === true && !n.permanentlyDeleted); } let notesSyncPromise = null; let notesSyncPending = false; let notesRetryCount = 0; async function syncNotesWithCloudAfterLocalChange() { if (!isUpstashConfigured()) return; if (notesSyncPromise) { notesSyncPending = true; return notesSyncPromise; } notesRetryCount = 0; notesSyncPromise = (async () => { const TIMEOUT_MS = 15000; let timeoutId = null; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error('同步超时(15秒)')); }, TIMEOUT_MS); }); const syncLogic = (async () => { try { notesSyncPending = false; const cloud = await fetchCloudNotes(); let local = getAllNotes(); let merged = mergeNotes(local, cloud); const localNow = getAllNotes(); const finalMerged = mergeNotes(localNow, cloud); saveNotes(finalMerged, true); await pushNotesToCloud(finalMerged); GM_setValue(NOTE_SYNC_STATUS_KEY, true); return getAllNotes(); } catch (error) { console.error('[笔记同步] 同步逻辑错误:', error); GM_setValue(NOTE_SYNC_STATUS_KEY, false); throw error; } })(); try { const result = await Promise.race([syncLogic, timeoutPromise]); clearTimeout(timeoutId); return result; } catch (error) { console.warn('[笔记同步] 同步失败:', error); throw error; } finally { notesSyncPromise = null; if (notesSyncPending && notesRetryCount < MAX_RETRY) { notesRetryCount++; notesSyncPending = false; setTimeout(() => { syncNotesWithCloudAfterLocalChange().catch(console.warn); }, 300); } else if (notesSyncPending && notesRetryCount >= MAX_RETRY) { notesSyncPending = false; } else { notesSyncPending = false; } if (timeoutId) clearTimeout(timeoutId); } })(); return notesSyncPromise; } function saveNotes(notes, skipSync = false) { const normalized = notes.map(n => { if (typeof n.deleted === 'undefined') n.deleted = false; if (typeof n.deletedAt === 'undefined') n.deletedAt = 0; if (typeof n.permanentlyDeleted === 'undefined') n.permanentlyDeleted = false; if (typeof n.syncAt === 'undefined') { n.syncAt = n.deletedAt || n.updatedAt || n.createdAt || Date.now(); } return n; }); GM_setValue(NOTES_STORAGE_KEY, normalized); if (isUpstashConfigured() && !skipSync) { syncNotesWithCloudAfterLocalChange().catch(console.warn); } } function addNoteFromTranslation(title, content) { const maxTitleLen = 20; let noteTitle = (title || '').trim(); if (noteTitle.length > maxTitleLen) { noteTitle = noteTitle.slice(0, maxTitleLen) + '...'; } if (!noteTitle) noteTitle = '无标题'; const allNotes = getAllNotes(); const now = Date.now(); const newNote = { id: now, title: noteTitle, content: content.trim() || '(空内容)', createdAt: now, updatedAt: now, deleted: false, deletedAt: 0 }; allNotes.push(newNote); saveNotes(allNotes); showToast('已添加到笔记本'); } async function fetchCloudNotes(overrideUrl, overrideToken) { if (!isUpstashConfigured() && !overrideUrl) return []; try { const result = await upstashRequest('GET', `/get/${UPSTASH_NOTES_KEY}`, undefined, overrideUrl, overrideToken); if (typeof result === 'string') { if (result.trim() === '') return []; try { let parsed = JSON.parse(result); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && typeof parsed.value === 'string') { parsed = JSON.parse(parsed.value); } return Array.isArray(parsed) ? parsed : []; } catch (e) { console.warn('[笔记] 云端数据不是合法 JSON:', result); return []; } } if (Array.isArray(result)) return result; if (result && typeof result === 'object' && typeof result.value === 'string') { try { const parsedValue = JSON.parse(result.value); return Array.isArray(parsedValue) ? parsedValue : []; } catch (e) { console.warn('[笔记] 云端 value 字段不是合法 JSON:', result.value); return []; } } return []; } catch (e) { console.warn('[笔记] 获取云端笔记失败:', e); throw e; } } async function pushNotesToCloud(notes) { if (!isUpstashConfigured()) return; try { await upstashRequest( 'POST', `/set/${UPSTASH_NOTES_KEY}`, { value: JSON.stringify(notes) } ); } catch (e) { console.warn( '[笔记] 推送云端失败:', e ); throw e; } } function getNoteSyncTime(note) { return Number(note?.syncAt || note?.deletedAt || note?.updatedAt || note?.createdAt || 0); } function mergeNotes(local, cloud) { const map = new Map(); const addItem = (item) => { if (!item || typeof item.id === 'undefined') return; const existing = map.get(item.id); if (!existing) { map.set(item.id, { ...item }); return; } const existingTime = getNoteSyncTime(existing); const newTime = getNoteSyncTime(item); if (newTime > existingTime) { map.set(item.id, { ...item }); } else if (newTime === existingTime) { if (item.permanentlyDeleted && !existing.permanentlyDeleted) { map.set(item.id, { ...item }); } else if (item.deleted && !existing.deleted) { map.set(item.id, { ...item }); } } }; (Array.isArray(local) ? local : []).forEach(addItem); (Array.isArray(cloud) ? cloud : []).forEach(addItem); return Array.from(map.values()); } function getTypewriterSpeedKey() { return GM_getValue('deepseek_typewriter_speed', DEFAULT_TYPEWRITER_SPEED_KEY); } function getTypewriterSpeedMs() { const key = getTypewriterSpeedKey(); const entry = TYPEWRITER_SPEED_MAP[key]; return entry ? entry.value : null; } function configureDeepSeekMarkdown() { if (!window.marked || configureDeepSeekMarkdown._configured) return; try { window.marked.setOptions({ gfm: true, breaks: true, pedantic: false, mangle: false, headerIds: false }); } catch (e) { console.warn('[DeepSeek Markdown] marked 配置失败,将使用默认配置:', e); } configureDeepSeekMarkdown._configured = true; } function renderDeepSeekMarkdown(text) { const source = typeof text === 'string' ? text : String(text ?? ''); if (!window.marked) return null; configureDeepSeekMarkdown(); try { return window.marked.parse(source, { gfm: true, breaks: true, pedantic: false, mangle: false, headerIds: false }); } catch (e) { console.warn('[DeepSeek Markdown] 渲染失败:', e); return null; } } function setDeepSeekMessageContent(element, text) { if (!element) return; const html = renderDeepSeekMarkdown(text); if (html !== null) { setElementHtml(element, html); } else { element.textContent = typeof text === 'string' ? text : String(text ?? ''); } } function scrollDeepSeekConversation(element) { const container = element?.closest('.ds-conversation'); if (!container) return; const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; if (distanceFromBottom < 80 || element._forceAutoScroll) { container.scrollTop = container.scrollHeight; } } function typewriter(element, text, speedMs, useMarkdown = false, onComplete) { if (!element) return; const source = typeof text === 'string' ? text : String(text ?? ''); if (element._typewriterTimer) { clearTimeout(element._typewriterTimer); element._typewriterTimer = null; } element._typewriterSource = source; element._typewriterIndex = 0; element._forceAutoScroll = true; setElementHtml(element, ''); if (!useMarkdown) { if (speedMs === null || speedMs === undefined || speedMs <= 0) { element.textContent = source; element._forceAutoScroll = false; if (onComplete) onComplete(); return; } } if (speedMs === null || speedMs === undefined || speedMs <= 0 || !window.marked) { if (useMarkdown && window.marked) { setDeepSeekMessageContent(element, source); } else { element.textContent = source; } element._forceAutoScroll = false; if (onComplete) onComplete(); return; } configureDeepSeekMarkdown(); const charsPerFrame = speedMs <= 5 ? 4 : speedMs <= 10 ? 3 : speedMs <= 20 ? 2 : 1; const renderInterval = Math.max(10, Math.min(35, speedMs * charsPerFrame)); function addChunk() { if (element._typewriterSource !== source) return; if (element._typewriterIndex >= source.length) { setDeepSeekMessageContent(element, source); element._typewriterTimer = null; element._forceAutoScroll = false; scrollDeepSeekConversation(element); if (onComplete) onComplete(); return; } const nextIndex = Math.min( source.length, element._typewriterIndex + charsPerFrame ); const currentText = source.slice(0, nextIndex); element._typewriterIndex = nextIndex; const html = renderDeepSeekMarkdown(currentText); if (html !== null) { setElementHtml(element, html); } else { element.textContent = currentText; } scrollDeepSeekConversation(element); element._typewriterTimer = setTimeout(addChunk, renderInterval); } addChunk(); } function clearAllTypewriters() { const root = getPopupRoot(); if (!root) return; const messages = root.querySelectorAll('.ds-message.assistant'); messages.forEach(el => { if (el._typewriterTimer) { clearTimeout(el._typewriterTimer); el._typewriterTimer = null; } }); } let popup = null; let selectionTimeout = null; let lastSelectionText = ''; let savedRange = null; let currentAudio = null; let currentAudioKey = null; let currentEngine = 'youdao'; let microsoftSrcTtsSpeed = null; let microsoftTargetTtsSpeed = null; let microsoftLastPlayed = null; let targetLang = 'zh-Hans'; let currentCache = { text: '', youdao: null, microsoft: {}, google: {}, deepseek: { data: null, history: [] } }; let deepseekState = { history: [] }; let pointerIsDown = false; let interactingWithPopup = false; let popupInteractionTimeout = null; let currentRequestId = 0; let isConfigPageOpen = false; let mouseDownInPopup = false; let touchScrollActive = false; let touchStartX = 0; let touchStartY = 0; let suppressNextOutsideClick = false; let touchSelectionPending = false; let touchSource = null; let globalNoteTtsStop = null; let wordListPageState = { sortMode: 'time', favorites: [], originalFavorites: [] }; let notesPageState = { sortMode: 'time' }; function getTrashWords() { return getFavoriteWords().filter(item => item.deleted === true && !item.permanentlyDeleted); } function cleanOldDeletedItems() { const now = Date.now(); const sevenDays = 7 * 24 * 60 * 60 * 1000; const thirtyDays = 30 * 24 * 60 * 60 * 1000; let words = getFavoriteWords(); if (Array.isArray(words)) { const initialCount = words.length; words = words.filter(item => { if (item.deleted && !item.permanentlyDeleted && item.deletedAt) { return (now - item.deletedAt) <= sevenDays; } if (item.permanentlyDeleted && item.deletedAt) { return (now - item.deletedAt) <= thirtyDays; } return true; }); if (words.length !== initialCount) { saveFavoriteWords(words); } } let notes = getAllNotes(); if (Array.isArray(notes)) { const initialCount = notes.length; notes = notes.filter(item => { if (item.deleted && !item.permanentlyDeleted && item.deletedAt) { return (now - item.deletedAt) <= sevenDays; } if (item.permanentlyDeleted && item.deletedAt) { return (now - item.deletedAt) <= thirtyDays; } return true; }); if (notes.length !== initialCount) { saveNotes(notes); } } } function getCurrentPopupRect() { if (!popup) return null; const rect = popup.getBoundingClientRect(); return { left: rect.left + window.scrollX, top: rect.top + window.scrollY, width: rect.width, height: rect.height }; } function getEnabledEngines() { const defaultEnabled = ['youdao', 'microsoft', 'google', 'deepseek']; return GM_getValue('enabled_engines', defaultEnabled); } function isTTSBtnEnabled() { return GM_getValue('enable_tts_btn', true); } function isNotebookEnabled() { return GM_getValue('show_notebook_btn', true); } function isWordlistEnabled() { return GM_getValue('show_wordlist_btn', true); } function getThemeMode() { return GM_getValue('theme_mode', 'system'); } function getTTSApiUrl() { const custom = GM_getValue('custom_tts_api', '').trim(); return custom || DEFAULT_TTS_API; } function getTTSSpeed() { return GM_getValue('custom_tts_speed', 'normal'); } function getFavoriteWords() { let data = GM_getValue('favorite_words', []); if (!Array.isArray(data)) { console.warn('[单词本] 存储数据损坏,已重置为空数组'); data = []; GM_setValue('favorite_words', data); } return data; } function saveFavoriteWords(words, skipSync = false) { const normalized = words.map(item => { if (typeof item.deleted === 'undefined') item.deleted = false; if (typeof item.deletedAt === 'undefined') item.deletedAt = 0; if (typeof item.permanentlyDeleted === 'undefined') item.permanentlyDeleted = false; if (typeof item.syncAt === 'undefined') { item.syncAt = item.deletedAt || item.time || Date.now(); } return item; }); GM_setValue('favorite_words', normalized); if (isUpstashConfigured() && !skipSync) { syncFavoriteWithCloudAfterLocalChange().catch(console.warn); } } function migrateFavoriteData() { const words = getFavoriteWords(); if (!Array.isArray(words)) { GM_setValue('favorite_words', []); return; } let changed = false; const migrated = words.map(item => { if (typeof item.deleted === 'undefined') { changed = true; return { word: item.word, time: item.time || Date.now(), deleted: false, deletedAt: 0 }; } return item; }); if (changed) saveFavoriteWords(migrated); } const UPSTASH_FAVORITES_KEY = 'favorite_words'; function getUpstashUrl() { return GM_getValue('upstash_redis_rest_url', '').trim(); } function getUpstashToken() { return GM_getValue('upstash_redis_rest_token', '').trim(); } function isUpstashConfigured() { const enabled = GM_getValue('upstash_enabled', true); if (!enabled) return false; return !!(getUpstashUrl() && getUpstashToken()); } function setSyncingStatus(el) { if (!el) return; const html = ` 保存位置:本地 + Upstash 云端 | 正在同步 `; setElementHtml(el, html); } function upstashRequest(method, path, body, overrideUrl, overrideToken) { return new Promise((resolve, reject) => { const baseUrl = overrideUrl || getUpstashUrl(); const token = overrideToken || getUpstashToken(); if (!baseUrl || !token) { reject(new Error('Upstash 未配置')); return; } const url = baseUrl.replace(/\/$/, '') + path; GM_xmlhttpRequest({ method: method, url: url, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, data: body ? JSON.stringify(body) : undefined, timeout: 10000, onload(res) { if (res.status >= 200 && res.status < 300) { try { const data = JSON.parse(res.responseText); if (data.error) { reject(new Error(data.error)); } else { resolve(data.result); } } catch (e) { reject(new Error('Upstash 响应解析失败')); } } else { reject(new Error(`Upstash HTTP ${res.status}`)); } }, ontimeout() { reject(new Error('Upstash 请求超时')); }, onerror() { reject(new Error('Upstash 网络错误')); } }); }); } async function fetchCloudFavorites(overrideUrl, overrideToken) { if (!isUpstashConfigured() && !overrideUrl) return []; try { const result = await upstashRequest('GET', `/get/${UPSTASH_FAVORITES_KEY}`, undefined, overrideUrl, overrideToken); if (typeof result === 'string') { if (result.trim() === '') return []; try { let parsed = JSON.parse(result); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && typeof parsed.value === 'string') { parsed = JSON.parse(parsed.value); } return Array.isArray(parsed) ? parsed : []; } catch (e) { console.warn('[同步] 云端数据不是合法 JSON:', result); return []; } } if (Array.isArray(result)) { return result; } if (result && typeof result === 'object' && typeof result.value === 'string') { try { const parsedValue = JSON.parse(result.value); return Array.isArray(parsedValue) ? parsedValue : []; } catch (e) { console.warn('[同步] 云端 value 字段不是合法 JSON:', result.value); return []; } } return []; } catch (e) { console.warn('[同步] 获取云端收藏失败:', e); throw e; } } async function pushCloudFavorites(words) { if (!isUpstashConfigured()) return; try { await upstashRequest('POST', `/set/${UPSTASH_FAVORITES_KEY}`, { value: JSON.stringify(words) }); } catch (e) { console.warn('推送云端收藏失败:', e); } } let favoritesSyncPromise = null; let favoritesSyncPending = false; let favoritesRetryCount = 0; async function syncFavoriteWithCloudAfterLocalChange() { if (!isUpstashConfigured()) return; if (favoritesSyncPromise) { favoritesSyncPending = true; return favoritesSyncPromise; } favoritesRetryCount = 0; favoritesSyncPromise = (async () => { const TIMEOUT_MS = 15000; let timeoutId = null; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error('同步超时(15秒)')); }, TIMEOUT_MS); }); const syncLogic = (async () => { try { favoritesSyncPending = false; const cloud = await fetchCloudFavorites(); let local = getFavoriteWords(); let merged = mergeFavoriteWords(local, cloud); const localNow = getFavoriteWords(); const finalMerged = mergeFavoriteWords(localNow, cloud); saveFavoriteWords(finalMerged, true); await pushCloudFavorites(finalMerged); GM_setValue(WORD_SYNC_STATUS_KEY, true); return getFavoriteWords(); } catch (error) { console.error('[单词本同步] 同步逻辑错误:', error); GM_setValue(WORD_SYNC_STATUS_KEY, false); throw error; } })(); try { const result = await Promise.race([syncLogic, timeoutPromise]); clearTimeout(timeoutId); return result; } catch (error) { console.warn('[单词本同步] 同步失败:', error); throw error; } finally { favoritesSyncPromise = null; if (favoritesSyncPending && favoritesRetryCount < MAX_RETRY) { favoritesRetryCount++; favoritesSyncPending = false; setTimeout(() => { syncFavoriteWithCloudAfterLocalChange().catch(console.warn); }, 300); } else if (favoritesSyncPending && favoritesRetryCount >= MAX_RETRY) { favoritesSyncPending = false; } else { favoritesSyncPending = false; } if (timeoutId) clearTimeout(timeoutId); } })(); return favoritesSyncPromise; } function getFavoriteSyncTime(item) { return Number(item?.syncAt || item?.deletedAt || item?.time || 0); } function mergeFavoriteWords(local, cloud) { const map = new Map(); const addItem = (item) => { if (!item || !item.word) return; const word = item.word; const existing = map.get(word); if (!existing) { map.set(word, { ...item }); return; } const existingTime = getFavoriteSyncTime(existing); const newTime = getFavoriteSyncTime(item); if (newTime > existingTime) { map.set(word, { ...item }); } else if (newTime === existingTime) { if (item.permanentlyDeleted && !existing.permanentlyDeleted) { map.set(word, { ...item }); } else if (item.deleted && !existing.deleted) { map.set(word, { ...item }); } } }; (Array.isArray(local) ? local : []).forEach(addItem); (Array.isArray(cloud) ? cloud : []).forEach(addItem); return Array.from(map.values()); } function isWordFavorite(word) { return getFavoriteWords().some(item => item.word === word && !item.deleted); } function addFavoriteWord(word) { const words = getFavoriteWords(); const existing = words.find(item => item.word === word); if (existing) { if (existing.deleted) { existing.deleted = false; existing.deletedAt = 0; existing.time = Date.now(); existing.syncAt = Date.now(); existing.permanentlyDeleted = false; } } else { words.push({ word, time: Date.now(), deleted: false, deletedAt: 0 }); } saveFavoriteWords(words); syncFavoriteWithCloudAfterLocalChange(); } function addFavoriteWords(wordArray) { if (!wordArray || wordArray.length === 0) return; const words = getFavoriteWords(); const now = Date.now(); let addedCount = 0; wordArray.forEach(word => { const existing = words.find(item => item.word === word); if (existing) { if (existing.deleted) { existing.deleted = false; existing.deletedAt = 0; existing.time = now; existing.syncAt = now; existing.permanentlyDeleted = false; addedCount++; } } else { words.push({ word, time: now, deleted: false, deletedAt: 0 }); addedCount++; } }); if (addedCount > 0) { saveFavoriteWords(words); } return addedCount; } function removeFavoriteWord(word) { const words = getFavoriteWords(); const item = words.find(item => item.word === word); if (item && !item.deleted) { const now = Date.now(); item.deleted = true; item.deletedAt = now; item.syncAt = now; saveFavoriteWords(words); syncFavoriteWithCloudAfterLocalChange(); } } function toggleFavoriteWord(word) { const words = getFavoriteWords(); const item = words.find(item => item.word === word); if (item && !item.deleted) { const now = Date.now(); item.deleted = true; item.deletedAt = now; item.syncAt = now; saveFavoriteWords(words); syncFavoriteWithCloudAfterLocalChange(); return false; } else { if (item) { const now = Date.now(); item.deleted = false; item.deletedAt = 0; item.time = now; item.syncAt = now; item.permanentlyDeleted = false; } else { words.push({ word, time: Date.now(), deleted: false, deletedAt: 0 }); } saveFavoriteWords(words); syncFavoriteWithCloudAfterLocalChange(); return true; } } function enableAutoHideScrollbar(el, delay = 1000) { if (!el) return; el.style.setProperty('--scrollbar-opacity', '0'); const show = () => { el.style.setProperty('--scrollbar-opacity', '1'); clearTimeout(el._scrollHideTimer); }; const hide = () => { el._scrollHideTimer = setTimeout(() => { el.style.setProperty('--scrollbar-opacity', '0'); }, delay); }; el.addEventListener('mouseenter', show, { passive: true }); el.addEventListener('scroll', show, { passive: true }); el.addEventListener('mouseleave', hide, { passive: true }); el.addEventListener('touchend', hide, { passive: true }); el.addEventListener('touchcancel', hide, { passive: true }); if (el.matches(':hover')) { show(); } else { show(); hide(); } } const extraStyle = ` .ds-result-box { max-height: 180px; overflow-y: auto; margin-bottom: 8px; } .ds-result-content { font-size: 15px; font-weight: 500; color: #0f1115; line-height: 1.6; white-space: pre-wrap; word-break: break-word; padding: 6px 0; } .ds-conversation { display: flex; flex-direction: column; align-items: flex-start; max-height: 334px; overflow-y: auto; margin: 6px 0 8px 0; padding: 4px 0; width: 100%; } .ds-message-wrapper { display: flex; flex-direction: column; align-items: flex-start; margin-bottom: 6px; width: 100%; } .ds-message-wrapper .ds-message.assistant { margin-bottom: 0; } .ds-message-wrapper .copy-btn { background: transparent; border: none; padding: 4px; cursor: pointer; color: #61666b; border-radius: 50%; transition: opacity 0.4s ease, background 0.2s; opacity: 1; } .ds-message-wrapper .copy-btn.visible { opacity: 1; } .ds-message-wrapper .copy-btn:hover { background: rgba(0,0,0,0.06); } .ds-message-wrapper .play-btn { background: transparent; border: none; padding: 4px; cursor: pointer; color: #61666b; border-radius: 50%; transition: opacity 0.4s ease, background 0.2s; opacity: 1; } .ds-message-wrapper .play-btn:hover { background: rgba(0,0,0,0.06); } .ds-message-wrapper.user-wrapper { align-items: flex-end; } .ds-actions-row { display: flex; flex-direction: row; align-items: center; gap: 2px; margin-top: 2px; } .ds-message-wrapper.user-wrapper .ds-actions-row { justify-content: flex-end; } .ds-message-wrapper .copy-btn, .ds-message-wrapper .play-btn { margin-top: 0 !important; align-self: auto !important; padding: 4px; } .ds-message { max-width: 80%; padding: 8px 14px; border-radius: 14px; word-break: break-word; overflow-wrap: anywhere; font-size: 14px; line-height: 1.5; display: inline-block; background: #f0f0f0; color: #0f1115; } .ds-message.assistant { line-height: 1.55; padding: 10px 16px; background: #f0f0f0; color: #0f1115; border-radius: 18px 18px 18px 4px; max-width: 90%; white-space: normal; } .ds-message.assistant .message-text { display: block; white-space: normal; word-break: break-word; overflow-wrap: anywhere; min-width: 0; } .ds-message.assistant p { margin: 0 0 8px 0; line-height: 1.55; } .ds-message.assistant p:last-child { margin-bottom: 0; } .ds-message.assistant h1, .ds-message.assistant h2, .ds-message.assistant h3, .ds-message.assistant h4, .ds-message.assistant h5, .ds-message.assistant h6 { margin: 14px 0 7px; line-height: 1.35; font-weight: 650; } .ds-message.assistant h1:first-child, .ds-message.assistant h2:first-child, .ds-message.assistant h3:first-child, .ds-message.assistant h4:first-child, .ds-message.assistant h5:first-child, .ds-message.assistant h6:first-child { margin-top: 0; } .ds-message.assistant h1 { font-size: 1.45em; } .ds-message.assistant h2 { font-size: 1.28em; } .ds-message.assistant h3 { font-size: 1.15em; } .ds-message.assistant h4, .ds-message.assistant h5, .ds-message.assistant h6 { font-size: 1em; } .ds-message.assistant strong { font-weight: 650; } .ds-message.assistant em { font-style: italic; } .ds-message.assistant del { opacity: 0.72; } .ds-message.assistant ul, .ds-message.assistant ol { margin: 6px 0 9px; padding-left: 1.55em; } .ds-message.assistant ul ul, .ds-message.assistant ul ol, .ds-message.assistant ol ul, .ds-message.assistant ol ol { margin-top: 2px; margin-bottom: 2px; } .ds-message.assistant li { margin: 2px 0; padding-left: 2px; line-height: 1.5; } .ds-message.assistant li > p { margin: 0; } .ds-message.assistant code { background: rgba(0, 0, 0, 0.075); padding: 0.12em 0.36em; border-radius: 5px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 0.88em; overflow-wrap: anywhere; } .ds-message.assistant pre { display: block; width: 100%; box-sizing: border-box; margin: 9px 0; padding: 11px 12px; background: rgba(0, 0, 0, 0.065); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 8px; overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 1.5; font-size: 0.88em; tab-size: 4; -webkit-overflow-scrolling: touch; } .ds-message.assistant pre code { display: block; padding: 0; background: transparent; border-radius: 0; font-size: inherit; white-space: pre; overflow-wrap: normal; word-break: normal; } .ds-message.assistant blockquote { margin: 8px 0; padding: 4px 0 4px 12px; border-left: 3px solid #c9cdd1; color: #5f6368; } .ds-message.assistant blockquote p:last-child { margin-bottom: 0; } .ds-message.assistant hr { border: 0; border-top: 1px solid #d9dce0; margin: 12px 0; } .ds-message.assistant a { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; } .ds-message.assistant a:hover { opacity: 0.72; } .ds-message.assistant table { display: block; width: 100%; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 9px 0; font-size: 0.93em; } .ds-message.assistant th, .ds-message.assistant td { padding: 6px 9px; border: 1px solid #d9dce0; text-align: left; vertical-align: top; } .ds-message.assistant th { font-weight: 650; background: rgba(0, 0, 0, 0.035); } .ds-message.assistant img { display: block; max-width: 100%; height: auto; border-radius: 7px; margin: 8px 0; } .ds-message.assistant input[type="checkbox"] { margin-right: 0.45em; vertical-align: -1px; } .ds-message.user { align-self: flex-end; background: #2c2f33; color: #ffffff; border-radius: 18px 18px 4px 18px; padding: 10px 16px; max-width: 90%; } .ds-input-area { display: flex; gap: 6px; padding-top: 10px; margin-top: 4px; width: 100%; } .ds-input-area input { flex: 1; border: 1px solid #dadce0; border-radius: 10px; padding: 8px 14px; font-size: 13px; outline: none; background: #ffffff; color: #0f1115; min-width: 0; } .ds-input-area input:focus { border-color: #0f1115; } .ds-send-btn { padding: 8px 16px; background: #1a1d21; color: #ffffff; border: none; border-radius: 10px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background 0.2s; white-space: nowrap; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .ds-send-btn:hover { background: #33373d; } .ds-send-btn:active { background: #000000; } .ds-loading { text-align: center; color: #61666b; font-size: 12px; padding: 4px 0; } .ds-conversation, .gt-scroll-box, .meanings, .word-list-container, .config-scroll-content, .notes-list-container, .note-editor-textarea, .deleted-notes-container { overflow-x: hidden; --scrollbar-opacity: 0; scrollbar-width: thin; scrollbar-color: rgba(128, 128, 128, calc(0.4 * var(--scrollbar-opacity, 1))) transparent; } .ds-conversation::-webkit-scrollbar, .gt-scroll-box::-webkit-scrollbar, .meanings::-webkit-scrollbar, .word-list-container::-webkit-scrollbar, .config-scroll-content::-webkit-scrollbar, .notes-list-container::-webkit-scrollbar, .note-editor-textarea::-webkit-scrollbar, .deleted-notes-container::-webkit-scrollbar { width: 4px; height: 4px; background: transparent; } .ds-conversation::-webkit-scrollbar-thumb, .gt-scroll-box::-webkit-scrollbar-thumb, .meanings::-webkit-scrollbar-thumb, .word-list-container::-webkit-scrollbar-thumb, .config-scroll-content::-webkit-scrollbar-thumb, .notes-list-container::-webkit-scrollbar-thumb, .note-editor-textarea::-webkit-scrollbar-thumb, .deleted-notes-container::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, calc(0.4 * var(--scrollbar-opacity, 1))); border-radius: 4px; transition: background 0.3s; } .ds-conversation::-webkit-scrollbar-thumb:hover .gt-scroll-box::-webkit-scrollbar-thumb:hover, .meanings::-webkit-scrollbar-thumb:hover, .word-list-container::-webkit-scrollbar-thumb:hover, .config-scroll-content::-webkit-scrollbar-thumb:hover, .notes-list-container::-webkit-scrollbar-thumb:hover, .note-editor-textarea::-webkit-scrollbar-thumb:hover, .deleted-notes-container::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.7); } .word-list-panel { display: flex; flex-direction: column; } .word-list-container { flex: 1 1 auto; overflow-y: auto; max-height: 264px; } .word-list-item { display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap; padding: 8px 16px; border-bottom: 1px solid #f0f0f0; } .word-list-word { white-space: normal; word-break: break-word; font-size: 16px; font-weight: 500; flex-shrink: 1; margin-right: 8px; min-width: 0; } .word-list-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; } .word-list-search.active { background: #f1f3f4 !important; color: #1a73e8 !important; } .word-list-sort-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background-color: #f7f7f7; font-size: 14px; flex-wrap: wrap; transition: background-color 0.2s, border-color 0.2s; } .config-label-sort{ color: #333333; font-weight: 500; margin-right: 4px; transition: color 0.2s; } .sort-option { display: inline-flex; align-items: center; padding: 1px 6px; background-color: transparent; color: #444444; border: 1px solid transparent; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; user-select: none; white-space: nowrap; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .sort-option:hover { background-color: #e8e8e8; color: #222222; border-color: #cccccc; } .sort-option:active { transform: scale(0.96); } .sort-option.active { background-color: #222222; color: #ffffff; border-color: #222222; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); pointer-events: none; } .sort-option:focus-visible { outline: 2px solid #888888; outline-offset: 2px; } .word-list-sync-status { padding: 8px 16px; font-size: 13px; color: #61666b; background-color: #f7f7f7; border-bottom: 1px solid #e0e0e0; transition: background-color 0.2s, color 0.2s, border-color 0.2s; } .word-list-actions .gt-icon-btn { width: 28px; height: 28px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; } .word-list-actions .fav-star { display: inline-flex; align-items: center; justify-content: center; } .note-view-content { font-size: 14px; line-height: 1.55; color: #0f1115; padding: 4px 0; } .note-view-content h1, .note-view-content h2, .note-view-content h3, .note-view-content h4, .note-view-content h5, .note-view-content h6 { margin: 14px 0 7px; line-height: 1.35; font-weight: 650; } .note-view-content h1:first-child, .note-view-content h2:first-child, .note-view-content h3:first-child, .note-view-content h4:first-child, .note-view-content h5:first-child, .note-view-content h6:first-child { margin-top: 0; } .note-view-content h1 { font-size: 1.45em; } .note-view-content h2 { font-size: 1.28em; } .note-view-content h3 { font-size: 1.15em; } .note-view-content h4, .note-view-content h5, .note-view-content h6 { font-size: 1em; } .note-view-content strong { font-weight: 650; } .note-view-content em { font-style: italic; } .note-view-content del { opacity: 0.72; } .note-view-content p { margin: 0 0 8px 0; line-height: 1.55; } .note-view-content p:last-child { margin-bottom: 0; } .note-view-content ul, .note-view-content ol { margin: 6px 0 9px; padding-left: 1.55em; } .note-view-content ul ul, .note-view-content ul ol, .note-view-content ol ul, .note-view-content ol ol { margin-top: 2px; margin-bottom: 2px; } .note-view-content li { margin: 2px 0; padding-left: 2px; line-height: 1.5; } .note-view-content li > p { margin: 0; } .note-view-content code { background: rgba(0, 0, 0, 0.075); padding: 0.12em 0.36em; border-radius: 5px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 0.88em; overflow-wrap: anywhere; } .note-view-content pre { display: block; width: 100%; box-sizing: border-box; margin: 9px 0; padding: 11px 12px; background: rgba(0, 0, 0, 0.065); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 8px; overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 1.5; font-size: 0.88em; tab-size: 4; -webkit-overflow-scrolling: touch; } .note-view-content pre code { display: block; padding: 0; background: transparent; border-radius: 0; font-size: inherit; white-space: pre; overflow-wrap: normal; word-break: normal; } .note-view-content blockquote { margin: 8px 0; padding: 4px 0 4px 12px; border-left: 3px solid #c9cdd1; color: #5f6368; } .note-view-content blockquote p:last-child { margin-bottom: 0; } .note-view-content hr { border: 0; border-top: 1px solid #d9dce0; margin: 12px 0; } .note-view-content a { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; } .note-view-content a:hover { opacity: 0.72; } .note-view-content table { display: block; width: 100%; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 9px 0; font-size: 0.93em; } .note-view-content th, .note-view-content td { padding: 6px 9px; border: 1px solid #d9dce0; text-align: left; vertical-align: top; } .note-view-content th { font-weight: 650; background: rgba(0, 0, 0, 0.035); } .note-view-content img { display: block; max-width: 100%; height: auto; border-radius: 7px; margin: 8px 0; } .note-view-content input[type="checkbox"] { margin-right: 0.45em; vertical-align: -1px; } .note-view-title { flex: 1; min-width: 0; word-wrap: break-word; white-space: normal; margin: 0px 8px 0px 4px; padding-bottom: 4px; font-size: 18px; font-weight: 600; } .config-title { display: flex; align-items: flex-start; } .header-action-btn { flex-shrink: 0; } .word-result { background: #f9f9f9; } .word-result .phonetics { margin-bottom: 4px; } .word-result .meanings { font-size: 14px; line-height: 1.5; } .word-result .phonetic-item { font-size: 13px; } .word-trigger { cursor: pointer; text-decoration: underline dotted #9aa0a6 1px; text-underline-offset: 3px; transition: background 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .word-trigger:hover { background: rgba(0, 0, 0, 0.05); } .word-trigger.active { background: rgba(0, 0, 0, 0.05); } .inline-word-card { max-height: 180px; overflow-y: auto; margin-top: 8px; padding: 6px 10px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e0e0e0; scrollbar-width: thin; scrollbar-color: rgba(128, 128, 128, calc(0.4 * var(--scrollbar-opacity, 1))) transparent; } .inline-word-card::-webkit-scrollbar { width: 4px; height: 4px; background: transparent; } .inline-word-card::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, calc(0.4 * var(--scrollbar-opacity, 1))); border-radius: 4px; } .inline-word-card::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.7); } .inline-word-card .meanings { max-height: none !important; overflow: visible !important; } .upstash-syncing { display: inline-flex; align-items: center; gap: 5px; } .upstash-sync-spinner { display: inline-block; width: 10px; height: 10px; box-sizing: border-box; border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: upstashSyncSpin 0.75s linear infinite; flex: 0 0 auto; vertical-align: middle; } @keyframes upstashSyncSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .upstash-sync-spinner { animation: none; } } .word-forms { font-size: 12px; color: #888; display: flex; flex-wrap: wrap; gap: 4px 0; margin-top: 6px; } .examples-section { margin-top: 8px; padding-top: 6px; } .examples-title { font-weight: 500; font-size: 13px; color: #61666b; margin-bottom: 4px; } .example-item { margin-bottom: 6px; font-size: 13px; line-height: 1.5; } .example-en { color: #0f1115; } .example-en b { color: #638c0b; } .example-zh { color: #61666b; font-size: 12px; } .example-source { color: #9aa0a6; font-size: 11px; margin-top: 1px; } .gt-text-source, .gt-text-target { white-space: pre-wrap; word-break: normal; overflow-wrap: break-word; } .tts-read-token { border-radius: 3px; transition: background-color .1s ease, box-shadow .1s ease, color .1s ease; display: inline; word-break: normal; overflow-wrap: break-word; } .gt-text-target { display: block; width: 100%; box-sizing: border-box; white-space: pre-wrap; word-break: normal; overflow-wrap: break-word; } .gt-text-source.editable-word { display: block !important; width: 100%; box-sizing: border-box; white-space: pre-wrap; word-break: normal; overflow-wrap: break-word; } .tts-read-token.tts-reading-token { background: rgba(26, 115, 232, .22); box-shadow: 0 0 0 2px rgba(26, 115, 232, .15); color: #0b3d91; } :host(.theme-dark) .tts-read-token.tts-reading-token { background: rgba(138, 180, 248, .32); box-shadow: 0 0 0 2px rgba(138, 180, 248, .22); color: #e8eaed; } @media (prefers-color-scheme: dark) { :host(.theme-system) .tts-read-token.tts-reading-token { background: rgba(138, 180, 248, .32); box-shadow: 0 0 0 2px rgba(138, 180, 248, .22); color: #e8eaed; } } .gt-actions .custom-popover-select .popover-trigger { padding: 2px 4px; font-size: 12px; gap: 2px; white-space: nowrap; } .gt-actions .custom-popover-select .popover-dropdown { left: auto; right: 0; min-width: 110px; } :host(.theme-dark) .gt-actions .custom-popover-select .popover-trigger { color: #cfd3d6; } @media (prefers-color-scheme: dark) { :host(.theme-system) .gt-actions .custom-popover-select .popover-trigger { color: #cfd3d6; } } .note-view-content p, .note-view-content li, .note-view-content h1, .note-view-content h2, .note-view-content h3, .note-view-content h4, .note-view-content h5, .note-view-content h6, .note-view-content blockquote { position: relative; padding-right: 24px; } .paragraph-tts-btn { position: absolute; right: 0; top: 0.2em; transition: opacity 0.2s, color 0.2s, background 0.2s; cursor: pointer; color: #61666b; padding: 2px; background: transparent; border: none; line-height: 1; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; opacity: 0.35; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .note-view-content p:hover .paragraph-tts-btn, .note-view-content li:hover .paragraph-tts-btn, .note-view-content h1:hover .paragraph-tts-btn, .note-view-content h2:hover .paragraph-tts-btn, .note-view-content h3:hover .paragraph-tts-btn, .note-view-content h4:hover .paragraph-tts-btn, .note-view-content h5:hover .paragraph-tts-btn, .note-view-content h6:hover .paragraph-tts-btn, .note-view-content blockquote:hover .paragraph-tts-btn, .paragraph-tts-btn.active { opacity: 1; } .paragraph-tts-btn:hover { background: rgba(0, 0, 0, 0.06); } .paragraph-tts-btn.active { color: #1a73e8; background: rgba(26, 115, 232, 0.10); } :host(.theme-dark) .paragraph-tts-btn { color: #cfd3d6; } :host(.theme-dark) .paragraph-tts-btn:hover { background: rgba(255,255,255,0.08); } :host(.theme-dark) .paragraph-tts-btn.active { color: #8ab4f8; background: rgba(138, 180, 248, 0.15); } @media (prefers-color-scheme: dark) { :host(.theme-system) .paragraph-tts-btn { color: #cfd3d6; } :host(.theme-system) .paragraph-tts-btn:hover { background: rgba(255,255,255,0.08); } :host(.theme-system) .paragraph-tts-btn.active { color: #8ab4f8; background: rgba(138, 180, 248, 0.15); } } .note-tts-controls { display: flex; align-items: center; gap: 8px; padding: 10px 0 8px; margin-top: 8px; flex-wrap: wrap; } .note-tts-play-all-btn { flex: 1 1 auto; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 14px; background: #1a1d21; color: #fff; border: none; border-radius: 10px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background 0.2s; -webkit-tap-highlight-color: transparent; min-width: 100px; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .note-tts-play-all-btn:hover { background: #33373d; } .note-tts-play-all-btn.playing { background: #d93025; } .note-tts-play-all-btn.playing:hover { background: #b7231a; } .note-tts-select-wrap { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: #61666b; flex-shrink: 0; } .note-tts-select-label { font-size: 12px; color: #61666b; white-space: nowrap; } .note-tts-select-wrap .custom-popover-select .popover-trigger { height: 30px; padding: 0 8px; border-radius: 8px; border: 1px solid #dadce0; background: #fff; min-width: 78px; justify-content: space-between; font-size: 12px; } .note-tts-select-wrap .custom-popover-select .popover-trigger:hover, .note-tts-select-wrap .custom-popover-select.open .popover-trigger { background: #f1f3f4; } .note-tts-select-wrap .custom-popover-select .popover-dropdown { left: auto; right: 0; min-width: 140px; } :host(.theme-dark) .note-tts-select-label { color: #cfd3d6; } :host(.theme-dark) .note-tts-select-wrap .custom-popover-select .popover-trigger { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-dark) .note-tts-select-wrap .custom-popover-select .popover-trigger:hover, :host(.theme-dark) .note-tts-select-wrap .custom-popover-select.open .popover-trigger { background: #3c4043; } :host(.theme-dark) .note-tts-play-all-btn { background: #e8eaed; color: #0f1115; } :host(.theme-dark) .note-tts-play-all-btn:hover { background: #ffffff; } :host(.theme-dark) .note-tts-play-all-btn.playing { background: #f28b82; color: #0f1115; } :host(.theme-dark) .note-tts-play-all-btn.playing:hover { background: #ee675c; } @media (prefers-color-scheme: dark) { :host(.theme-system) .note-tts-select-label { color: #cfd3d6; } :host(.theme-system) .note-tts-select-wrap .custom-popover-select .popover-trigger { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-system) .note-tts-select-wrap .custom-popover-select .popover-trigger:hover, :host(.theme-system) .note-tts-select-wrap .custom-popover-select.open .popover-trigger { background: #3c4043; } :host(.theme-system) .note-tts-play-all-btn { background: #e8eaed; color: #0f1115; } :host(.theme-system) .note-tts-play-all-btn:hover { background: #ffffff; } :host(.theme-system) .note-tts-play-all-btn.playing { background: #f28b82; color: #0f1115; } :host(.theme-system) .note-tts-play-all-btn.playing:hover { background: #ee675c; } } :host(.theme-dark) .note-view-content { color: #e8eaed; } :host(.theme-dark) .note-view-content a { color: #8ab4f8; } :host(.theme-dark) .note-view-content code { background: rgba(255, 255, 255, 0.10); color: #e8eaed; } :host(.theme-dark) .note-view-content pre { background: rgba(0, 0, 0, 0.28); border-color: rgba(255, 255, 255, 0.08); } :host(.theme-dark) .note-view-content blockquote { border-left-color: #777; color: #c9cdd1; } :host(.theme-dark) .note-view-content hr { border-top-color: #555; } :host(.theme-dark) .note-view-content th, :host(.theme-dark) .note-view-content td { border-color: #555; color: #e8eaed; } :host(.theme-dark) .note-view-content th { background: rgba(255, 255, 255, 0.06); } :host(.theme-dark) .word-result { background: #2c2c2c !important; } :host(.theme-dark) .word-result .word-result-content { color: #e8eaed; } :host(.theme-dark) .word-result-content a { color: #cfd3d6 !important; border-bottom-color: #555 !important; } :host(.theme-dark) .word-result .phonetic-item { color: #cfd3d6; } :host(.theme-dark) .word-result .phonetic-label, :host(.theme-dark) .word-result .phonetic-val { color: #adb2b8; } :host(.theme-dark) .word-result .pronounce-button { color: #cfd3d6; } :host(.theme-dark) .word-result .pronounce-button:hover { background: #3c4043; color: #e8eaed; } :host(.theme-dark) .word-result .meanings { color: #f9fafb; } :host(.theme-dark) .inline-word-card { background: #2c2c2c; border-color: #3c4043; } :host(.theme-dark) .word-trigger { text-decoration: underline dotted #5f6368 1px; } :host(.theme-dark) .word-trigger:hover { background: rgba(255, 255, 255, 0.05); } :host(.theme-dark) .word-trigger.active { background: rgba(255, 255, 255, 0.05); } :host(.theme-dark) .word-forms { color: #aaa; } :host(.theme-dark) .example-en { color: #e8eaed; } :host(.theme-dark) .example-zh { color: #cfd3d6; } :host(.theme-dark) .example-source { color: #9aa0a6; } @media (prefers-color-scheme: dark) { :host(.theme-system) .note-view-content { color: #e8eaed; } :host(.theme-system) .note-view-content a { color: #8ab4f8; } :host(.theme-system) .note-view-content code { background: rgba(255, 255, 255, 0.10); color: #e8eaed; } :host(.theme-system) .note-view-content pre { background: rgba(0, 0, 0, 0.28); border-color: rgba(255, 255, 255, 0.08); } :host(.theme-system) .note-view-content blockquote { border-left-color: #777; color: #c9cdd1; } :host(.theme-system) .note-view-content hr { border-top-color: #555; } :host(.theme-system) .note-view-content th, :host(.theme-system) .note-view-content td { border-color: #555; color: #e8eaed; } :host(.theme-system) .note-view-content th { background: rgba(255, 255, 255, 0.06); } :host(.theme-system) .word-result { background: #2c2c2c !important; } :host(.theme-system) .word-result .word-result-content { color: #e8eaed; } :host(.theme-system) .word-result-content a { color: #cfd3d6 !important; border-bottom-color: #555 !important; } :host(.theme-system) .word-result .phonetic-item { color: #cfd3d6; } :host(.theme-system) .word-result .phonetic-label, :host(.theme-system) .word-result .phonetic-val { color: #adb2b8; } :host(.theme-system) .word-result .pronounce-button { color: #cfd3d6; } :host(.theme-system) .word-result .pronounce-button:hover { background: #3c4043; color: #e8eaed; } :host(.theme-system) .word-result .meanings { color: #f9fafb; } :host(.theme-system) .inline-word-card { background: #2c2c2c; border-color: #3c4043; } :host(.theme-system) .word-trigger { text-decoration: underline dotted #5f6368 1px; } :host(.theme-system) .word-trigger:hover { background: rgba(255, 255, 255, 0.05); } :host(.theme-system) .word-trigger.active { background: rgba(255, 255, 255, 0.05); } :host(.theme-system) .word-forms { color: #aaa; } :host(.theme-system) .example-en { color: #e8eaed; } :host(.theme-system) .example-zh { color: #cfd3d6; } :host(.theme-system) .example-source { color: #9aa0a6; } } .note-editor-textarea { scrollbar-width: thin !important; scrollbar-color: rgba(128, 128, 128, 0.4) transparent !important; } .note-editor-textarea::-webkit-scrollbar { width: 4px !important; height: 4px !important; background: transparent !important; } .note-editor-textarea::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.4) !important; border-radius: 4px !important; } .note-editor-textarea::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.7) !important; } :host(.theme-dark) .note-editor-textarea, :host(.theme-dark) .note-view-content { scrollbar-color: rgba(200, 200, 200, 0.3) transparent !important; } :host(.theme-dark) .note-editor-textarea::-webkit-scrollbar-thumb, :host(.theme-dark) .note-view-content::-webkit-scrollbar-thumb { background: rgba(200, 200, 200, 0.3) !important; } :host(.theme-dark) .note-editor-textarea::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .note-view-content::-webkit-scrollbar-thumb:hover { background: rgba(200, 200, 200, 0.6) !important; } @media (prefers-color-scheme: dark) { :host(.theme-system) .note-editor-textarea, :host(.theme-system) .note-view-content { scrollbar-color: rgba(200, 200, 200, 0.3) transparent !important; } :host(.theme-system) .note-editor-textarea::-webkit-scrollbar-thumb, :host(.theme-system) .note-view-content::-webkit-scrollbar-thumb { background: rgba(200, 200, 200, 0.3) !important; } :host(.theme-system) .note-editor-textarea::-webkit-scrollbar-thumb:hover, :host(.theme-system) .note-view-content::-webkit-scrollbar-thumb:hover { background: rgba(200, 200, 200, 0.6) !important; } } :host(.theme-dark) .ds-conversation, :host(.theme-dark) .gt-scroll-box, :host(.theme-dark) .meanings, :host(.theme-dark) .word-list-container, :host(.theme-dark) .config-scroll-content, :host(.theme-dark) .notes-list-container, :host(.theme-dark) .note-editor-textarea, :host(.theme-dark) .deleted-notes-container { scrollbar-color: rgba(200, 200, 200, calc(0.3 * var(--scrollbar-opacity, 1))) transparent; } :host(.theme-dark) .ds-conversation::-webkit-scrollbar-thumb, :host(.theme-dark) .gt-scroll-box::-webkit-scrollbar-thumb, :host(.theme-dark) .meanings::-webkit-scrollbar-thumb, :host(.theme-dark) .word-list-container::-webkit-scrollbar-thumb, :host(.theme-dark) .config-scroll-content::-webkit-scrollbar-thumb, :host(.theme-dark) .notes-list-container::-webkit-scrollbar-thumb, :host(.theme-dark) .note-editor-textarea::-webkit-scrollbar-thumb, :host(.theme-dark) .deleted-notes-container::-webkit-scrollbar-thumb { background: rgba(200, 200, 200, calc(0.3 * var(--scrollbar-opacity, 1))); } :host(.theme-dark) .ds-conversation::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .gt-scroll-box::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .meanings::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .word-list-container::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .config-scroll-content::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .notes-list-container::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .note-editor-textarea::-webkit-scrollbar-thumb:hover, :host(.theme-dark) .deleted-notes-container::-webkit-scrollbar-thumb:hover { background: rgba(200, 200, 200, 0.6); } :host(.theme-dark) .ds-send-btn { background: #e8eaed; color: #0f1115; } :host(.theme-dark) .ds-send-btn:hover { background: #ffffff; } :host(.theme-dark) .ds-input-area input { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-dark) .ds-input-area input:focus { border-color: #f9fafb; } :host(.theme-dark) .ds-message.user { background: #e8eaed; color: #0f1115; } :host(.theme-dark) .ds-message.assistant { background: #3c4043; color: #ffffff; } :host(.theme-dark) .ds-message.assistant code { background: rgba(255,255,255,0.10); } :host(.theme-dark) .ds-message.assistant pre { background: rgba(0,0,0,0.28); border-color: rgba(255,255,255,0.08); } :host(.theme-dark) .ds-message.assistant blockquote { border-left-color: #777; color: #c9cdd1; } :host(.theme-dark) .ds-message.assistant hr { border-top-color: #555; } :host(.theme-dark) .ds-message.assistant th, :host(.theme-dark) .ds-message.assistant td { border-color: #555; } :host(.theme-dark) .ds-message.assistant th { background: rgba(255,255,255,0.06); } :host(.theme-dark) .word-list-sort-bar { background-color: #2c2c2c; } :host(.theme-dark) .config-label-sort { color: #bbbbbb; } :host(.theme-dark) .sort-option { color: #aaaaaa; } :host(.theme-dark) .sort-option:hover { background-color: #4a4a4a; color: #f0f0f0; border-color: #666666; } :host(.theme-dark) .sort-option.active { background-color: #e0e0e0; color: #1a1a1a; border-color: #e0e0e0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); } :host(.theme-dark) .sort-option:focus-visible { outline-color: #888888; } :host(.theme-dark) .ds-message-wrapper .copy-btn:hover { background: rgba(255,255,255,0.1); } :host(.theme-dark) .ds-message-wrapper .play-btn:hover { background: rgba(255,255,255,0.1); } :host(.theme-dark) .word-list-sync-status { background-color: #2c2c2c; color: #bbbbbb; border-bottom-color: #3c4043; } @media (prefers-color-scheme: dark) { :host(.theme-system) .ds-conversation, :host(.theme-system) .gt-scroll-box, :host(.theme-system) .meanings, :host(.theme-system) .word-list-container, :host(.theme-system) .config-scroll-content, :host(.theme-system) .notes-list-container, :host(.theme-system) .note-editor-textarea, :host(.theme-system) .deleted-notes-container { scrollbar-color: rgba(200, 200, 200, calc(0.3 * var(--scrollbar-opacity, 1))) transparent; } :host(.theme-system) .ds-conversation::-webkit-scrollbar-thumb, :host(.theme-system) .gt-scroll-box::-webkit-scrollbar-thumb, :host(.theme-system) .meanings::-webkit-scrollbar-thumb, :host(.theme-system) .word-list-container::-webkit-scrollbar-thumb, :host(.theme-system) .config-scroll-content::-webkit-scrollbar-thumb, :host(.theme-system) .notes-list-container::-webkit-scrollbar-thumb, :host(.theme-system) .note-editor-textarea::-webkit-scrollbar-thumb, :host(.theme-system) .deleted-notes-container::-webkit-scrollbar-thumb { background: rgba(200, 200, 200, calc(0.3 * var(--scrollbar-opacity, 1))); } :host(.theme-system) .ds-conversation::-webkit-scrollbar-thumb:hover, :host(.theme-system) .gt-scroll-box::-webkit-scrollbar-thumb:hover, :host(.theme-system) .meanings::-webkit-scrollbar-thumb:hover, :host(.theme-system) .word-list-container::-webkit-scrollbar-thumb:hover, :host(.theme-system) .config-scroll-content::-webkit-scrollbar-thumb:hover, :host(.theme-system) .notes-list-container::-webkit-scrollbar-thumb:hover, :host(.theme-system) .note-editor-textarea::-webkit-scrollbar-thumb:hover, :host(.theme-system) .deleted-notes-container::-webkit-scrollbar-thumb:hover { background: rgba(200, 200, 200, 0.6); } :host(.theme-system) .ds-send-btn { background: #e8eaed; color: #0f1115; } :host(.theme-system) .ds-send-btn:hover { background: #ffffff; } :host(.theme-system) .ds-input-area input { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-system) .ds-input-area input:focus { border-color: #f9fafb; } :host(.theme-system) .ds-message.user { background: #e8eaed; color: #0f1115; } :host(.theme-system) .ds-message.assistant { background: #3c4043; color: #ffffff; } :host(.theme-system) .ds-message.assistant code { background: rgba(255,255,255,0.10); } :host(.theme-system) .ds-message.assistant pre { background: rgba(0,0,0,0.28); border-color: rgba(255,255,255,0.08); } :host(.theme-system) .ds-message.assistant blockquote { border-left-color: #777; color: #c9cdd1; } :host(.theme-system) .ds-message.assistant hr { border-top-color: #555; } :host(.theme-system) .ds-message.assistant th, :host(.theme-system) .ds-message.assistant td { border-color: #555; } :host(.theme-system) .ds-message.assistant th { background: rgba(255,255,255,0.06); } :host(.theme-system) .word-list-sort-bar { background-color: #2c2c2c; } :host(.theme-system) .config-label-sort { color: #bbbbbb; } :host(.theme-system) .sort-option { color: #aaaaaa; } :host(.theme-system) .sort-option:hover { background-color: #4a4a4a; color: #f0f0f0; border-color: #666666; } :host(.theme-system) .sort-option.active { background-color: #e0e0e0; color: #1a1a1a; border-color: #e0e0e0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); } :host(.theme-system) .sort-option:focus-visible { outline-color: #888888; } :host(.theme-system) .ds-message-wrapper .copy-btn:hover { background: rgba(255,255,255,0.1); } :host(.theme-system) .ds-message-wrapper .play-btn:hover { background: rgba(255,255,255,0.1); } :host(.theme-system) .word-list-sync-status { background-color: #2c2c2c; color: #bbbbbb; border-bottom-color: #3c4043; } } .config-textarea { width: 100%; padding: 6px 8px; border: 1px solid #dadce0; border-radius: 10px; font-size: 12px; font-family: inherit; background: #ffffff; color: #0f1115; resize: vertical; transition: border-color 0.2s; } .config-textarea:focus { outline: none; border-color: #0f1115; } :host(.theme-dark) .config-textarea { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-dark) .config-textarea:focus { border-color: #f9fafb; } @media (prefers-color-scheme: dark) { :host(.theme-system) .config-textarea { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-system) .config-textarea:focus { border-color: #f9fafb; } :host(.theme-system) .cf-disabled .key-input-field { background: #3c4043 !important; color: #888 !important; } } .notes-list-container { flex: 1 1 auto; overflow-y: auto; max-height: 264px; } .notes-list-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; border-bottom: 1px solid #f0f0f0; } .notes-list-title { font-size: 15px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; margin-right: 8px; } .notes-list-actions { display: flex; gap: 2px; flex-shrink: 0; } .notes-list-actions .gt-icon-btn { width: 28px; height: 28px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; } .note-editor-title { font-size: 16px; font-weight: 600; margin-bottom: 12px; } .note-editor-title input { width: 100%; padding: 6px 8px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; background: #ffffff; color: #0f1115; } .note-editor-title input:focus { outline: none; border-color: #0f1115; } .note-editor-content textarea { width: 100%; min-height: 180px; padding: 6px 8px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; font-family: inherit; background: #ffffff; color: #0f1115; resize: vertical; } .note-editor-content textarea:focus { outline: none; border-color: #0f1115; } .note-view-content { max-height: 300px; overflow-y: auto; padding: 8px 4px; font-size: 14px; line-height: 1.6; } .note-view-content h1, .note-view-content h2, .note-view-content h3 { margin: 10px 0 6px; } .note-view-content p { margin: 6px 0; } .note-view-content ul, .note-view-content ol { padding-left: 1.5em; } .note-view-content code { background: rgba(0,0,0,0.06); padding: 0.1em 0.3em; border-radius: 4px; } .note-view-content pre { background: rgba(0,0,0,0.04); padding: 8px; border-radius: 6px; overflow-x: auto; } :host(.theme-dark) .note-view-content code { background: rgba(255,255,255,0.08); } :host(.theme-dark) .note-view-content pre { background: rgba(255,255,255,0.04); } :host(.theme-dark) .notes-list-item, :host(.theme-dark) .word-list-item { border-bottom-color: #3c4043; } :host(.theme-dark) .word-list-search.active { background: #3c4043 !important; color: #1a73e8 !important; } :host(.theme-dark) .note-editor-title input, :host(.theme-dark) .note-editor-content textarea { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-dark) .note-editor-title input:focus, :host(.theme-dark) .note-editor-content textarea:focus { border-color: #f9fafb; } @media (prefers-color-scheme: dark) { :host(.theme-system) .note-view-content code { background: rgba(255,255,255,0.08); } :host(.theme-system) .note-view-content pre { background: rgba(255,255,255,0.04); } :host(.theme-system) .notes-list-item, :host(.theme-system) .word-list-item { border-bottom-color: #3c4043; } :host(.theme-system) .word-list-search.active { background: #3c4043 !important; color: #1a73e8 !important; } :host(.theme-system) .note-editor-title input, :host(.theme-system) .note-editor-content textarea { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-system) .note-editor-title input:focus, :host(.theme-system) .note-editor-content textarea:focus { border-color: #f9fafb; } } .deleted-notes-container { flex: 1 1 auto; overflow-y: auto; max-height: 264px; } .deleted-notes-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; border-bottom: 1px solid #f0f0f0; } :host(.theme-dark) .deleted-notes-item { border-bottom-color: #3c4043; } @media (prefers-color-scheme: dark) { :host(.theme-system) .deleted-notes-item { border-bottom-color: #3c4043; } } `; const style = ` :host { position: absolute; z-index: 2147483646; display: none; } .popup-container { position: relative; width: 380px; padding: 18px 24px; background: #ffffff; border-radius: 24px; box-shadow: 0 4px 20px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.1); color: #0f1115; font-family: Google Sans, Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; line-height: 1.5; font-size: 14px; box-sizing: border-box; text-align: left; transition: width 0.15s ease, padding 0.15s ease; } :host(.theme-dark) .custom-tooltip { background: rgba(245, 245, 245, 0.96); color: #151515; box-shadow: 0 4px 12px rgba(0,0,0,0.35); } :host(.theme-dark) .custom-tooltip::after { background: rgba(245, 245, 245, 0.96); } @media (prefers-color-scheme: dark) { :host(.theme-system) .custom-tooltip { background: rgba(245, 245, 245, 0.96); color: #151515; } :host(.theme-system) .custom-tooltip::after { background: rgba(245, 245, 245, 0.96); } } .toast-msg { position: absolute; bottom: -40px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: #fff; padding: 6px 12px; border-radius: 4px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.3s; max-width: min(300px, 80vw); white-space: normal; word-break: break-word; text-align: center; z-index: 10; box-sizing: border-box; } :host(.show-toast) .toast-msg { opacity: 1; } :host(.mini-mode) .popup-container { width: auto; padding: 8px 12px; user-select: none; -webkit-user-select: none; } :host(.mini-mode) .popup-content { display: flex; align-items: center; justify-content: center; gap: 8px; } .mini-icon-btn { background: none; border: none; cursor: pointer; padding: 4px; margin: 0; outline: none; display: inline-flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 12px; transition: background 0.2s ease, transform 0.1s ease; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; color: #61666b; } .mini-icon-btn:hover { background: #f1f3f4; transform: scale(1.05); color: #1a73e8; } .mini-icon-btn:active { transform: scale(0.95); } .mini-icon-btn img, .mini-icon-btn svg { width: 28px; height: 28px; display: block; pointer-events: none; } .popup-container *, .popup-container *::before, .popup-container *::after { box-sizing: border-box; } .popup-arrow { display: none; position: absolute; width: 12px; height: 12px; background: inherit; top: -5px; left: 50%; transform: translateX(-50%) rotate(45deg); z-index: 0; pointer-events: none; border-radius: 2px; } :host(.above-selection) .popup-arrow { top: auto; bottom: -5px; } .popup-content { position: relative; z-index: 1; display: block; width: 100%; min-width: 0; padding: 0; margin: 0; color: inherit; font: inherit; line-height: inherit; } .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 12px; color: #61666b; } .header-actions { display: flex; align-items: center; gap: 4px; } .header-action-btn, .favorite-btn { width: 28px; height: 28px; cursor: pointer; color: #61666b; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; transition: background 0.2s, color 0.2s; background: transparent; border: none; padding: 0; outline: none; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .favorite-btn { opacity: 0; animation: favFadeIn 0.4s ease forwards; flex-shrink: 0; } @keyframes favFadeIn { from { opacity: 0; } to { opacity: 1; } } .header-action-btn:hover, .header-action-btn:active, .favorite-btn:hover, .favorite-btn:active{ background: #f1f3f4; color: #0f1115; } .header-action-btn.close-btn:hover, .header-action-btn.close-btn:active { color: #0f1115; } .engine-switch { display: inline-flex; align-items: center; border-radius: 10px; gap: 4px; } .engine-icon-btn { border: none; background: transparent; padding: 4px; border-radius: 12px; cursor: pointer; align-items: center; justify-content: center; transition: all 0.2s ease; outline: none; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .engine-icon-btn img { width: 28px; height: 28px; display: block; } .engine-icon-btn:hover { background: #f1f3f4; transform: scale(1.05); } .engine-icon-btn:active { transform: scale(.95); } .engine-icon-btn.active { background: #f1f3f4; transform: scale(.95); } .engine-icon-btn.active span { color: #1a73e8; font-weight: 600; } .gt-section { display: flex; flex-direction: column; gap: 4px; } .gt-lang-bar { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #61666b; font-weight: 500; margin-bottom: 2px; } .gt-lang-bar1{ padding-left: 4px; } .custom-popover-select { position: relative; display: inline-block; user-select: none; -webkit-user-select: none; } .popover-trigger { display: inline-flex; align-items: center; gap: 4px; background: transparent; color: #61666b; font-size: 12px; font-weight: 500; border: 1px solid transparent; border-radius: 6px; padding: 2px 4px; cursor: pointer; transition: background 0.2s, color 0.2s, border-color 0.2s; outline: none; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .popover-trigger:hover, .custom-popover-select.open .popover-trigger { background: #f1f3f4; } .popover-trigger svg { width: 12px; height: 12px; transition: transform 0.2s ease; stroke: currentColor; } .custom-popover-select.open .popover-trigger svg { transform: rotate(180deg); } .popover-dropdown { display: none; position: absolute; top: calc(100% + 4px); left: 0; min-width: 120px; background: #ffffff; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.1); padding: 4px 4px; z-index: 100; transform-origin: top left; animation: popover-in 0.15s ease-out; } .custom-popover-select.open .popover-dropdown { display: block; } @keyframes popover-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .popover-option { padding: 6px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; transition: background 0.15s; gap: 8px; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .popover-option:hover { background: #f1f3f4; } .popover-option .check-icon { width: 14px; height: 14px; display: none; stroke: currentColor; flex-shrink: 0; } .popover-option.selected .check-icon { display: block; } .gt-actions { display: flex; align-items: center; gap: 6px; } .gt-icon-btn { background: none; border: none; padding: 6px; margin: 0; cursor: pointer; color: #61666b; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; transition: background 0.2s, color 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .gt-icon-btn:hover { background: #f1f3f4; } .gt-scroll-box { max-height: 180px; overflow-y: auto; padding: 0 4px; } .gt-text-source { font-size: 15px; font-weight: 500; color: #0f1115; line-height: 1.5; display: block; white-space: pre-wrap; word-break: normal; overflow-wrap: break-word; width: 100%; box-sizing: border-box; } .gt-text-target { font-size: 15px; font-weight: 500; color: #0f1115; line-height: 1.6; word-break: break-word; overflow-wrap: break-word; white-space: pre-wrap; } .gt-pinyin { font-size: 13px; color: #61666b; margin-top: 4px; word-break: break-word; } .gt-divider { width: 200px; height: 1.5px; background-color: #e0e0e0; margin: 14px 0 8px 66px; border: none; } .word-title { font-size: 15px; font-weight: 500; color: #0f1115; margin-bottom: 8px; word-break: break-word; white-space: pre-wrap; display: flex; align-items: center; gap: 8px; align-items: center; } .editable-word { display: inline-flex; align-items: center; min-height: 28px; outline: none; cursor: text; border-radius: 2px; transition: background-color 0.2s, border-color 0.2s; } .editable-word:hover, .editable-word:focus { background-color: rgba(0, 0, 0, 0.04); } .definition-section { margin-bottom: 12px; } .phonetics { display: flex; column-gap: 16px; flex-wrap: wrap; font-size: 13px; margin-bottom: 10px; align-items: center; } .phonetic-item { display: flex; align-items: center; gap: 6px; color: #61666b; font-family: 'Lucida Sans Unicode', 'Arial Unicode MS', Arial, sans-serif; } .phonetic-item .phonetic-label { color: #61666b; font-family: system-ui, -apple-system, sans-serif; font-weight: normal; } .phonetic-item .phonetic-val { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Calibri, Roboto, sans-serif; color: #61666b; font-size: 12px; font-weight: normal; -webkit-font-smoothing: antialiased; } .phonetic-item .pronounce-button { width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 50%; background: transparent; color: #61666b; cursor: pointer; transition: background 0.2s, color 0.2s; user-select: none; flex-shrink: 0; padding: 0; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .pronounce-button:hover { background: #f1f3f4; color: #0f1115; } .meanings { font-size: 15px; font-weight: 500; color: #0f1115; max-height: 285px; overflow-y: auto; margin-bottom: 12px; padding-right: 4px; #white-space: pre-wrap; } .meanings div { margin-bottom: 6px; } .dict-tabs { display: flex; font-size: 12px; justify-content: space-between; align-items: center; width: 100%; box-sizing: border-box; margin-bottom: 4px; } .dict-tab { display: inline-flex; align-items: center; justify-content: center; font-size: 11px; color: #555555; background-color: rgba(0, 0, 0, 0.05); padding: 3px 8px; border-radius: 12px; line-height: 1; box-sizing: border-box; text-decoration: none; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .dict-tab:hover { background-color: rgba(0, 0, 0, 0.12); color: #1a1a1a; text-decoration: none; } .loading-wrapper { display: flex; flex-direction: column; gap: 12px; padding: 8px 0; min-height: 120px; } .skeleton-bar { height: 14px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 37%, #f0f0f0 63%); background-size: 400% 100%; border-radius: 4px; animation: skeleton-loading 1.4s ease infinite; } .skeleton-bar.short { width: 40%; } .skeleton-bar.medium { width: 70%; } .skeleton-bar.long { width: 95%; } @keyframes skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } } .config-panel { display: flex; flex-direction: column; height: 100%; padding: 0; box-sizing: border-box; } .config-header { flex-shrink: 0; margin-bottom: 12px; } .config-scroll-content { flex: 1; overflow-y: auto; padding-right: 6px; max-height: 327px; } .config-footer { flex-shrink: 0; padding-top: 12px; margin-top: 8px; display: flex; justify-content: flex-end; gap: 10px; } .config-title { font-size: 16px; font-weight: bold; color: #0f1115; display: flex; justify-content: space-between; align-items: center; } .config-section { display: flex; flex-direction: column; gap: 6px; margin-bottom: 20px; } .config-label-row { display: flex; justify-content: space-between; align-items: center; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .config-label-row a, .reset-prompt-link, .config-title a { color: #61666b; text-decoration: none; font-size: 12px; display: inline-flex; align-items: center; gap: 2px; transition: color 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .config-title span span { color: #61666b; text-decoration: none; font-size: 12px; display: inline-flex; align-items: center; gap: 2px; font-weight: normal; margin-left: 6px; } .switch-view-mode { cursor: pointer; padding: 4px; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .config-title a { font-weight: normal; margin-left: 6px; } .config-label-row a:hover, .reset-prompt-link:hover, .config-title a:hover { color: #0f1115; text-decoration: underline; } .config-label { font-size: 13px; font-weight: 500; color: #0f1115; } .config-label::before { content: "▪"; display: inline-block; margin-right: 6px; font-weight: bold; opacity: 0.7; #transform: translateY(-1px); } .nd-config-label { font-size: 12px; font-weight: 500; color: #61666b; } .rd-config-label { font-size: 12px; color: #61666b; } .config-checkbox-group, .config-radio-group { display: flex; flex-wrap: wrap; gap: 6px 10px; font-size: 13px; } .config-checkbox-label, .config-radio-label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; color: #61666b; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .config-actions-bar { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding: 5px 0px; } .word-list-footer { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding: 10px 0px 5px; } .cancel-config-btn { padding: 8px 18px; background: none; color: #0f1115; border: 1px solid #dadce0; border-radius: 10px; cursor: pointer; font-size: 13px; font-weight: 500; user-select: none; transition: background 0.2s, color 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .cancel-config-btn:hover { background: rgba(38,49,72,.06); } .save-config-btn { padding: 8px 18px; background: #1a1d21; color: #ffffff; border: none; border-radius: 10px; cursor: pointer; font-size: 13px; font-weight: 500; user-select: none; transition: background 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .save-config-btn:hover { background: #33373d; } .tts-input-row { display: flex; gap: 6px; width: 100%; } .tts-btn-row, .ds-btn-row { display: flex; gap: 10px; justify-content: space-between; align-items: center; } .tts-speed-box, ds-speed-box { display: flex; align-items: center; color: #61666b; gap: 4px; } .tts-action-btn { padding: 4px 10px; border: 1px solid #dadce0; border-radius: 10px; background: none; color: #61666b; font-size: 12px; font-weight: 500; cursor: pointer; user-select: none; transition: all 0.2s; height: 28px; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .tts-action-btn:hover { background: #f1f3f4; color: #0f1115; } .tts-status-msg, .ds-status-msg, .upstash-status-msg { font-size: 12px; margin-top: 2px; display: none; } .tts-status-msg.success, .ds-status-msg.success, .upstash-status-msg.success { color: #1e8e3e; display: block; } .tts-status-msg.error, .ds-status-msg.error, .upstash-status-msg.error { color: #d93025; display: block; } .ds-key-config-card { display: flex; flex-direction: column; gap: 12px; width: 100%; padding: 14px 16px; background: #f8f9fa; border: 1px solid #dadce0; border-radius: 10px; margin: 8px 0; box-sizing: border-box; } .ds-key-config-header { display: flex; align-items: center; justify-content: space-between; font-size: 13px; color: #d93025; font-weight: 500; } .ds-key-config-header a { color: #61666b; text-decoration: none; font-size: 12px; display: inline-flex; align-items: center; gap: 2px; transition: color 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .ds-key-config-header a:hover { color: #0f1115; text-decoration: underline; } .ds-key-form { display: flex; gap: 8px; width: 100%; } .input-with-eye { position: relative; flex: 1; display: flex; align-items: center; } .input-with-eye .key-input-field { width: 100%; padding-right: 32px; } .toggle-pwd-btn { position: absolute; right: 6px; background: none; border: none; padding: 4px; margin: 0; cursor: pointer; color: #61666b; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; outline: none; transition: color 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .toggle-pwd-btn:hover { color: #0f1115; } .toggle-pwd-btn svg { width: 16px; height: 16px; display: block; } .key-input-field { flex: 1; height: 34px; padding: 0 10px; border: 1px solid #dadce0; border-radius: 10px; font-size: 13px; outline: none; background: #ffffff; color: #0f1115; transition: border-color 0.2s, box-shadow 0.2s; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .key-input-field:focus { border-color: #0f1115; } .save-key-btn { height: 34px; padding: 0 16px; background: #1a1d21; color: #ffffff; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; white-space: nowrap; -webkit-tap-highlight-color: transparent; -webkit-appearance: none; } .save-key-btn:hover { background: #33373d; } .save-key-btn:active { background: #000000; } :host(.theme-dark) .popup-container { background: #292a2d; color: #e8eaed; box-shadow: 0 4px 20px rgba(0,0,0,0.5); } .zdww { margin-top: 12px; border: 1px solid #e0e0e0; border-radius: 10px; overflow: hidden; } .zdw { padding: 8px 12px; background: #f8f9fa; cursor: pointer; user-select: none; } .zdt { font-size: 13px; font-weight: 500; } .prompts-arrow { font-size: 16px; color: #61666b; } .prompts-content { padding: 12px 16px; background: #ffffff; } :host(.theme-dark) .engine-icon-btn:hover { background: rgba(255, 255, 255, 0.1); } :host(.theme-dark) .engine-icon-btn.active { background: #3c4043; } :host(.theme-dark) .popover-trigger { color: #cfd3d6; } :host(.theme-dark) .popover-trigger:hover, :host(.theme-dark) .custom-popover-select.open .popover-trigger { background: #3c4043; } :host(.theme-dark) .popover-dropdown { background: #292a2d; box-shadow: 0 4px 16px rgba(0,0,0,0.5); } :host(.theme-dark) .popover-option { color: #cfd3d6; } :host(.theme-dark) .popover-option:hover { background: #3c4043; } :host(.theme-dark) .dict-tab { color: #c0c0c0; background-color: rgba(255, 255, 255, 0.08); } :host(.theme-dark) .dict-tab:hover { background-color: rgba(255, 255, 255, 0.18); color: #ffffff; } :host(.theme-dark) .gt-lang-bar { color: #cfd3d6; } :host(.theme-dark) .card-header, :host(.theme-dark) .phonetic-item, :host(.theme-dark) .gt-pinyin, :host(.theme-dark) .config-label, :host(.theme-dark) .config-title, :host(.theme-dark) .tts-speed-box, :host(.theme-dark) .ds-speed-box { color: #f9fafb; } :host(.theme-dark) .nd-config-label { color: #cfd3d6; } :host(.theme-dark) .config-title span{ color: #f9fafb } :host(.theme-dark) .word-title, :host(.theme-dark) .meanings, :host(.theme-dark) .gt-text-source, :host(.theme-dark) .gt-text-target { color: #f9fafb; } :host(.theme-dark) .gt-icon-btn:hover, :host(.theme-dark) .mini-icon-btn:hover { background: #3c4043; color: #e8eaed; } :host(.theme-dark) .header-action-btn, :host(.theme-dark) .favorite-btn { color: #cfd3d6; } :host(.theme-dark) .header-action-btn:hover, :host(.theme-dark) .header-action-btn:active, :host(.theme-dark) .favorite-btn:hover, :host(.theme-dark) .favorite-btn:active { background: #3c4043; } :host(.theme-dark) .header-action-btn.close-btn:hover, :host(.theme-dark) .header-action-btn.close-btn:active { color: #e8eaed; } :host(.theme-dark) .gt-icon-btn { color: #cfd3d6; } :host(.theme-dark) .gt-divider, :host(.theme-dark) .config-title, :host(.theme-dark) .config-actions-bar { border-color: #3c4043; } :host(.theme-dark) .gt-divider { background-color: #4a4e51; } :host(.theme-dark) .phonetic-label, :host(.theme-dark) .phonetic-val, :host(.theme-dark) .pronounce-button { background: transparent; color: #cfd3d6; } :host(.theme-dark) .pronounce-button:hover { background: #3c4043; color: #e8eaed; } :host(.theme-dark) .editable-word:hover, :host(.theme-dark) .editable-word:focus { background-color: rgba(255, 255, 255, 0.08); } :host(.theme-dark) .skeleton-bar { background: linear-gradient(90deg, #3c4043 25%, #4a4e51 37%, #3c4043 63%); background-size: 400% 100%; } :host(.theme-dark) .ds-key-config-card { background: #303134; border-color: #3c4043; } :host(.theme-dark) .ds-key-config-header { color: #f28b82; } :host(.theme-dark) .ds-key-config-header a, :host(.theme-dark) .config-label-row a, :host(.theme-dark) .reset-prompt-link, :host(.theme-dark) .config-title a { color: #cfd3d6; } :host(.theme-dark) .config-title span span { color: #cfd3d6; } :host(.theme-dark) .ds-key-config-header a:hover, :host(.theme-dark) .config-label-row a:hover, :host(.theme-dark) .reset-prompt-link:hover, :host(.theme-dark) .config-title a:hover { color: #f9fafb; } :host(.theme-dark) .key-input-field { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-dark) .key-input-field:focus { border-color: #f9fafb; } :host(.theme-dark) .toggle-pwd-btn { color: #cfd3d6; } :host(.theme-dark) .toggle-pwd-btn:hover { color: #f9fafb; } :host(.theme-dark) .save-key-btn { background: #e8eaed; color: #0f1115; } :host(.theme-dark) .save-key-btn:hover { background: #ffffff; } :host(.theme-dark) .tts-action-btn { border-color: #61666b; color: #e8eaed; } :host(.theme-dark) .tts-action-btn:hover { background: #3c4043; } :host(.theme-dark) .config-checkbox-label, :host(.theme-dark) .config-radio-label { color: #adb2b8; } :host(.theme-dark) .cancel-config-btn { border-color:#61666b; color: #f9fafb; } :host(.theme-dark) .cancel-config-btn:hover { background: rgba(255,255,255,.08); } :host(.theme-dark) .save-config-btn { background: #e8eaed; color: #0f1115; } :host(.theme-dark) .save-config-btn:hover { background: #ffffff; } :host(.theme-light) input[type="radio"], :host(.theme-light) input[type="checkbox"] { color-scheme: light; accent-color: #0f1115; } :host(.theme-dark) input[type="radio"], :host(.theme-dark) input[type="checkbox"] { color-scheme: dark; accent-color: #e8eaed; } :host(.theme-system) input[type="radio"], :host(.theme-system) input[type="checkbox"] { color-scheme: light dark; accent-color: #0f1115; } :host(.theme-dark) .config-scroll-content::-webkit-scrollbar-thumb { background: rgba(200, 200, 200, calc(0.3 * var(--scrollbar-opacity, 1))); } :host(.theme-dark) .config-scroll-content::-webkit-scrollbar-thumb:hover { background: rgba(200, 200, 200, 0.6); } :host(.theme-dark) .zdw { background: #303134; } :host(.theme-dark) .zdww { border-color: #3c4043; } :host(.theme-dark) .prompts-content { background: #292a2d; } :host(.theme-dark) .cf-hint, :host(.theme-dark) .rd-config-label { color: #adb2b8; } @media (prefers-color-scheme: dark) { :host(.theme-system) input[type="radio"], :host(.theme-system) input[type="checkbox"] { accent-color: #e8eaed; } :host(.theme-system) .popup-container { background: #292a2d; color: #e8eaed; box-shadow: 0 4px 20px rgba(0,0,0,0.5); } :host(.theme-system) .engine-icon-btn:hover { background: rgba(255, 255, 255, 0.1); } :host(.theme-system) .engine-icon-btn.active { background: #3c4043; } :host(.theme-system) .popover-trigger { color: #cfd3d6; } :host(.theme-system) .popover-trigger:hover, :host(.theme-system) .custom-popover-select.open .popover-trigger { background: #3c4043; } :host(.theme-system) .popover-dropdown { background: #292a2d; box-shadow: 0 4px 16px rgba(0,0,0,0.5); } :host(.theme-system) .popover-option { color: #cfd3d6; } :host(.theme-system) .popover-option:hover { background: #3c4043; } :host(.theme-system) .dict-tab { color: #c0c0c0; background-color: rgba(255, 255, 255, 0.08); } :host(.theme-system) .dict-tab:hover { background-color: rgba(255, 255, 255, 0.18); color: #ffffff; } :host(.theme-system) .gt-lang-bar { color: #cfd3d6; } :host(.theme-system) .card-header, :host(.theme-system) .phonetic-item, :host(.theme-system) .gt-pinyin, :host(.theme-system) .config-label, :host(.theme-system) .config-title, :host(.theme-system) .tts-speed-box, :host(.theme-system) .ds-speed-box { color: #f9fafb; } :host(.theme-system) .nd-config-label { color: #cfd3d6; } :host(.theme-system) .config-title span{ color: #f9fafb } :host(.theme-system) .word-title, :host(.theme-system) .meanings, :host(.theme-system) .gt-text-source, :host(.theme-system) .gt-text-target { color: #f9fafb; } :host(.theme-system) .gt-icon-btn:hover, :host(.theme-system) .mini-icon-btn:hover { background: #3c4043; color: #e8eaed; } :host(.theme-system) .header-action-btn, :host(.theme-system) .favorite-btn{ color: #cfd3d6; } :host(.theme-system) .header-action-btn:hover, :host(.theme-system) .header-action-btn:active, :host(.theme-system) .favorite-btn:hover, :host(.theme-system) .favorite-btn:active { background: #3c4043; } :host(.theme-system) .header-action-btn.close-btn:hover, :host(.theme-system) .header-action-btn.close-btn:active { color: #e8eaed; } :host(.theme-system) .gt-icon-btn { color: #cfd3d6; } :host(.theme-system) .gt-divider, :host(.theme-system) .config-title, :host(.theme-system) .config-actions-bar { border-color: #3c4043; } :host(.theme-system) .gt-divider { background-color: #4a4e51; } :host(.theme-system) .phonetic-label, :host(.theme-system) .phonetic-val, :host(.theme-system) .pronounce-button { background: transparent; color: #cfd3d6; } :host(.theme-system) .pronounce-button:hover { background: #3c4043; color: #e8eaed; } :host(.theme-system) .dict-tabs { border-top-color: #3c4043; } :host(.theme-system) .editable-word:hover, :host(.theme-system) .editable-word:focus { background-color: rgba(255, 255, 255, 0.08); } :host(.theme-system) .skeleton-bar { background: linear-gradient(90deg, #3c4043 25%, #4a4e51 37%, #3c4043 63%); background-size: 400% 100%; } :host(.theme-system) .ds-key-config-card { background: #303134; border-color: #3c4043; } :host(.theme-system) .ds-key-config-header { color: #f28b82; } :host(.theme-system) .ds-key-config-header a, :host(.theme-system) .config-label-row a, :host(.theme-system) .reset-prompt-link, :host(.theme-system) .config-title a{ color: #cfd3d6; } :host(.theme-system) .config-title span span { color: #cfd3d6; } :host(.theme-system) .ds-key-config-header a:hover, :host(.theme-system) .config-label-row a:hover, :host(.theme-system) .reset-prompt-link:hover, :host(.theme-system) .config-title a:hover{ color: #f9fafb; } :host(.theme-system) .key-input-field { background: #292a2d; border-color: #61666b; color: #f9fafb; } :host(.theme-system) .key-input-field:focus { border-color: #f9fafb; } :host(.theme-system) .toggle-pwd-btn { color: #cfd3d6; } :host(.theme-system) .toggle-pwd-btn:hover { color: #f9fafb; } :host(.theme-system) .save-key-btn { background: #e8eaed; color: #0f1115; } :host(.theme-system) .save-key-btn:hover { background: #ffffff; } :host(.theme-system) .tts-action-btn { border-color: #61666b; color: #e8eaed; } :host(.theme-system) .tts-action-btn:hover { background: #3c4043; } :host(.theme-system) .config-checkbox-label, :host(.theme-system) .config-radio-label { color: #adb2b8; } :host(.theme-system) .cancel-config-btn { border-color:#61666b; color: #f9fafb; } :host(.theme-system) .cancel-config-btn:hover { background: rgba(255,255,255,.08); } :host(.theme-system) .save-config-btn { background: #e8eaed; color: #0f1115; } :host(.theme-system) .save-config-btn:hover { background: #ffffff; } :host(.theme-system) .config-scroll-content::-webkit-scrollbar-thumb { background: rgba(200, 200, 200, calc(0.3 * var(--scrollbar-opacity, 1))); } :host(.theme-system) .config-scroll-content::-webkit-scrollbar-thumb:hover { background: rgba(200, 200, 200, 0.6); } :host(.theme-system) .zdw { background: #303134; } :host(.theme-system) .zdww { border-color: #3c4043; } :host(.theme-system) .prompts-content { background: #292a2d; } :host(.theme-system) .cf-hint, :host(.theme-system) .rd-config-label { color: #adb2b8; } } ${extraStyle} `; const EYE_SVG_OPEN = ``; const EYE_SVG_CLOSED = ``; const NOTEBOOK_ICON_SVG = ``; const NOTEBOOK_ICON_SVG_ADD = ``; const TRASH_ICON_SVG = ``; GM_registerMenuCommand("⚙️ 页面配置项", () => { const selectionRect = getSelectionRect() || { left: (window.innerWidth - 380) / 2, top: (window.innerHeight - 495) / 2, width: 380, height: 495 }; renderConfigPage(selectionRect); }); function applyThemeToPopup() { if (!popup) return; const theme = getThemeMode(); popup.classList.remove('theme-light', 'theme-dark', 'theme-system'); if (theme === 'light') { popup.classList.add('theme-light'); } else if (theme === 'dark') { popup.classList.add('theme-dark'); } else { popup.classList.add('theme-system'); } } function createCustomPopoverHtml(id, options, currentValue) { const currentObj = options.find(o => o.value === currentValue) || options[0]; const arrowSvg = ``; const checkSvg = ``; const optionsHtml = options.map(opt => `
${escapeHtml(opt.label)} ${checkSvg}
`).join(''); return `
${optionsHtml}
`; } function renderKeyConfigCard(errorMsg = '未配置 DeepSeek API Key', placeholder = '请输入 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') { return `
⚠️ ${errorMsg} 查看教程
`; } function sendDeepSeekMessages(messages) { const apiKey = GM_getValue("deepseek_api_key", ""); return new Promise((resolve, reject) => { if (!apiKey) { const htmlKeyInput = renderKeyConfigCard('未配置 DeepSeek API Key', '请输入 sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); reject({ message: htmlKeyInput, isHtml: true }); return; } GM_xmlhttpRequest({ method: 'POST', url: 'https://api.deepseek.com/chat/completions', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'deepseek-chat', messages: messages, temperature: 0.3 }), timeout: 15000, onload(res) { if (res.status >= 200 && res.status < 300) { try { const data = JSON.parse(res.responseText); const content = data.choices[0]?.message?.content?.trim(); if (content) { resolve(content); } else { reject(new Error('DeepSeek 返回内容为空')); } } catch (e) { reject(new Error('DeepSeek 响应解析失败')); } } else if (res.status === 401) { const htmlInvalidKey = renderKeyConfigCard('API Key 无效或过期', '请重新输入 API Key'); reject({ message: htmlInvalidKey, isHtml: true }); } else if (res.status === 402) { reject(new Error('DeepSeek 账户余额不足')); } else { reject(new Error(`DeepSeek 请求失败 (HTTP ${res.status})`)); } }, ontimeout() { reject(new Error('DeepSeek 请求超时,请检查网络')); }, onerror() { reject(new Error('DeepSeek 网络连接失败')); } }); }); } function queryDeepSeek(word, tl = 'zh-Hans') { const isZh = tl === 'zh-Hans' || tl === 'zh-CN'; const targetLangName = isZh ? '中文(简体)' : '英文'; const isSingleWord = !word.trim().includes(' ') && word.length < 30; const promptKey = isSingleWord ? 'deepseek_prompt_word' : 'deepseek_prompt_sentence'; let systemPrompt = GM_getValue(promptKey, isSingleWord ? DEFAULT_DEEPSEEK_PROMPT_WORD : DEFAULT_DEEPSEEK_PROMPT_SENTENCE); systemPrompt = systemPrompt.replace(/\{word\}/g, word).replace(/\{targetLang\}/g, targetLangName); const messages = [ { role: 'system', content: systemPrompt }, { role: 'user', content: word } ]; return sendDeepSeekMessages(messages).then(content => { return { word, translation: content, pinyin: '', srcPinyin: '', srcLang: 'auto' }; }); } function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function showToast(msg) { if (!popup) return; const MAX_LENGTH = 80; if (typeof msg === 'string' && msg.length > MAX_LENGTH) { msg = msg.slice(0, MAX_LENGTH) + '…'; } const shadowRoot = popup.shadowRoot; let toast = shadowRoot.querySelector('.toast-msg'); if (!toast) { toast = document.createElement('div'); toast.className = 'toast-msg'; shadowRoot.querySelector('.popup-container').appendChild(toast); } toast.textContent = msg; popup.classList.add('show-toast'); clearTimeout(popup.toastTimeout); popup.toastTimeout = setTimeout(() => { popup.classList.remove('show-toast'); }, 2000); } function getTrustedHtml(html) { if (window.trustedTypes) { let policy = window.trustedTypes.getPolicy && window.trustedTypes.getPolicy('translookup'); if (!policy && window.trustedTypes.createPolicy) { try { policy = window.trustedTypes.createPolicy('translookup', { createHTML: value => value }); } catch (e) { policy = window.trustedTypes.getPolicy && window.trustedTypes.getPolicy('translookup'); } } if (policy) return policy.createHTML(html); } return html; } function setElementHtml(element, html) { const trustedHtml = getTrustedHtml(html); element.innerHTML = trustedHtml; convertTitlesToCustomTooltips(element); bindPopupEvents(); afterRenderBindScrollbars(); } function afterRenderBindScrollbars() { const root = getPopupRoot(); if (!root) return; const conv = root.querySelector('#ds-conversation'); if (conv) enableAutoHideScrollbar(conv); const scrollBoxes = root.querySelectorAll('.gt-scroll-box, .meanings'); scrollBoxes.forEach(el => enableAutoHideScrollbar(el)); const configScroll = root.querySelector('.config-scroll-content'); if (configScroll) enableAutoHideScrollbar(configScroll); const notesList = root.querySelector('.notes-list-container'); if (notesList) enableAutoHideScrollbar(notesList); const noteView = root.querySelector('.note-view-content'); if (noteView) enableAutoHideScrollbar(noteView); const noteEditor = root.querySelector('.note-editor-textarea'); if (noteEditor) enableAutoHideScrollbar(noteEditor); const deletedContainer = root.querySelector('.deleted-notes-container'); if (deletedContainer) enableAutoHideScrollbar(deletedContainer); } let customTooltip = null; let customTooltipTimer = null; let customTooltipTarget = null; let customTooltipHandlersInstalled = false; function ensureCustomTooltip() { if (customTooltip && document.body.contains(customTooltip)) { return customTooltip; } customTooltip = document.createElement('div'); customTooltip.id = 'translookup-custom-tooltip'; customTooltip.setAttribute('role', 'tooltip'); customTooltip.setAttribute('aria-hidden', 'true'); customTooltip.style.cssText = ` position: fixed; z-index: 2147483647; display: block; max-width: 280px; padding: 6px 10px; border-radius: 7px; background: rgba(32, 33, 36, 0.94); color: #fff; font-family: Google Sans, Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; font-size: 12px; font-weight: 400; line-height: 1.4; white-space: nowrap; word-break: break-word; box-sizing: border-box; box-shadow: 0 3px 8px rgba(0,0,0,.18), 0 1px 3px rgba(0,0,0,.12); pointer-events: none; user-select: none; opacity: 0; visibility: hidden; transform: translateY(4px); transition: opacity .12s ease, transform .12s ease, visibility .12s; `; document.body.appendChild(customTooltip); return customTooltip; } function convertTitlesToCustomTooltips(root) { const MAX_TOOLTIP_LENGTH = 20; if (!root || typeof root.querySelectorAll !== 'function') return; const processElement = (el) => { const title = el.getAttribute('title'); if (title) { let trimmed = title.trim(); if (trimmed.length > MAX_TOOLTIP_LENGTH) { trimmed = trimmed.slice(0, MAX_TOOLTIP_LENGTH) + '…'; } el.setAttribute('data-custom-tooltip', trimmed); el.removeAttribute('title'); } }; if (root.nodeType === Node.ELEMENT_NODE && root.hasAttribute('title')) { processElement(root); } root.querySelectorAll('[title]').forEach(processElement); } function getTooltipTargetFromEvent(event) { const path = typeof event.composedPath === 'function' ? event.composedPath() : []; for (const node of path) { if ( node && node.nodeType === Node.ELEMENT_NODE && node.hasAttribute?.('data-custom-tooltip') ) { return node; } } return getClosestElement(event.target, '[data-custom-tooltip]'); } function positionCustomTooltip(target, tooltip) { if (!target || !tooltip) return; const rect = target.getBoundingClientRect(); if (!rect || (!rect.width && !rect.height)) return; tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; tooltip.style.display = 'block'; tooltip.style.transform = 'translateY(0)'; const tooltipRect = tooltip.getBoundingClientRect(); const gap = 8; const edge = 6; let left = rect.left + (rect.width - tooltipRect.width) / 2; let top = rect.top - tooltipRect.height - gap; let bottomPlacement = false; if (left < edge) { left = edge; } if (left + tooltipRect.width > window.innerWidth - edge) { left = window.innerWidth - tooltipRect.width - edge; } if (top < edge) { top = rect.bottom + gap; bottomPlacement = true; } if (top + tooltipRect.height > window.innerHeight - edge) { top = Math.max(edge, rect.top - tooltipRect.height - gap); bottomPlacement = false; } tooltip.style.left = `${Math.round(left)}px`; tooltip.style.top = `${Math.round(top)}px`; tooltip.dataset.placement = bottomPlacement ? 'bottom' : 'top'; } function showCustomTooltip(target, text) { if (!target || !text) return; const tooltip = ensureCustomTooltip(); if (tooltip.parentNode === document.body) { document.body.appendChild(tooltip); } tooltip.style.zIndex = '2147483647'; clearTimeout(customTooltipTimer); customTooltipTarget = target; tooltip.textContent = text; tooltip.setAttribute('aria-hidden', 'false'); tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; tooltip.style.transform = 'translateY(4px)'; positionCustomTooltip(target, tooltip); customTooltipTimer = setTimeout(() => { if (customTooltipTarget !== target) return; tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; tooltip.style.transform = 'translateY(0)'; }, 500); } function hideCustomTooltip() { clearTimeout(customTooltipTimer); customTooltipTarget = null; if (!customTooltip) return; customTooltip.style.opacity = '0'; customTooltip.style.visibility = 'hidden'; customTooltip.style.transform = 'translateY(4px)'; customTooltip.setAttribute('aria-hidden', 'true'); } function initCustomTooltips(shadowRoot) { if (!shadowRoot) return; ensureCustomTooltip(); convertTitlesToCustomTooltips(shadowRoot); if (!customTooltipHandlersInstalled) { customTooltipHandlersInstalled = true; window.addEventListener('scroll', hideCustomTooltip, true); window.addEventListener('resize', hideCustomTooltip); document.addEventListener('keydown', event => { if (event.key === 'Escape') hideCustomTooltip(); }, true); } if (!shadowRoot.__customTooltipObserver) { const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { convertTitlesToCustomTooltips(node); } }); } else if ( mutation.type === 'attributes' && mutation.attributeName === 'title' && mutation.target.nodeType === Node.ELEMENT_NODE ) { convertTitlesToCustomTooltips(mutation.target); } } }); observer.observe(shadowRoot, { subtree: true, childList: true, attributes: true, attributeFilter: ['title'] }); shadowRoot.__customTooltipObserver = observer; } if (!shadowRoot.__customTooltipEventsBound) { shadowRoot.__customTooltipEventsBound = true; shadowRoot.addEventListener('mouseover', event => { if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { return; } const target = getTooltipTargetFromEvent(event); if (!target) return; const related = event.relatedTarget; if (related && target.contains?.(related)) return; const text = target.getAttribute('data-custom-tooltip'); if (!text) return; showCustomTooltip(target, text); }, true); shadowRoot.addEventListener('mouseout', event => { const target = getTooltipTargetFromEvent(event); if (!target) return; const related = event.relatedTarget; if (related && target.contains?.(related)) return; if (customTooltipTarget === target) { hideCustomTooltip(); } }, true); } } function createPopup() { if (popup) return popup; popup = document.createElement('div'); popup.id = 'translookup-popup'; popup.style.display = 'none'; popup.style.position = 'absolute'; popup.style.zIndex = '2147483646'; popup.style.maxWidth = `${document.documentElement.clientWidth - 20}px`; if (!popup.shadowRoot) popup.attachShadow({ mode: 'open' }); const shadowRoot = popup.shadowRoot; if (!shadowRoot.querySelector('style[data-translookup-style]')) { const styleEl = document.createElement('style'); styleEl.setAttribute('data-translookup-style', 'true'); styleEl.textContent = style; shadowRoot.appendChild(styleEl); } const container = document.createElement('div'); container.className = 'popup-container'; const arrow = document.createElement('div'); arrow.className = 'popup-arrow'; container.appendChild(arrow); const contentWrapper = document.createElement('div'); contentWrapper.className = 'popup-content'; container.appendChild(contentWrapper); shadowRoot.appendChild(container); initCustomTooltips(shadowRoot); shadowRoot.addEventListener('click', onPopupClick); document.body.appendChild(popup); applyThemeToPopup(); return popup; } function getPopupRoot() { return popup?.shadowRoot || popup; } function isEventInsidePopup(event) { return !!popup && event.composedPath && event.composedPath().includes(popup); } function getClosestElement(node, selector) { return node && typeof node.closest === 'function' ? node.closest(selector) : null; } function saveDeepSeekCache() { const cacheEntry = currentCache.deepseek; if (!cacheEntry) return; cacheEntry.history = deepseekState.history.slice(); } function bindPopupEvents() { const popupRoot = getPopupRoot(); if (!popupRoot) return; const closeBtn = popupRoot.querySelector('.close-btn'); if (closeBtn) closeBtn.onclick = (e) => { e.stopPropagation(); hidePopup(); }; const editables = popupRoot.querySelectorAll('.editable-word'); editables.forEach(el => { el.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); this.blur(); } }); el.addEventListener('blur', function(e) { const newText = this.textContent.replace(/\s+/g, ' ').trim(); if (newText && newText !== lastSelectionText) { setTimeout(() => { if (newText !== lastSelectionText) { lastSelectionText = newText; currentCache.deepseek = { data: null, history: [] }; deepseekState = { history: [] }; fetchAndRender(newText); } }, 150); } else if (!newText) { this.textContent = lastSelectionText; } }); }); attachDeepSeekEvents(); } function attachDeepSeekEvents() { const popupRoot = getPopupRoot(); if (!popupRoot) return; const input = popupRoot.querySelector('#ds-input'); const sendBtn = popupRoot.querySelector('#ds-send-btn'); if (!input || !sendBtn) return; if (input._listener) return; input._listener = true; const sendMessage = () => { const text = input.value.trim(); if (!text) return; input.value = ''; deepseekState.history.push({ role: 'user', content: text }); saveDeepSeekCache(); const conv = popupRoot.querySelector('#ds-conversation'); if (conv) { const placeholder = conv.querySelector('#ds-loading-placeholder'); if (placeholder) placeholder.remove(); const wrapper = document.createElement('div'); wrapper.className = 'ds-message-wrapper user-wrapper'; const userDiv = document.createElement('div'); userDiv.className = 'ds-message user'; userDiv.textContent = text; wrapper.appendChild(userDiv); const actionsRow = document.createElement('div'); actionsRow.className = 'ds-actions-row'; const copyBtn = document.createElement('button'); copyBtn.className = 'gt-icon-btn copy-btn'; copyBtn.setAttribute('data-text', encodeURIComponent(text)); copyBtn.title = '复制'; copyBtn.innerHTML = getTrustedHtml(``); copyBtn.style.opacity = '1'; actionsRow.appendChild(copyBtn); const playBtn = document.createElement('button'); playBtn.className = 'gt-icon-btn play-btn edge-tts-audio'; const isChinese = /[\u4e00-\u9fa5]/.test(text); const voice = isChinese ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; playBtn.setAttribute('data-text', encodeURIComponent(text)); playBtn.setAttribute('data-voice', voice); playBtn.title = '播放'; playBtn.innerHTML = getTrustedHtml(``); playBtn.style.opacity = '1'; actionsRow.appendChild(playBtn); wrapper.appendChild(actionsRow); conv.appendChild(wrapper); conv.scrollTop = conv.scrollHeight; } const loadingDiv = document.createElement('div'); loadingDiv.className = 'ds-loading'; loadingDiv.textContent = '思考中... ( 请勿离开此页面 )'; loadingDiv.id = 'ds-loading-temp'; if (conv) { conv.appendChild(loadingDiv); conv.scrollTop = conv.scrollHeight; } const systemPrompt = GM_getValue('deepseek_prompt_chat', DEFAULT_DEEPSEEK_PROMPT_CHAT); const messages = [ { role: 'system', content: systemPrompt }, ...deepseekState.history ]; sendDeepSeekMessages(messages) .then(response => { const temp = popupRoot.querySelector('#ds-loading-temp'); if (temp) temp.remove(); deepseekState.history.push({ role: 'assistant', content: response }); const initialText = currentCache.text; const cacheKey = getCacheKey('deepseek', initialText, targetLang); const existing = translationCache.get(cacheKey); if (existing) { existing.history = deepseekState.history; translationCache.set(cacheKey, existing); } else { const data = currentCache.deepseek.data; setTranslationCache(cacheKey, { data, history: deepseekState.history }); } saveDeepSeekCache(); const conv2 = popupRoot.querySelector('#ds-conversation'); if (conv2) { const wrapper = document.createElement('div'); wrapper.className = 'ds-message-wrapper'; const aiDiv = document.createElement('div'); aiDiv.className = 'ds-message assistant'; const span = document.createElement('span'); span.className = 'message-text'; aiDiv.appendChild(span); wrapper.appendChild(aiDiv); const actionsRow = document.createElement('div'); actionsRow.className = 'ds-actions-row'; const copyBtn = document.createElement('button'); copyBtn.className = 'gt-icon-btn copy-btn'; copyBtn.setAttribute('data-text', encodeURIComponent(response)); copyBtn.title = '复制'; copyBtn.innerHTML = getTrustedHtml(``); copyBtn.style.opacity = '0'; actionsRow.appendChild(copyBtn); const playBtn = document.createElement('button'); playBtn.className = 'gt-icon-btn play-btn edge-tts-audio'; const isChineseResp = /[\u4e00-\u9fa5]/.test(response); const voiceResp = isChineseResp ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; playBtn.setAttribute('data-text', encodeURIComponent(response)); playBtn.setAttribute('data-voice', voiceResp); playBtn.title = '播放'; playBtn.innerHTML = getTrustedHtml(``); playBtn.style.opacity = '0'; actionsRow.appendChild(playBtn); const addNoteBtn = document.createElement('button'); addNoteBtn.className = 'gt-icon-btn add-note-btn'; const idx = deepseekState.history.length - 1; addNoteBtn.setAttribute('data-index', idx); addNoteBtn.title = '添加到笔记本'; addNoteBtn.innerHTML = getTrustedHtml(NOTEBOOK_ICON_SVG_ADD); addNoteBtn.style.opacity = '0'; actionsRow.appendChild(addNoteBtn); wrapper.appendChild(actionsRow); conv2.appendChild(wrapper); const speedMs = getTypewriterSpeedMs(); typewriter(span, response, speedMs, true, () => { copyBtn.style.opacity = '1'; playBtn.style.opacity = '1'; addNoteBtn.style.opacity = '1'; conv2.scrollTop = conv2.scrollHeight; }); } }) .catch(error => { const temp = popupRoot.querySelector('#ds-loading-temp'); if (temp) temp.remove(); let errMsg = error.message || '请求失败'; if (error.isHtml) errMsg = error.message; showToast('发送失败: ' + errMsg); deepseekState.history.pop(); saveDeepSeekCache(); const conv3 = popupRoot.querySelector('#ds-conversation'); if (conv3) { const lastMsg = conv3.querySelector('.ds-message:last-child'); if (lastMsg && lastMsg.classList.contains('user')) { lastMsg.remove(); } } }); }; sendBtn.addEventListener('click', sendMessage); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } }); } function getWebAudioContext() { if (!webAudioCtx) { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!AudioContextClass) throw new Error('Web Audio API not supported'); webAudioCtx = new AudioContextClass(); } if (webAudioCtx.state === 'suspended') { webAudioCtx.resume().catch(() => {}); } return webAudioCtx; } function stopWebAudio() { if (webAudioSource) { try { webAudioSource.stop(); webAudioSource.disconnect(); } catch (e) {} webAudioSource = null; } if (currentPreciseHighlightCleanup) { try { currentPreciseHighlightCleanup(); } catch (e) {} currentPreciseHighlightCleanup = null; } try { const root = getPopupRoot(); if (root) { root.querySelectorAll('.gt-text-source, .gt-text-target').forEach(el => { unwrapAllTtsSpans(el); }); } } catch (e) {} } function playAudioBuffer(arrayBuffer) { return new Promise((resolve, reject) => { const ctx = getWebAudioContext(); if (ctx.state === 'suspended') { ctx.resume().catch(e => reject(e)); } ctx.decodeAudioData(arrayBuffer, (audioBuffer) => { stopWebAudio(); const source = ctx.createBufferSource(); source.buffer = audioBuffer; source.connect(ctx.destination); source.onended = () => { if (webAudioSource === source) { webAudioSource = null; } source.disconnect(); resolve(); }; const startAt = ctx.currentTime + 0.01; source.start(startAt); webAudioSource = source; resolve(); }, (err) => { reject(new Error('音频解码失败: ' + err.message)); }); }); } function stopCurrentAudio() { stopWebAudio(); if (currentAudio) { try { currentAudio.pause(); currentAudio.currentTime = 0; if (currentAudio.blobUrl) { URL.revokeObjectURL(currentAudio.blobUrl); currentAudio.blobUrl = null; } currentAudio.src = ''; currentAudio.load(); currentAudio.onended = null; currentAudio.onerror = null; } catch(e) {} currentAudio = null; currentAudioKey = null; } if (typeof GoogleService !== 'undefined' && GoogleService.stop) { GoogleService.stop(); } } function requestTTSBlob(apiUrl, text, voice, speedKey = getTTSSpeed()) { return new Promise((resolve, reject) => { if (!apiUrl) { reject(new Error('未配置 TTS 节点')); return; } const speedVal = TTS_SPEED_MAP[speedKey]?.value || '+0%'; GM_xmlhttpRequest({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ input: text, voice: voice, speed: speedVal, pitch: "0", style: "general" }), responseType: 'blob', timeout: 10000, onload(res) { if (res.status >= 200 && res.status < 300 && res.response && res.response.size > 0) { resolve(res.response); } else { reject(new Error(`请求失败 (HTTP ${res.status})`)); } }, ontimeout() { reject(new Error('请求超时')); }, onerror() { reject(new Error('网络请求异常')); } }); }); } function makeEdgeTtsCacheKey(apiUrl, text, voice, speedKey) { return `${apiUrl}\u0001${voice}\u0001${speedKey}\u0001${text}`; } function getEdgeTtsCache(key) { const entry = edgeTtsCache.get(key); if (!entry) return null; const idx = edgeTtsCacheOrder.indexOf(key); if (idx !== -1) edgeTtsCacheOrder.splice(idx, 1); edgeTtsCacheOrder.push(key); return entry; } function setEdgeTtsCache(key, entry) { if (edgeTtsCache.has(key)) { edgeTtsCache.delete(key); const i = edgeTtsCacheOrder.indexOf(key); if (i !== -1) edgeTtsCacheOrder.splice(i, 1); } while (edgeTtsCacheOrder.length >= EDGE_TTS_CACHE_LIMIT) { const oldest = edgeTtsCacheOrder.shift(); if (oldest !== undefined) edgeTtsCache.delete(oldest); } edgeTtsCacheOrder.push(key); edgeTtsCache.set(key, entry); } function clearEdgeTtsCache() { edgeTtsCache.clear(); edgeTtsCacheOrder.length = 0; } function decodeEdgeTtsBase64(base64Audio) { return new Promise((resolve, reject) => { let bytes; try { const binary = atob(base64Audio); bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); } catch (e) { reject(new Error('base64 解码失败')); return; } const ctx = getWebAudioContext(); if (ctx.state === 'suspended') ctx.resume().catch(() => {}); ctx.decodeAudioData(bytes.buffer, resolve, reject); }); } function playTTS(text, voice, highlightContainer = null, speedKey = null, onEnded = null) { if (!text) { if (typeof onEnded === 'function') onEnded(false); return; } const currentSpeedKey = speedKey || getTTSSpeed(); const apiUrl = getTTSApiUrl(); const cacheKey = makeEdgeTtsCacheKey(apiUrl, text, voice, currentSpeedKey); stopCurrentAudio(); const cached = getEdgeTtsCache(cacheKey); if (cached) { playDecodedEdgeTts(cached, highlightContainer, onEnded); return; } const timingsUrl = apiUrl.replace(/\/v1\/audio\/speech$/, '/v1/audio/speech-with-timings'); const speedVal = TTS_SPEED_MAP[currentSpeedKey]?.value || '1'; GM_xmlhttpRequest({ method: 'POST', url: timingsUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ input: text, voice: voice, speed: speedVal, pitch: '0', style: 'general' }), timeout: 25000, onload(res) { if (res.status < 200 || res.status >= 300) { showToast(`TTS 朗读失败 (HTTP ${res.status})`); if (typeof onEnded === 'function') onEnded(false); return; } let data; try { data = JSON.parse(res.responseText); } catch (e) { showToast('TTS 响应解析失败'); if (typeof onEnded === 'function') onEnded(false); return; } if (!data || !data.audio) { showToast('TTS 返回数据异常'); if (typeof onEnded === 'function') onEnded(false); return; } decodeEdgeTtsBase64(data.audio) .then(audioBuffer => { const entry = { audioBuffer, wordBoundaries: data.wordBoundaries || [] }; if (audioBuffer.duration <= EDGE_TTS_CACHE_MAX_DURATION) { setEdgeTtsCache(cacheKey, entry); } playDecodedEdgeTts(entry, highlightContainer, onEnded); }) .catch(err => { console.warn('音频解码失败:', err); showToast('音频解码失败'); if (typeof onEnded === 'function') onEnded(false); }); }, onerror() { showToast('TTS 网络错误'); if (typeof onEnded === 'function') onEnded(false); }, ontimeout() { showToast('TTS 请求超时'); if (typeof onEnded === 'function') onEnded(false); } }); } function playDecodedEdgeTts(entry, highlightContainer, onEnded = null) { const { audioBuffer, wordBoundaries } = entry; const ctx = getWebAudioContext(); if (ctx.state === 'suspended') ctx.resume().catch(() => {}); stopWebAudio(); unwrapAllTtsSpans(highlightContainer); const source = ctx.createBufferSource(); source.buffer = audioBuffer; source.connect(ctx.destination); let spans = null; if (highlightContainer && wordBoundaries && wordBoundaries.length > 0) { const textNodes = []; const walker = document.createTreeWalker(highlightContainer, NodeFilter.SHOW_TEXT); let fullText = ''; let node; while ((node = walker.nextNode())) { textNodes.push({ node: node, start: fullText.length, end: fullText.length + node.nodeValue.length }); fullText += node.nodeValue; } const matches = []; let searchStart = 0; for (const wb of wordBoundaries) { if (!wb.text) continue; const idx = fullText.indexOf(wb.text, searchStart); if (idx === -1) continue; matches.push({ start: idx, end: idx + wb.text.length, wb: wb }); searchStart = idx + wb.text.length; } const wrapped = []; for (let i = matches.length - 1; i >= 0; i--) { const m = matches[i]; const tn = textNodes.find(t => m.start >= t.start && m.end <= t.end); if (!tn) continue; const localStart = m.start - tn.start; const localEnd = m.end - tn.start; try { const range = document.createRange(); range.setStart(tn.node, localStart); range.setEnd(tn.node, localEnd); const span = document.createElement('span'); span.className = 'tts-read-token'; range.surroundContents(span); wrapped.unshift({ span: span, offsetSec: m.wb.offsetSec, durationSec: m.wb.durationSec }); } catch (e) { } } if (wrapped.length > 0) { spans = wrapped; } } const startAt = ctx.currentTime + 0.05; let rafId = 0; let lastIdx = -1; if (spans && spans.length > 0) { const tick = () => { const elapsed = ctx.currentTime - startAt; let idx = -1; for (let i = 0; i < spans.length; i++) { if (elapsed < spans[i].offsetSec + spans[i].durationSec) { idx = i; break; } } if (idx === -1) idx = spans.length - 1; if (idx !== lastIdx) { if (lastIdx >= 0 && spans[lastIdx] && spans[lastIdx].span) { spans[lastIdx].span.classList.remove('tts-reading-token'); } if (spans[idx] && spans[idx].span) { spans[idx].span.classList.add('tts-reading-token'); } lastIdx = idx; } rafId = requestAnimationFrame(tick); }; rafId = requestAnimationFrame(tick); } const restoreHighlight = () => { cancelAnimationFrame(rafId); if (spans && spans.length > 0) { for (const s of spans) { const sp = s.span; if (!sp || !sp.parentNode) continue; const parent = sp.parentNode; while (sp.firstChild) { parent.insertBefore(sp.firstChild, sp); } parent.removeChild(sp); } } if (highlightContainer && highlightContainer.normalize) { highlightContainer.normalize(); } if (currentPreciseHighlightCleanup === restoreHighlight) { currentPreciseHighlightCleanup = null; } }; currentPreciseHighlightCleanup = restoreHighlight; source.onended = () => { if (webAudioSource === source) webAudioSource = null; try { source.disconnect(); } catch (e) {} restoreHighlight(); if (typeof onEnded === 'function') onEnded(true); }; source.start(startAt); webAudioSource = source; } function playGoogleAudio(text, lang = 'en') { GoogleService.playAudio(text, lang); } function playPronunciation(word, accent = 'uk', audioUrl) { const baseUrl = audioUrl || `https://dict.youdao.com/dictvoice?type=${accent === 'uk' ? 1 : 2}&audio=${encodeURIComponent(word)}`; const cacheKey = `youdao_${accent}_${baseUrl}`; stopCurrentAudio(); const playWithWebAudio = (blob) => { blob.arrayBuffer().then(arrayBuf => { playAudioBuffer(arrayBuf).catch(err => { console.warn('有道发音 Web Audio 播放失败:', err); showToast('发音播放失败: ' + err.message); }); }).catch(err => { console.warn('有道发音 Blob 转换失败:', err); showToast('发音数据读取失败'); }); }; if (youdaoAudioCache.has(cacheKey)) { const blob = youdaoAudioCache.get(cacheKey); playWithWebAudio(blob); return; } GM_xmlhttpRequest({ method: 'GET', url: baseUrl, responseType: 'blob', timeout: 10000, onload: function (response) { if (response.status === 200 && response.response && response.response.size > 0) { const blob = response.response; youdaoAudioCache.set(cacheKey, blob); playWithWebAudio(blob); } else { console.warn('音频请求失败,状态码:', response.status); showToast('发音获取失败'); } }, onerror: function (err) { console.error('音频网络请求错误', err); showToast('网络错误,无法播放'); }, ontimeout: function () { showToast('发音请求超时'); } }); } function renderTrashPage(selectionRect, filterDays = 7) { cleanOldDeletedItems(); stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); isConfigPageOpen = false; applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const closeIconSvg = ``; let currentFilterDays = filterDays; let currentSortMode = 'time'; const filterOptions = [ { value: '1', label: '1 天内' }, { value: '3', label: '3 天内' }, { value: '7', label: '7 天内' } ]; const filterPopoverHtml = createCustomPopoverHtml('trash-filter-popover', filterOptions, String(currentFilterDays)); const html = `
单词回收站 ( 超过 7 天的自动永久删除 )
筛选: ${filterPopoverHtml}
排序:
`; setElementHtml(contentWrapper, html); const popupRoot = getPopupRoot(); popupRoot.__currentTrashSortMode = currentSortMode; const updateTrashList = (days, sortMode) => { const now = Date.now(); const trashWords = getTrashWords().sort((a, b) => (b.deletedAt || 0) - (a.deletedAt || 0)); const filtered = trashWords.filter(item => { if (!item.deletedAt) return false; const diff = now - item.deletedAt; return diff <= days * 24 * 60 * 60 * 1000; }); let sorted = filtered.slice(); if (sortMode === 'alpha') { sorted.sort((a, b) => a.word.localeCompare(b.word)); } else { sorted.sort((a, b) => (b.deletedAt || 0) - (a.deletedAt || 0)); } const container = popupRoot.querySelector('#trash-list-container'); if (!container) return; if (sorted.length === 0) { setElementHtml(container, `
当前回收站为空
`); popupRoot.__currentTrashList = []; return; } popupRoot.__currentTrashList = sorted.map(item => item.word); const itemsHtml = sorted.map(item => { const word = escapeHtml(item.word); return `
${word}
`; }).join(''); setElementHtml(container, itemsHtml); bindWordListSearch(container); enableAutoHideScrollbar(container); container.querySelectorAll('.trash-permanent-delete-btn').forEach(btn => { btn.addEventListener('click', async function(e) { e.stopPropagation(); const word = this.getAttribute('data-word'); if (!word) return; if (await customConfirm(`确定要永久删除单词“${word}”吗?此操作不可恢复!`)) { const words = getFavoriteWords(); const target = words.find(item => item.word === word); if (target) { const now = Date.now(); target.deleted = true; target.deletedAt = now; target.syncAt = now; target.permanentlyDeleted = true; saveFavoriteWords(words); } showToast('已永久删除'); const filterSelect = popupRoot.querySelector('#trash-filter-popover'); const days = filterSelect ? parseInt(filterSelect.getAttribute('data-value'), 10) : currentFilterDays; updateTrashList(days, currentSortMode); } }); }); }; popupRoot.__updateTrashList = updateTrashList; updateTrashList(currentFilterDays, currentSortMode); const filterPopover = popupRoot.querySelector('#trash-filter-popover'); if (filterPopover) { const observer = new MutationObserver(() => { const newVal = filterPopover.getAttribute('data-value'); if (newVal) { const days = parseInt(newVal, 10); currentFilterDays = days; updateTrashList(days, currentSortMode); } }); observer.observe(filterPopover, { attributes: true, attributeFilter: ['data-value'] }); popupRoot.__filterObserver = observer; } const sortBtns = popupRoot.querySelectorAll('.sort-option'); sortBtns.forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); const mode = this.getAttribute('data-sort'); if (mode === currentSortMode) return; currentSortMode = mode; popupRoot.__currentTrashSortMode = mode; sortBtns.forEach(b => b.classList.remove('active')); this.classList.add('active'); updateTrashList(currentFilterDays, currentSortMode); }); }); const copyAllBtn = popupRoot.querySelector('.trash-copy-all-btn'); if (copyAllBtn) { copyAllBtn.onclick = (e) => { e.stopPropagation(); const words = popupRoot.__currentTrashList || []; if (words.length === 0) { showToast('没有可复制的单词'); return; } const text = words.join('\n'); navigator.clipboard.writeText(text) .then(() => showToast(`已复制 ${words.length} 个单词`)) .catch(() => showToast('复制失败,请检查权限')); }; } const emptyBtn = popupRoot.querySelector('.trash-empty-btn'); if (emptyBtn) { emptyBtn.onclick = async (e) => { e.stopPropagation(); cleanOldDeletedItems(); const allTrash = getTrashWords(); if (allTrash.length === 0) { showToast('回收站已空'); return; } if (await customConfirm(`确定要清空回收站所有的单词(共 ${allTrash.length} 个)吗?此操作不可恢复!`)) { const words = getFavoriteWords(); const now = Date.now(); words.forEach(item => { if (item.deleted && !item.permanentlyDeleted) { item.permanentlyDeleted = true; item.syncAt = now; } }); saveFavoriteWords(words); showToast('已清空回收站'); const filterSelect = popupRoot.querySelector('#trash-filter-popover'); const days = filterSelect ? parseInt(filterSelect.getAttribute('data-value'), 10) : currentFilterDays; updateTrashList(days, currentSortMode); } }; } const backBtn = popupRoot.querySelector('.trash-back-btn'); if (backBtn) { backBtn.onclick = (e) => { e.stopPropagation(); const rect = getCurrentPopupRect(); if (rect) renderWordListPage(rect, true); }; } const closeBtn = popupRoot.querySelector('.close-btn'); if (closeBtn) { closeBtn.onclick = (e) => { e.stopPropagation(); hidePopup(); }; } showPopupAt(selectionRect); } function renderAddWordPage(selectionRect) { stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); isConfigPageOpen = false; applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const closeIconSvg = ``; const html = `
新增单词
`; setElementHtml(contentWrapper, html); showPopupAt(selectionRect); const root = getPopupRoot(); const textarea = root.querySelector('#add-word-textarea'); const cancelBtn = root.querySelector('.add-word-cancel'); const saveBtn = root.querySelector('.add-word-save'); const closeBtn = root.querySelector('.close-btn'); const goBackToList = () => { const rect = getCurrentPopupRect(); if (rect) renderWordListPage(rect, true); else hidePopup(); }; const handleSave = async () => { const raw = textarea.value; const words = raw.split(/[\s,;,、\n]+/).map(s => s.trim()).filter(s => s.length > 0); const unique = [...new Set(words)]; if (unique.length === 0) { showToast('未识别到有效单词'); return; } const confirmMsg = `共识别到 ${unique.length} 个单词(已去重),是否确定添加?`; const confirmed = await customConfirm(confirmMsg); if (!confirmed) { return; } const added = addFavoriteWords(unique); showToast(`已添加 ${added} 个单词`); goBackToList(); }; if (cancelBtn) cancelBtn.onclick = (e) => { e.stopPropagation(); goBackToList(); }; if (saveBtn) saveBtn.onclick = (e) => { e.stopPropagation(); handleSave(); }; if (closeBtn) closeBtn.onclick = (e) => { e.stopPropagation(); goBackToList(); }; textarea.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); handleSave(); } }); afterRenderBindScrollbars(); } function renderWordEditor(selectionRect, word) { stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); isConfigPageOpen = false; applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const closeIconSvg = ``; const html = `
编辑单词
`; setElementHtml(contentWrapper, html); showPopupAt(selectionRect); const root = getPopupRoot(); const input = root.querySelector('#word-editor-input'); const cancelBtn = root.querySelector('.word-editor-cancel'); const saveBtn = root.querySelector('.word-editor-save'); const closeBtn = root.querySelector('.close-btn'); const goBackToList = () => { const rect = getCurrentPopupRect(); if (rect) { renderWordListPage(rect, true); } else { hidePopup(); } }; const handleSave = () => { const newWord = input.value.trim(); if (!newWord) { showToast('单词不能为空'); return; } if (newWord === word) { showToast('未修改'); return; } let allWords = getFavoriteWords(); const existingIndex = allWords.findIndex(item => item.word === newWord && !item.permanentlyDeleted); const existing = existingIndex !== -1 ? allWords[existingIndex] : null; const targetIndex = allWords.findIndex(item => item.word === word && !item.deleted && !item.permanentlyDeleted); if (targetIndex === -1) { showToast('未找到该单词'); return; } const target = allWords[targetIndex]; if (existing) { if (existing.deleted && !existing.permanentlyDeleted) { existing.deleted = false; existing.deletedAt = 0; existing.time = Date.now(); existing.syncAt = Date.now(); existing.permanentlyDeleted = false; } else { showToast(`单词“${newWord}”已存在`); return; } } else { allWords.push({ word: newWord, time: Date.now(), deleted: false, deletedAt: 0, permanentlyDeleted: false, syncAt: Date.now() }); } target.deleted = true; target.permanentlyDeleted = true; target.deletedAt = Date.now(); target.syncAt = Date.now(); saveFavoriteWords(allWords); showToast('已更新'); goBackToList(); }; if (cancelBtn) cancelBtn.onclick = (e) => { e.stopPropagation(); goBackToList(); }; if (saveBtn) saveBtn.onclick = (e) => { e.stopPropagation(); handleSave(); }; if (closeBtn) closeBtn.onclick = (e) => { e.stopPropagation(); goBackToList(); }; input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleSave(); } }); input.focus(); input.setSelectionRange(input.value.length, input.value.length); } function renderWordListPage(selectionRect, skipSync = false) { stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); isConfigPageOpen = false; applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); wordListPageState.favorites = getFavoriteWords().filter(item => !item.deleted); wordListPageState.originalFavorites = getFavoriteWords().filter(item => !item.deleted); wordListPageState.sortMode = 'time'; const closeIconSvg = ``; const trashIconSvg = ``; const listIconSvg = ``; const addIconSvg = ``; const html = `
单词本
排序:
`; setElementHtml(contentWrapper, html); renderWordListContent(); showPopupAt(selectionRect); const syncStatusEl = contentWrapper.querySelector('.word-list-sync-status'); if (isUpstashConfigured()) { if (skipSync) { const updateStatusText = () => { if (!syncStatusEl) return; if (favoritesSyncPromise) { setSyncingStatus(syncStatusEl); return; } const lastSuccess = GM_getValue(WORD_SYNC_STATUS_KEY, null); let text; if (lastSuccess === null) { text = '保存位置:本地 + Upstash 云端 | 尚未同步'; } else if (lastSuccess === true) { text = '保存位置:本地 + Upstash 云端 | 同步成功 ✓'; } else { text = '保存位置:本地 + Upstash 云端 | 上次同步失败'; } syncStatusEl.textContent = text; }; if (favoritesSyncPromise) { if (syncStatusEl) setSyncingStatus(syncStatusEl); favoritesSyncPromise.finally(() => { updateStatusText(); }); } else { updateStatusText(); } } else { setSyncingStatus(syncStatusEl); syncFavoriteWithCloudAfterLocalChange() .then(() => { if (syncStatusEl) syncStatusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步成功 ✓'; wordListPageState.favorites = getFavoriteWords().filter(item => !item.deleted); wordListPageState.originalFavorites = getFavoriteWords().filter(item => !item.deleted); renderWordListContent(); }) .catch(err => { if (syncStatusEl) syncStatusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步失败,使用本地数据'; console.warn('同步收藏失败:', err); }); } } else { syncStatusEl.textContent = '保存位置:仅本地浏览器'; } const popupRoot = getPopupRoot(); const addBtn = popupRoot.querySelector('.add-word-btn'); if (addBtn) { addBtn.onclick = (e) => { e.stopPropagation(); const rect = getCurrentPopupRect(); if (rect) renderAddWordPage(rect); }; } const trashBtn = popupRoot.querySelector('.trash-btn'); if (trashBtn) { trashBtn.onclick = (e) => { e.stopPropagation(); const rect = getCurrentPopupRect(); if (rect) renderTrashPage(rect); }; } } function bindWordListSearch(container) { if (!container || container.dataset.searchListener) return; container.dataset.searchListener = 'true'; container.addEventListener('click', function(e) { const searchBtn = e.target.closest('.word-list-search'); if (searchBtn) { e.stopPropagation(); const word = searchBtn.dataset.word; const resultDiv = container.querySelector(`.word-result[data-word="${word}"]`); if (!resultDiv) return; const isHidden = resultDiv.style.display === 'none'; if (isHidden) { resultDiv.style.display = 'block'; searchBtn.classList.add('active'); if (wordDictCache.has(word)) { fillWordResult(resultDiv, word, wordDictCache.get(word)); } else { setElementHtml(resultDiv, '
正在查询…
'); queryWord(word) .then(data => { wordDictCache.set(word, data); fillWordResult(resultDiv, word, data); }) .catch(err => { setElementHtml(resultDiv, `
查询失败:${escapeHtml(err.message)}
`); }); } } else { resultDiv.style.display = 'none'; searchBtn.classList.remove('active'); } return; } const pronounceBtn = e.target.closest('.pronounce-button'); if (pronounceBtn) { e.stopPropagation(); const item = pronounceBtn.closest('.phonetic-item'); const resultDiv = pronounceBtn.closest('.word-result'); if (item && resultDiv) { const word = resultDiv.dataset.word; const audioUrl = item.dataset.audio; const type = item.dataset.type; if (audioUrl) { playPronunciation(word, type, audioUrl); } } } }); } function fillWordResult(container, word, data) { const phonetics = data.phonetics || []; const meanings = data.meanings || []; const audio = data.audio || {}; const showWordForms = GM_getValue('youdao_show_wordforms', true); const showExamples = GM_getValue('youdao_show_examples', true); const audioIconSvg = ``; let phoneticsHtml = ''; if (phonetics.length) { phoneticsHtml = phonetics.map(item => { const phonetic = item.phonetic || ''; const type = item.type || 'uk'; const audioUrl = audio[type] || ''; return `
${escapeHtml(item.label)} /${escapeHtml(phonetic)}/ ${audioIconSvg}
`; }).join(''); } let meaningsHtml = meanings.map(line => `
${escapeHtml(line)}
`).join(''); if (!meaningsHtml) meaningsHtml = '
未找到释义
'; let examplesHtml = ''; if (showExamples && data.examples && data.examples.length) { const exampleItems = data.examples.slice(0, 3).map(exp => { const cleanText = exp.en.replace(/<[^>]*>/g, ''); return `
${exp.en}
${escapeHtml(exp.zh)}
`; }).join(''); examplesHtml = `
双语例句
${exampleItems}
`; } let wordFormsHtml = ''; if (showWordForms && data.wordForms && data.wordForms.length) { const items = data.wordForms.map(wf => `${escapeHtml(wf.name)}:${escapeHtml(wf.value)}` ).join(''); wordFormsHtml = `
${items}
`; } const youdaoUrl = `https://www.youdao.com/w/eng/${encodeURIComponent(word)}`; const htmlContent = `
${phoneticsHtml ? `
${phoneticsHtml}
` : ''}
${meaningsHtml}${wordFormsHtml}${examplesHtml}
查看完整释义 →
`; setElementHtml(container, htmlContent); } function renderWordListContent() { const root = getPopupRoot(); if (!root) return; const container = root.querySelector('#word-list-container'); if (!container) return; const favorites = wordListPageState.favorites.slice(); if (favorites.length === 0) { setElementHtml(container, `
暂无单词,点击右上角「+」新增
`); return; } if (wordListPageState.sortMode === 'alpha') { favorites.sort((a, b) => a.word.localeCompare(b.word)); } else { favorites.sort((a, b) => (b.time || 0) - (a.time || 0)); } const itemsHtml = favorites.map(item => { const word = escapeHtml(item.word); return `
${word}
`; }).join(''); setElementHtml(container, itemsHtml); bindWordListSearch(container); enableAutoHideScrollbar(container); container.querySelectorAll('.word-list-pronounce').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const word = btn.getAttribute('data-word'); const isChinese = /[\u4e00-\u9fa5]/.test(word); const voice = isChinese ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; playTTS(word, voice); }; }); container.querySelectorAll('.word-list-copy').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const word = btn.getAttribute('data-word'); if (word) { navigator.clipboard.writeText(word) .then(() => showToast(`已复制到剪贴板`)) .catch(() => showToast('复制失败,请检查权限')); const originalColor = btn.style.color; btn.style.color = '#1a73e8'; setTimeout(() => { btn.style.color = originalColor; }, 800); } }; }); container.querySelectorAll('.word-list-toggle-fav').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const word = btn.getAttribute('data-word'); const statusEl = root.querySelector('.word-list-sync-status'); if (statusEl && isUpstashConfigured()) { setSyncingStatus(statusEl); } removeFavoriteWord(word); wordListPageState.favorites = getFavoriteWords().filter(item => !item.deleted); renderWordListContent(); showToast(`已移入回收站`); if (isUpstashConfigured()) { syncFavoriteWithCloudAfterLocalChange() .then(() => { if (statusEl) statusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步成功 ✓'; }) .catch(() => { if (statusEl) statusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步失败,使用本地数据'; }); } }; }); container.querySelectorAll('.word-list-edit').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const word = btn.getAttribute('data-word'); const rect = getCurrentPopupRect(); if (rect && word) { renderWordEditor(rect, word); } }; }); } function renderNotesPage(selectionRect, skipSync = false) { stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); isConfigPageOpen = false; applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const closeIconSvg = ``; const addIconSvg = ``; const html = `
笔记本
${isUpstashConfigured() ? '保存位置:本地 + Upstash 云端' : '保存位置:仅本地浏览器'}
排序:
`; setElementHtml(contentWrapper, html); renderNotesList(); showPopupAt(selectionRect); const root = getPopupRoot(); const addBtn = root.querySelector('.notes-add-btn'); if (addBtn) { addBtn.onclick = (e) => { e.stopPropagation(); const rect = getCurrentPopupRect(); if (rect) renderNoteEditor(rect, null); }; } const deletedBtn = root.querySelector('.notes-deleted-btn'); if (deletedBtn) { deletedBtn.onclick = (e) => { e.stopPropagation(); const rect = getCurrentPopupRect(); if (rect) renderDeletedNotesPage(rect); }; } const backBtn = root.querySelector('.notes-back-btn'); if (backBtn) { backBtn.onclick = (e) => { e.stopPropagation(); if (lastSelectionText) { fetchAndRender(lastSelectionText); } else { hidePopup(); } }; } const sortBtns = root.querySelectorAll('.sort-option'); sortBtns.forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); const mode = this.getAttribute('data-sort'); if (mode === notesPageState.sortMode) return; notesPageState.sortMode = mode; sortBtns.forEach(b => b.classList.remove('active')); this.classList.add('active'); renderNotesList(); }); }); const copyAllBtn = root.querySelector('.notes-copy-all-btn'); if (copyAllBtn) { copyAllBtn.onclick = (e) => { e.stopPropagation(); const notes = getNotes(); if (notes.length === 0) { showToast('没有笔记'); return; } const text = notes.map(n => `标题:${n.title}\n内容:${n.content}`).join('\n\n---\n\n'); navigator.clipboard.writeText(text) .then(() => showToast(`已复制 ${notes.length} 篇笔记`)) .catch(() => showToast('复制失败')); }; } const closeBtn = root.querySelector('.close-btn'); if (closeBtn) { closeBtn.onclick = (e) => { e.stopPropagation(); hidePopup(); }; } const syncStatusEl = root.querySelector('#notes-sync-status'); if (isUpstashConfigured()) { if (skipSync) { const updateStatusText = () => { if (!syncStatusEl) return; if (notesSyncPromise) { setSyncingStatus(syncStatusEl); return; } const lastSuccess = GM_getValue(NOTE_SYNC_STATUS_KEY, null); let text; if (lastSuccess === null) { text = '保存位置:本地 + Upstash 云端 | 尚未同步'; } else if (lastSuccess === true) { text = '保存位置:本地 + Upstash 云端 | 同步成功 ✓'; } else { text = '保存位置:本地 + Upstash 云端 | 上次同步失败'; } syncStatusEl.textContent = text; }; if (notesSyncPromise) { if (syncStatusEl) setSyncingStatus(syncStatusEl); notesSyncPromise.finally(() => { updateStatusText(); }); } else { updateStatusText(); } } else { setSyncingStatus(syncStatusEl); syncNotesWithCloudAfterLocalChange() .then(() => { if (syncStatusEl) syncStatusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步成功 ✓'; renderNotesList(); }) .catch(err => { if (syncStatusEl) syncStatusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步失败,使用本地数据'; console.warn('[笔记] 同步失败:', err); }); } } else { syncStatusEl.textContent = '保存位置:仅本地浏览器'; } } function renderNotesList() { const root = getPopupRoot(); if (!root) return; const container = root.querySelector('#notes-list-container'); if (!container) return; let notes = getNotes(); if (notes.length === 0) { setElementHtml(container, `
暂无笔记,点击右上角「+」新增
`); return; } if (notesPageState.sortMode === 'alpha') { notes.sort((a, b) => a.title.localeCompare(b.title)); } else { notes.sort((a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0)); } const itemsHtml = notes.map(note => { const title = escapeHtml(note.title); const id = note.id; return `
${title}
`; }).join(''); setElementHtml(container, itemsHtml); enableAutoHideScrollbar(container); container.querySelectorAll('.notes-view-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = btn.getAttribute('data-id'); if (id) { const rect = getCurrentPopupRect(); if (rect) renderNoteView(rect, parseInt(id, 10), false); } }; }); container.querySelectorAll('.notes-edit-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = btn.getAttribute('data-id'); if (id) { const rect = getCurrentPopupRect(); if (rect) renderNoteEditor(rect, parseInt(id, 10)); } }; }); container.querySelectorAll('.notes-copy-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = btn.getAttribute('data-id'); if (id) { const notes = getAllNotes(); const note = notes.find(n => n.id === parseInt(id, 10)); if (note && !note.deleted) { const text = `标题:${note.title}\n内容:${note.content}`; navigator.clipboard.writeText(text) .then(() => showToast('已复制到剪贴板')) .catch(() => showToast('复制失败')); const originalColor = btn.style.color; btn.style.color = '#1a73e8'; setTimeout(() => { btn.style.color = originalColor; }, 800); } } }; }); container.querySelectorAll('.notes-delete-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = btn.getAttribute('data-id'); if (id) { const statusEl = root.querySelector('#notes-sync-status'); if (statusEl && isUpstashConfigured()) { setSyncingStatus(statusEl); } let allNotes = getAllNotes(); const note = allNotes.find(n => n.id === parseInt(id, 10)); if (note) { const now = Date.now(); note.deleted = true; note.deletedAt = now; note.updatedAt = now; note.syncAt = now; saveNotes(allNotes); renderNotesList(); showToast('已移入回收站'); if (isUpstashConfigured()) { syncNotesWithCloudAfterLocalChange() .then(() => { if (statusEl) statusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步成功 ✓'; }) .catch(() => { if (statusEl) statusEl.textContent = '保存位置:本地 + Upstash 云端 | 同步失败,使用本地数据'; }); } } } }; }); } function renderNoteEditor(selectionRect, noteId, fromView = false, fromDeleted = false) { stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const isEdit = noteId !== null; let note = null; if (isEdit) { const notes = getAllNotes(); note = notes.find(n => n.id === noteId); if (!note) { showToast('笔记不存在'); return; } } const title = isEdit ? note.title : ''; const content = isEdit ? note.content : ''; const closeIconSvg = ``; const html = `
${isEdit ? '编辑笔记' : '新增笔记'} ( MD 语法 )
`; setElementHtml(contentWrapper, html); showPopupAt(selectionRect); const root = getPopupRoot(); const titleInput = root.querySelector('#note-editor-title-input'); const contentInput = root.querySelector('#note-editor-content-input'); const cancelBtn = root.querySelector('.note-editor-cancel'); const saveBtn = root.querySelector('.note-editor-save'); const closeBtn = root.querySelector('.close-btn'); const goBackToList = () => { const rect = getCurrentPopupRect(); if (rect) { if (fromView) { renderNoteView(rect, noteId, fromDeleted); } else { renderNotesPage(rect, true); } } else { hidePopup(); } }; const save = () => { const newTitle = titleInput.value.trim(); const newContent = contentInput.value.trim(); if (!newTitle) { showToast('标题不能为空'); return; } if (!newContent) { showToast('内容不能为空'); return; } if (isEdit && note) { if (newTitle === note.title && newContent === note.content) { showToast('未修改'); return; } } let allNotes = getAllNotes(); const now = Date.now(); if (isEdit) { const idx = allNotes.findIndex(n => n.id === noteId); if (idx !== -1) { allNotes[idx].title = newTitle; allNotes[idx].content = newContent; allNotes[idx].updatedAt = now; allNotes[idx].syncAt = now; if (allNotes[idx].deleted) { allNotes[idx].deleted = false; allNotes[idx].deletedAt = 0; } } } else { allNotes.push({ id: now, title: newTitle, content: newContent, createdAt: now, updatedAt: now, syncAt: now, deleted: false, deletedAt: 0 }); } saveNotes(allNotes); showToast(isEdit ? '笔记已更新' : '笔记已创建'); goBackToList(); }; const cancel = () => { goBackToList(); }; if (cancelBtn) cancelBtn.onclick = (e) => { e.stopPropagation(); cancel(); }; if (saveBtn) saveBtn.onclick = (e) => { e.stopPropagation(); save(); }; if (closeBtn) closeBtn.onclick = (e) => { e.stopPropagation(); hidePopup(); }; contentInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); save(); } }); titleInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); save(); } }); afterRenderBindScrollbars(); } function renderNoteView(selectionRect, noteId, fromDeleted = false) { stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const allNotes = getAllNotes(); const note = allNotes.find(n => n.id === noteId); if (!note) { showToast('笔记不存在'); if (lastSelectionText) fetchAndRender(lastSelectionText); return; } const closeIconSvg = ``; const defaultDisplayMode = GM_getValue('note_display_mode', 'markdown'); let currentViewMode = defaultDisplayMode; const modeIcon = currentViewMode === 'markdown' ? `` : ``; const modeTitle = currentViewMode === 'markdown' ? '切换纯文本' : '切换 Markdown'; const noteLangPopoverHtml = createCustomPopoverHtml( 'note-tts-lang-popover', [ { value: 'all', label: '中英混合' }, { value: 'en', label: '仅英文' }, { value: 'zh', label: '仅中文' } ], 'all' ); const noteSpeedPopoverHtml = createCustomPopoverHtml( 'note-tts-speed-popover', Object.keys(TTS_SPEED_MAP).map(key => ({ value: key, label: TTS_SPEED_MAP[key].label })), getTTSSpeed() ); const html = `
笔记详情 ${modeIcon}
${escapeHtml(note.title)}
${noteLangPopoverHtml}
${noteSpeedPopoverHtml}
`; setElementHtml(contentWrapper, html); showPopupAt(selectionRect); const root = getPopupRoot(); const contentEl = root.querySelector('#note-view-content'); const switchBtn = root.querySelector('#switch-view-mode'); const playAllBtn = root.querySelector('.note-tts-play-all-btn'); const playAllText = root.querySelector('.note-tts-play-text'); const playAllIcon = root.querySelector('.note-tts-play-icon'); const stopAllIcon = root.querySelector('.note-tts-stop-icon'); const langPopover = root.querySelector('#note-tts-lang-popover'); const speedPopover = root.querySelector('#note-tts-speed-popover'); let noteTtsGeneration = 0; let noteTtsQueue = []; let noteTtsIndex = 0; let noteTtsFailCount = 0; let noteTtsPlaying = false; function setPlayAllBtnState(playing) { if (!playAllBtn) return; noteTtsPlaying = playing; playAllBtn.classList.toggle('playing', playing); if (playAllText) playAllText.textContent = playing ? '停止朗读' : '朗读全文'; if (playAllIcon) playAllIcon.style.display = playing ? 'none' : ''; if (stopAllIcon) stopAllIcon.style.display = playing ? '' : 'none'; } function stopNoteTts() { noteTtsGeneration++; noteTtsQueue = []; noteTtsIndex = 0; noteTtsFailCount = 0; stopCurrentAudio(); if (contentEl) { contentEl.querySelectorAll('.paragraph-tts-btn').forEach(b => b.classList.remove('active')); } setPlayAllBtnState(false); } globalNoteTtsStop = stopNoteTts; function collectNoteSegments(mode) { if (!contentEl) return []; const blockSelector = 'p, li, h1, h2, h3, h4, h5, h6, blockquote'; const blocks = contentEl.querySelectorAll(blockSelector); const segs = []; blocks.forEach(el => { if (el.closest('pre') || el.closest('code')) return; if (el.querySelector('p, h1, h2, h3, h4, h5, h6, li, blockquote, ul, ol')) return; if (!el.querySelector('.paragraph-tts-btn')) return; const text = el.textContent.trim(); if (!text) return; const hasZh = /[\u4e00-\u9fa5]/.test(text); const hasEn = /[a-zA-Z]/.test(text); if (mode === 'en') { if (hasZh) return; if (!hasEn) return; } else if (mode === 'zh') { if (!hasZh) return; } segs.push({ text, el }); }); return segs; } function playNextInQueue() { const gen = noteTtsGeneration; if (noteTtsIndex >= noteTtsQueue.length) { stopNoteTts(); return; } const { text, el } = noteTtsQueue[noteTtsIndex]; if (contentEl) { contentEl.querySelectorAll('.paragraph-tts-btn').forEach(b => b.classList.remove('active')); } const paraBtn = el.querySelector('.paragraph-tts-btn'); if (paraBtn) paraBtn.classList.add('active'); if (contentEl) { try { const containerRect = contentEl.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); const delta = elRect.top - containerRect.top; const targetScroll = contentEl.scrollTop + delta - (contentEl.clientHeight / 2) + (elRect.height / 2); contentEl.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' }); } catch (e) {} } const isChinese = /[\u4e00-\u9fa5]/.test(text); const voice = isChinese ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; const speedKey = (speedPopover && speedPopover.getAttribute('data-value')) || getTTSSpeed(); playTTS(text, voice, el, speedKey, (success) => { if (gen !== noteTtsGeneration) return; if (!success) { noteTtsFailCount++; if (noteTtsFailCount >= 3) { showToast('连续朗读失败,已停止'); stopNoteTts(); return; } } else { noteTtsFailCount = 0; } noteTtsIndex++; playNextInQueue(); }); } if (playAllBtn) { playAllBtn.addEventListener('click', (e) => { e.stopPropagation(); if (noteTtsPlaying) { stopNoteTts(); return; } const mode = (langPopover && langPopover.getAttribute('data-value')) || 'all'; noteTtsQueue = collectNoteSegments(mode); if (noteTtsQueue.length === 0) { showToast('当前范围内没有可朗读的内容'); return; } noteTtsGeneration++; noteTtsIndex = 0; noteTtsFailCount = 0; setPlayAllBtnState(true); playNextInQueue(); }); } if (langPopover) { const langObserver = new MutationObserver(() => { if (!noteTtsPlaying) return; stopNoteTts(); const mode = langPopover.getAttribute('data-value') || 'all'; noteTtsQueue = collectNoteSegments(mode); if (noteTtsQueue.length === 0) { showToast('当前范围内没有可朗读的内容'); return; } noteTtsGeneration++; noteTtsIndex = 0; noteTtsFailCount = 0; setPlayAllBtnState(true); playNextInQueue(); }); langObserver.observe(langPopover, { attributes: true, attributeFilter: ['data-value'] }); root.__noteLangObserver = langObserver; } if (speedPopover) { const speedObserver = new MutationObserver(() => { if (!noteTtsPlaying) return; noteTtsGeneration++; stopCurrentAudio(); noteTtsFailCount = 0; playNextInQueue(); }); speedObserver.observe(speedPopover, { attributes: true, attributeFilter: ['data-value'] }); root.__noteSpeedObserver = speedObserver; } function splitParagraphsByBr(container) { if (!container) return; const targets = Array.from(container.querySelectorAll('p, li')); targets.forEach(p => { if (p.closest('pre') || p.closest('code')) return; if (!p.querySelector('br')) return; const nodes = Array.from(p.childNodes); const groups = [[]]; nodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') { groups.push([]); } else { groups[groups.length - 1].push(node); } }); if (groups.length <= 1) return; const fragment = document.createDocumentFragment(); const tagName = p.tagName; // 'P' 或 'LI' groups.forEach(groupNodes => { const text = groupNodes.map(n => n.textContent || '').join('').trim(); if (!text) return; const newEl = document.createElement(tagName); Array.from(p.attributes).forEach(attr => { if (attr.name !== 'id') newEl.setAttribute(attr.name, attr.value); }); groupNodes.forEach(n => newEl.appendChild(n)); fragment.appendChild(newEl); }); p.parentNode.replaceChild(fragment, p); }); } function renderContent() { if (!contentEl) return; if (currentViewMode === 'plain') { contentEl.style.whiteSpace = 'pre-wrap'; contentEl.textContent = note.content; } else { contentEl.style.whiteSpace = ''; const rendered = renderDeepSeekMarkdown(note.content) || note.content; setElementHtml(contentEl, rendered); } splitParagraphsByBr(contentEl); applyWordTriggers(contentEl); injectParagraphTTS(contentEl); enableAutoHideScrollbar(contentEl); const ttsControls = root.querySelector('.note-tts-controls'); if (ttsControls) { ttsControls.style.display = (currentViewMode === 'plain') ? 'none' : ''; } if (switchBtn) { const isMarkdown = currentViewMode === 'markdown'; switchBtn.title = isMarkdown ? '切换纯文本' : '切换 Markdown'; switchBtn.innerHTML = isMarkdown ? getTrustedHtml(``) : getTrustedHtml(``); } } function injectParagraphTTS(container) { if (!container) return; const blockSelector = 'p, li, h1, h2, h3, h4, h5, h6, blockquote'; const elements = container.querySelectorAll(blockSelector); elements.forEach(el => { if (el.closest('pre') || el.closest('code') || !el.textContent.trim()) return; if (el.querySelector('p, h1, h2, h3, h4, h5, h6, li, blockquote, ul, ol')) return; if (el.querySelector('.paragraph-tts-btn')) return; const btn = document.createElement('button'); btn.className = 'paragraph-tts-btn'; btn.innerHTML = getTrustedHtml(``); btn.title = '朗读本段'; btn.addEventListener('click', (e) => { e.stopPropagation(); stopNoteTts(); if (contentEl) { contentEl.querySelectorAll('.paragraph-tts-btn').forEach(b => b.classList.remove('active')); } btn.classList.add('active'); const text = el.textContent.trim(); const isChinese = /[\u4e00-\u9fa5]/.test(text); const voice = isChinese ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; const speedKey = (speedPopover && speedPopover.getAttribute('data-value')) || getTTSSpeed(); playTTS(text, voice, el, speedKey, (success) => { if (success) btn.classList.remove('active'); }); }); el.appendChild(btn); }); } renderContent(); if (switchBtn) { switchBtn.addEventListener('click', function(e) { e.stopPropagation(); stopNoteTts(); currentViewMode = (currentViewMode === 'markdown') ? 'plain' : 'markdown'; renderContent(); }); } let currentCard = null; let currentWord = ''; let cardVisible = true; let requestIdCounter = 0; let currentHighlightedTrigger = null; function clearCards(removeFromDOM = true) { if (removeFromDOM && currentCard && currentCard.parentNode) { currentCard.remove(); } currentCard = null; currentWord = ''; cardVisible = true; const titleEl = root.querySelector('.note-view-title'); if (titleEl) { titleEl.querySelectorAll('.inline-word-card').forEach(c => c.remove()); titleEl.dataset.hasCard = 'false'; } if (currentHighlightedTrigger) { currentHighlightedTrigger.style.backgroundColor = ''; currentHighlightedTrigger = null; } } function buildCardFromData(word, data) { const cardContainer = document.createElement('div'); cardContainer.className = 'inline-word-card'; const titleDiv = document.createElement('div'); titleDiv.className = 'word-title'; titleDiv.style.marginBottom = '0px'; titleDiv.style.display = 'flex'; titleDiv.style.alignItems = 'center'; const wordSpan = document.createElement('span'); wordSpan.textContent = word; titleDiv.appendChild(wordSpan); const isFav = isWordFavorite(word); const favBtn = document.createElement('button'); favBtn.className = 'favorite-btn'; favBtn.setAttribute('data-word', word); favBtn.setAttribute('data-fav', isFav ? '1' : '0'); favBtn.setAttribute('data-custom-tooltip', isFav ? '移入回收站' : '添加到单词本'); favBtn.innerHTML = isFav ? getTrustedHtml(``) : getTrustedHtml(``); favBtn.addEventListener('click', function(e) { e.stopPropagation(); const w = this.getAttribute('data-word'); const newFav = toggleFavoriteWord(w); this.setAttribute('data-fav', newFav ? '1' : '0'); this.innerHTML = newFav ? getTrustedHtml(``) : getTrustedHtml(``); this.setAttribute('data-custom-tooltip', newFav ? '移入回收站' : '添加到单词本'); showToast(newFav ? `已添加到单词本` : `已移入回收站`); }); titleDiv.appendChild(favBtn); cardContainer.appendChild(titleDiv); const contentContainer = document.createElement('div'); contentContainer.className = 'word-result'; cardContainer.appendChild(contentContainer); fillWordResult(contentContainer, word, data); bindWordListSearch(contentContainer); enableAutoHideScrollbar(cardContainer); return cardContainer; } function createWordCard(word) { if (currentCard && currentWord === word) { cardVisible = !cardVisible; currentCard.style.display = cardVisible ? '' : 'none'; if (!cardVisible && currentHighlightedTrigger) { currentHighlightedTrigger.style.backgroundColor = ''; currentHighlightedTrigger = null; } return; } clearCards(true); if (wordDictCache.has(word)) { const data = wordDictCache.get(word); const card = buildCardFromData(word, data); const titleEl = root.querySelector('.note-view-title'); titleEl.appendChild(card); currentCard = card; currentWord = word; cardVisible = true; titleEl.dataset.hasCard = 'true'; return; } const tempCard = document.createElement('div'); tempCard.className = 'inline-word-card'; tempCard.style.padding = '8px'; tempCard.style.color = '#61666b'; tempCard.style.fontSize = '14px'; tempCard.style.fontWeight = 'normal'; tempCard.textContent = '正在查询…'; const titleEl = root.querySelector('.note-view-title'); titleEl.appendChild(tempCard); currentCard = tempCard; currentWord = word; cardVisible = true; titleEl.dataset.hasCard = 'true'; const requestId = ++requestIdCounter; tempCard.dataset.requestId = requestId; queryWord(word) .then(data => { if (tempCard.dataset.requestId != requestId) return; if (!tempCard.parentNode) return; wordDictCache.set(word, data); tempCard.remove(); const card = buildCardFromData(word, data); titleEl.appendChild(card); currentCard = card; currentWord = word; cardVisible = true; titleEl.dataset.hasCard = 'true'; }) .catch(err => { if (tempCard.dataset.requestId != requestId) return; if (!tempCard.parentNode) return; tempCard.textContent = '查询失败: ' + err.message; tempCard.style.color = '#d93025'; }); } if (contentEl) { contentEl.addEventListener('click', function(e) { const trigger = e.target.closest('.word-trigger'); if (trigger) { e.stopPropagation(); const word = trigger.dataset.word; if (!word) return; if (currentCard && currentWord === word) { cardVisible = !cardVisible; currentCard.style.display = cardVisible ? '' : 'none'; if (cardVisible) { trigger.classList.add('active'); currentHighlightedTrigger = trigger; } else { trigger.classList.remove('active'); currentHighlightedTrigger = null; } return; } if (currentHighlightedTrigger) { currentHighlightedTrigger.classList.remove('active'); currentHighlightedTrigger = null; } clearCards(true); createWordCard(word); trigger.classList.add('active'); currentHighlightedTrigger = trigger; } }); } function applyWordTriggers(container) { if (!container) return; function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; const parts = text.split(/\b([a-zA-Z]+(?:['’\-][a-zA-Z]+)*)\b/); if (parts.length > 1) { const fragment = document.createDocumentFragment(); for (let i = 0; i < parts.length; i++) { if (i % 2 === 1) { const word = parts[i]; const span = document.createElement('span'); span.className = 'word-trigger'; span.title = '查看释义'; span.dataset.word = word; span.textContent = word; fragment.appendChild(span); } else if (parts[i]) { fragment.appendChild(document.createTextNode(parts[i])); } } node.parentNode.replaceChild(fragment, node); } } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE' && node.tagName !== 'PRE' && node.tagName !== 'CODE') { const children = Array.from(node.childNodes); for (const child of children) { processNode(child); } } } processNode(container); } const editBtn = root.querySelector('.note-view-edit'); const backBtn = root.querySelector('.note-view-back'); const closeBtn = root.querySelector('.close-btn'); const deleteBtn = root.querySelector('.note-view-delete'); const restoreBtn = root.querySelector('.note-view-restore'); const goBack = () => { const rect = getCurrentPopupRect(); if (rect) { if (fromDeleted) { renderDeletedNotesPage(rect); } else { renderNotesPage(rect, true); } } else { hidePopup(); } }; if (editBtn) { editBtn.onclick = (e) => { e.stopPropagation(); stopNoteTts(); const rect = getCurrentPopupRect(); if (rect) renderNoteEditor(rect, noteId, true, fromDeleted); }; } if (backBtn) { backBtn.onclick = (e) => { e.stopPropagation(); stopNoteTts(); goBack(); }; } if (closeBtn) { closeBtn.onclick = (e) => { e.stopPropagation(); stopNoteTts(); hidePopup(); }; } if (deleteBtn) { deleteBtn.onclick = (e) => { e.stopPropagation(); stopNoteTts(); let allNotes = getAllNotes(); const note = allNotes.find(n => n.id === noteId); if (note) { const now = Date.now(); note.deleted = true; note.deletedAt = now; note.updatedAt = now; note.syncAt = now; saveNotes(allNotes); showToast('已移入回收站'); const rect = getCurrentPopupRect(); if (rect) { if (fromDeleted) { renderDeletedNotesPage(rect); } else { renderNotesPage(rect); } } else { hidePopup(); } } }; } if (restoreBtn) { restoreBtn.onclick = (e) => { e.stopPropagation(); stopNoteTts(); let allNotes = getAllNotes(); const target = allNotes.find(n => n.id === noteId); if (target && target.deleted) { const now = Date.now(); target.deleted = false; target.deletedAt = 0; target.updatedAt = now; target.syncAt = now; saveNotes(allNotes); showToast('已移出回收站'); const rect = getCurrentPopupRect(); if (rect) { renderDeletedNotesPage(rect); } else { hidePopup(); } } else { showToast('笔记不存在或未被删除'); } }; } afterRenderBindScrollbars(); } function renderDeletedNotesPage(selectionRect, filterDays = 7) { cleanOldDeletedItems(); stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); isConfigPageOpen = false; applyThemeToPopup(); const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const closeIconSvg = ``; let currentFilterDays = filterDays; let currentSortMode = 'time'; const filterOptions = [ { value: '1', label: '1 天内' }, { value: '3', label: '3 天内' }, { value: '7', label: '7 天内' } ]; const filterPopoverHtml = createCustomPopoverHtml('deleted-notes-filter-popover', filterOptions, String(currentFilterDays)); const html = `
笔记回收站 ( 超过 7 天的自动永久删除 )
筛选: ${filterPopoverHtml}
排序:
`; setElementHtml(contentWrapper, html); const popupRoot = getPopupRoot(); const updateDeletedList = (days, sortMode) => { const now = Date.now(); let deleted = getDeletedNotes(); deleted.sort((a, b) => (b.deletedAt || 0) - (a.deletedAt || 0)); const filtered = deleted.filter(item => { if (!item.deletedAt) return false; const diff = now - item.deletedAt; return diff <= days * 24 * 60 * 60 * 1000; }); let sorted = filtered.slice(); if (sortMode === 'alpha') { sorted.sort((a, b) => a.title.localeCompare(b.title)); } else { sorted.sort((a, b) => (b.deletedAt || 0) - (a.deletedAt || 0)); } const container = popupRoot.querySelector('#deleted-notes-container'); if (!container) return; if (sorted.length === 0) { setElementHtml(container, `
当前回收站为空
`); popupRoot.__currentDeletedNotes = []; return; } popupRoot.__currentDeletedNotes = sorted; const itemsHtml = sorted.map(note => { const title = escapeHtml(note.title); const id = note.id; return `
${title}
`; }).join(''); setElementHtml(container, itemsHtml); enableAutoHideScrollbar(container); container.querySelectorAll('.deleted-note-view-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = btn.getAttribute('data-id'); if (id) { const rect = getCurrentPopupRect(); if (rect) renderNoteView(rect, parseInt(id, 10), true); } }; }); container.querySelectorAll('.deleted-note-copy-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = btn.getAttribute('data-id'); if (id) { const allNotes = getAllNotes(); const note = allNotes.find(n => n.id === parseInt(id, 10)); if (note) { const text = `标题:${note.title}\n内容:${note.content}`; navigator.clipboard.writeText(text) .then(() => showToast('已复制到剪贴板')) .catch(() => showToast('复制失败')); const originalColor = btn.style.color; btn.style.color = '#1a73e8'; setTimeout(() => { btn.style.color = originalColor; }, 800); } } }; }); container.querySelectorAll('.deleted-note-restore-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const id = btn.getAttribute('data-id'); if (id) { const allNotes = getAllNotes(); const note = allNotes.find(n => n.id === parseInt(id, 10)); if (note && note.deleted) { const now = Date.now(); note.deleted = false; note.deletedAt = 0; note.updatedAt = now; note.syncAt = now; saveNotes(allNotes); if (isUpstashConfigured()) { syncNotesWithCloudAfterLocalChange().catch(() => {}); } showToast(`已移出回收站`); const filterSelect = popupRoot.querySelector('#deleted-notes-filter-popover'); const days = filterSelect ? parseInt(filterSelect.getAttribute('data-value'), 10) : currentFilterDays; updateDeletedList(days, currentSortMode); } } }; }); container.querySelectorAll('.deleted-note-permanent-delete-btn').forEach(btn => { btn.addEventListener('click', async function(e) { e.stopPropagation(); const id = this.getAttribute('data-id'); if (!id) return; const noteId = parseInt(id, 10); const allNotes = getAllNotes(); const note = allNotes.find(n => n.id === noteId); if (!note) return; if (await customConfirm(`确定要永久删除笔记“${note.title}”吗?此操作不可恢复!`)) { const updated = getAllNotes(); const target = updated.find(n => n.id === noteId); if (target) { const now = Date.now(); target.deleted = true; target.deletedAt = now; target.updatedAt = now; target.syncAt = now; target.permanentlyDeleted = true; saveNotes(updated); } showToast('已永久删除'); const filterSelect = popupRoot.querySelector('#deleted-notes-filter-popover'); const days = filterSelect ? parseInt(filterSelect.getAttribute('data-value'), 10) : currentFilterDays; updateDeletedList(days, currentSortMode); } }); }); }; popupRoot.__updateDeletedList = updateDeletedList; updateDeletedList(currentFilterDays, currentSortMode); const filterPopover = popupRoot.querySelector('#deleted-notes-filter-popover'); if (filterPopover) { const observer = new MutationObserver(() => { const newVal = filterPopover.getAttribute('data-value'); if (newVal) { const days = parseInt(newVal, 10); currentFilterDays = days; updateDeletedList(days, currentSortMode); } }); observer.observe(filterPopover, { attributes: true, attributeFilter: ['data-value'] }); popupRoot.__deletedFilterObserver = observer; } const sortBtns = popupRoot.querySelectorAll('.sort-option'); sortBtns.forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); const mode = this.getAttribute('data-sort'); if (mode === currentSortMode) return; currentSortMode = mode; sortBtns.forEach(b => b.classList.remove('active')); this.classList.add('active'); const filterSelect = popupRoot.querySelector('#deleted-notes-filter-popover'); const days = filterSelect ? parseInt(filterSelect.getAttribute('data-value'), 10) : currentFilterDays; updateDeletedList(days, currentSortMode); }); }); const copyAllBtn = popupRoot.querySelector('.deleted-notes-copy-all'); if (copyAllBtn) { copyAllBtn.onclick = (e) => { e.stopPropagation(); const notes = popupRoot.__currentDeletedNotes || []; if (notes.length === 0) { showToast('没有可复制的笔记'); return; } const text = notes.map(n => `标题:${n.title}\n内容:${n.content}`).join('\n\n---\n\n'); navigator.clipboard.writeText(text) .then(() => showToast(`已复制 ${notes.length} 篇笔记`)) .catch(() => showToast('复制失败')); }; } const emptyBtn = popupRoot.querySelector('.deleted-notes-empty-btn'); if (emptyBtn) { emptyBtn.onclick = async (e) => { e.stopPropagation(); cleanOldDeletedItems(); const allTrash = getDeletedNotes(); if (allTrash.length === 0) { showToast('回收站已空'); return; } if (await customConfirm(`确定要清空回收站所有的笔记(共 ${allTrash.length} 篇)吗?此操作不可恢复!`)) { const notes = getAllNotes(); const now = Date.now(); notes.forEach(item => { if (item.deleted && !item.permanentlyDeleted) { item.permanentlyDeleted = true; item.syncAt = now; } }); saveNotes(notes); showToast('已清空回收站'); const filterSelect = popupRoot.querySelector('#deleted-notes-filter-popover'); const days = filterSelect ? parseInt(filterSelect.getAttribute('data-value'), 10) : currentFilterDays; updateDeletedList(days, currentSortMode); } }; } const backBtn = popupRoot.querySelector('.deleted-notes-back'); if (backBtn) { backBtn.onclick = (e) => { e.stopPropagation(); const rect = getCurrentPopupRect(); if (rect) renderNotesPage(rect, true); }; } const closeBtn = popupRoot.querySelector('.close-btn'); if (closeBtn) { closeBtn.onclick = (e) => { e.stopPropagation(); hidePopup(); }; } showPopupAt(selectionRect); } function renderConfigPage(selectionRect) { stopCurrentAudio(); currentPageType = 'other'; hideCustomTooltip(); isConfigPageOpen = true; const popupEl = createPopup(); applyThemeToPopup(); popupEl.classList.remove('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const currentKey = GM_getValue("deepseek_api_key", ""); const storedTtsUrl = GM_getValue('custom_tts_api', ''); const currentTtsSpeed = getTTSSpeed(); const enabledEngines = getEnabledEngines(); const enableTtsBtn = isTTSBtnEnabled(); const currentTheme = getThemeMode(); const currentTypewriterSpeedKey = getTypewriterSpeedKey(); const upstashUrl = GM_getValue('upstash_redis_rest_url', ''); const upstashToken = GM_getValue('upstash_redis_rest_token', ''); const currentGoogleTts = GM_getValue('google_tts_engine', 'google'); const currentPromptWord = GM_getValue('deepseek_prompt_word', DEFAULT_DEEPSEEK_PROMPT_WORD); const currentPromptSentence = GM_getValue('deepseek_prompt_sentence', DEFAULT_DEEPSEEK_PROMPT_SENTENCE); const currentPromptChat = GM_getValue('deepseek_prompt_chat', DEFAULT_DEEPSEEK_PROMPT_CHAT); const currentNoteDisplay = GM_getValue('note_display_mode', 'markdown'); const showNotebook = GM_getValue('show_notebook_btn', true); const showWordlist = GM_getValue('show_wordlist_btn', true); const closeIconSvg = ``; const checkboxHtml = ALL_ENGINES.map(eng => ` `).join('') + `` + `` + `` + `` + ``; const youdaoWordFormsChecked = GM_getValue('youdao_show_wordforms', true) ? 'checked' : ''; const youdaoExamplesChecked = GM_getValue('youdao_show_examples', true) ? 'checked' : ''; const youdaoDisplayHtml = `
有道词典显示内容
`; const speedOptions = Object.keys(TTS_SPEED_MAP).map(key => ({ value: key, label: TTS_SPEED_MAP[key].label })); const speedPopoverHtml = createCustomPopoverHtml('cfg-tts-speed', speedOptions, currentTtsSpeed); const typewriterSpeedOptions = Object.keys(TYPEWRITER_SPEED_MAP).map(key => ({ value: key, label: TYPEWRITER_SPEED_MAP[key].label })); const typewriterSpeedPopoverHtml = createCustomPopoverHtml('cfg-typewriter-speed', typewriterSpeedOptions, currentTypewriterSpeedKey); const ttsPlaceholder = `未配置,将使用默认 Edge TTS 节点`; const promptsExpanded = GM_getValue('deepseek_prompts_expanded', false); const expandIcon = promptsExpanded ? '▾' : '▸'; const rewardHtml = `
使用教程 问题反馈
`; const html = `
设置
配置 DeepSeek API Key 查看教程
DeepSeek AI 翻译引擎,可自定义提示词
打字速度: ${typewriterSpeedPopoverHtml}
自定义提示词设置 (点击展开/收起) ${expandIcon}
恢复默认值
恢复默认值
恢复默认值
配置 Edge TTS 节点 查看教程
微软翻译发音引擎,部署于 Cloudflare Workers
语速调节: ${speedPopoverHtml}
配置 Upstash 云同步(可选) 查看教程
将笔记本和单词本保存到云端,可跨设备同步
Serverless Data Platform
启用翻译引擎与功能
${checkboxHtml}
${youdaoDisplayHtml}
谷歌翻译发音引擎
笔记详情查看模式
主题模式
${rewardHtml}
`; setElementHtml(contentWrapper, html); const popupRoot = getPopupRoot(); const rewardToggle = popupRoot.querySelector('#reward-toggle-btn'); const rewardContainer = popupRoot.querySelector('#reward-qr-container'); const rewardImg = popupRoot.querySelector('#reward-qr-img'); if (rewardToggle && rewardContainer) { rewardToggle.addEventListener('click', function(e) { e.stopPropagation(); if (rewardContainer.style.display === 'none') { if (WECHAT_REWARD_QR_BASE64) { rewardImg.src = WECHAT_REWARD_QR_BASE64; rewardContainer.style.display = 'block'; const scrollContainer = popupRoot.querySelector('.config-scroll-content'); if (scrollContainer) { setTimeout(() => { const containerRect = scrollContainer.getBoundingClientRect(); const qrRect = rewardContainer.getBoundingClientRect(); const targetScroll = scrollContainer.scrollTop + (qrRect.top - containerRect.top) - containerRect.height / 2 + qrRect.height / 2; scrollContainer.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' }); }, 50); } } else { showToast('找不到赞赏码'); } } else { rewardContainer.style.display = 'none'; } }); } const toggleBtn = popupRoot.querySelector('#prompts-toggle'); const content = popupRoot.querySelector('#prompts-content'); const arrow = popupRoot.querySelector('#prompts-arrow'); if (toggleBtn && content && arrow) { toggleBtn.addEventListener('click', function(e) { e.stopPropagation(); const isExpanded = content.style.display !== 'none'; if (isExpanded) { content.style.display = 'none'; arrow.textContent = '▸'; arrow.classList.remove('open'); GM_setValue('deepseek_prompts_expanded', false); } else { content.style.display = 'block'; arrow.textContent = '▾'; GM_setValue('deepseek_prompts_expanded', true); } }); } const resetLinks = popupRoot.querySelectorAll('.reset-prompt-link'); resetLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('data-target'); const textarea = popupRoot.querySelector(`#${targetId}`); if (!textarea) return; let defaultValue = ''; let promptName = ''; if (targetId === 'cfg-deepseek-prompt-word') { defaultValue = DEFAULT_DEEPSEEK_PROMPT_WORD; promptName = '单词翻译'; } else if (targetId === 'cfg-deepseek-prompt-sentence') { defaultValue = DEFAULT_DEEPSEEK_PROMPT_SENTENCE; promptName = '句子翻译'; } else if (targetId === 'cfg-deepseek-prompt-chat') { defaultValue = DEFAULT_DEEPSEEK_PROMPT_CHAT; promptName = '聊天模式'; } textarea.value = defaultValue; showToast(`已恢复「${promptName}」默认提示词`); }); }); const testBtn = popupRoot.querySelector('.tts-test-btn'); let isTTSTesting = false; if (testBtn) { testBtn.onclick = (e) => { e.stopPropagation(); if (isTTSTesting) { showToast('请等待当前测试完成'); return; } const ttsInput = popupRoot.querySelector('#cfg-tts-api'); let testUrl = (ttsInput ? ttsInput.value.trim() : ''); if (!testUrl) { testUrl = DEFAULT_TTS_API; } const speedSelect = popupRoot.querySelector('#cfg-tts-speed'); const testSpeed = speedSelect ? speedSelect.getAttribute('data-value') : 'normal'; const statusEl = popupRoot.querySelector('.tts-status-msg'); isTTSTesting = true; testBtn.disabled = true; testBtn.style.opacity = '0.6'; testBtn.style.cursor = 'not-allowed'; if (statusEl) { statusEl.className = 'tts-status-msg'; statusEl.textContent = '正在测试 TTS 节点...'; statusEl.style.display = 'block'; } requestTTSBlob(testUrl, '你好,这是一个测试', 'zh-CN-XiaoxiaoNeural', testSpeed) .then(blob => { if (statusEl) { statusEl.className = 'tts-status-msg success'; statusEl.textContent = '✓ 节点测试成功!正在试播...'; } const blobUrl = URL.createObjectURL(blob); const audio = new Audio(blobUrl); audio.onended = () => URL.revokeObjectURL(blobUrl); audio.play().catch(() => {}); }) .catch(err => { if (statusEl) { statusEl.className = 'tts-status-msg error'; statusEl.textContent = `✕ 节点测试失败: ${err.message}`; } }) .finally(() => { isTTSTesting = false; testBtn.disabled = false; testBtn.style.opacity = '1'; testBtn.style.cursor = 'pointer'; }); }; } const dsTestBtn = popupRoot.querySelector('.ds-test-btn'); let isDSTesting = false; if (dsTestBtn) { dsTestBtn.onclick = (e) => { e.stopPropagation(); if (isDSTesting) { showToast('请等待当前测试完成'); return; } const keyInput = popupRoot.querySelector('#cfg-ds-key'); const apiKey = keyInput ? keyInput.value.trim() : ''; const statusEl = popupRoot.querySelector('.ds-status-msg'); if (!apiKey) { if (statusEl) { statusEl.className = 'ds-status-msg error'; statusEl.textContent = '✕ 请输入 API Key'; statusEl.style.display = 'block'; } return; } isDSTesting = true; dsTestBtn.disabled = true; dsTestBtn.style.opacity = '0.6'; dsTestBtn.style.cursor = 'not-allowed'; if (statusEl) { statusEl.className = 'ds-status-msg'; statusEl.textContent = '正在测试 DeepSeek API...'; statusEl.style.display = 'block'; } GM_xmlhttpRequest({ method: 'POST', url: 'https://api.deepseek.com/chat/completions', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: 'deepseek-chat', messages: [{ role: 'user', content: 'Say "Hello" in one word.' }], temperature: 0.3, max_tokens: 10 }), timeout: 10000, onload(res) { if (statusEl) { if (res.status === 200) { statusEl.className = 'ds-status-msg success'; statusEl.textContent = '✓ API Key 有效,测试成功!'; } else if (res.status === 401) { statusEl.className = 'ds-status-msg error'; statusEl.textContent = '✕ API Key 无效或已过期,请重新输入'; } else if (res.status === 402) { statusEl.className = 'ds-status-msg error'; statusEl.textContent = '✕ 账户余额不足,请充值'; } else { statusEl.className = 'ds-status-msg error'; statusEl.textContent = `✕ 请求失败 (HTTP ${res.status})`; } } isDSTesting = false; dsTestBtn.disabled = false; dsTestBtn.style.opacity = '1'; dsTestBtn.style.cursor = 'pointer'; }, ontimeout() { if (statusEl) { statusEl.className = 'ds-status-msg error'; statusEl.textContent = '✕ 请求超时,请检查网络'; } isDSTesting = false; dsTestBtn.disabled = false; dsTestBtn.style.opacity = '1'; dsTestBtn.style.cursor = 'pointer'; }, onerror() { if (statusEl) { statusEl.className = 'ds-status-msg error'; statusEl.textContent = '✕ 网络连接失败'; } isDSTesting = false; dsTestBtn.disabled = false; dsTestBtn.style.opacity = '1'; dsTestBtn.style.cursor = 'pointer'; } }); }; } const upstashTestBtn = popupRoot.querySelector('.upstash-test-btn'); let isUpstashTesting = false; if (upstashTestBtn) { upstashTestBtn.onclick = (e) => { e.stopPropagation(); if (isUpstashTesting) { showToast('请等待当前测试完成'); return; } const urlInput = popupRoot.querySelector('#cfg-upstash-url'); const tokenInput = popupRoot.querySelector('#cfg-upstash-token'); const statusEl = popupRoot.querySelector('.upstash-status-msg'); const testUrl = urlInput ? urlInput.value.trim() : ''; const testToken = tokenInput ? tokenInput.value.trim() : ''; if (!testUrl || !testToken) { if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'ds-status-msg error'; statusEl.textContent = '✕ 请填写 Upstash URL 和 Token'; } return; } isUpstashTesting = true; upstashTestBtn.disabled = true; upstashTestBtn.style.opacity = '0.6'; upstashTestBtn.style.cursor = 'not-allowed'; if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'upstash-status-msg'; statusEl.textContent = '正在测试 Upstash 连接...'; } fetchCloudFavorites(testUrl, testToken) .then(() => { if (statusEl) { statusEl.className = 'upstash-status-msg success'; statusEl.textContent = '✓ Upstash 连接成功!'; } }) .catch(err => { if (statusEl) { statusEl.className = 'upstash-status-msg error'; statusEl.textContent = `✕ 连接失败: ${err.message}`; } }) .finally(() => { isUpstashTesting = false; upstashTestBtn.disabled = false; upstashTestBtn.style.opacity = '1'; upstashTestBtn.style.cursor = 'pointer'; }); }; } const deepseekCheckbox = popupRoot.querySelector('#cfg-engine-deepseek'); const deepseekSection = popupRoot.querySelector('#deepseek-config-section'); if (deepseekCheckbox && deepseekSection) { const toggleSection = () => { deepseekSection.style.display = deepseekCheckbox.checked ? '' : 'none'; }; toggleSection(); deepseekCheckbox.addEventListener('change', toggleSection); } const upstashCheckbox = popupRoot.querySelector('#cfg-upstash-enabled'); const upstashSection = popupRoot.querySelector('#upstash-config-section'); if (upstashCheckbox && upstashSection) { const toggleUpstashSection = () => { upstashSection.style.display = upstashCheckbox.checked ? '' : 'none'; }; toggleUpstashSection(); upstashCheckbox.addEventListener('change', toggleUpstashSection); } const cancelBtn = popupRoot.querySelector('.cancel-config-btn'); if (cancelBtn) { cancelBtn.onclick = (e) => { e.stopPropagation(); isConfigPageOpen = false; if (lastSelectionText) { fetchAndRender(lastSelectionText); } else { hidePopup(); } }; } const saveBtn = popupRoot.querySelector('.save-config-btn'); if (saveBtn) { saveBtn.onclick = (e) => { e.stopPropagation(); const keyInput = popupRoot.querySelector('#cfg-ds-key'); if (keyInput) { GM_setValue("deepseek_api_key", keyInput.value.trim()); } const ttsInput = popupRoot.querySelector('#cfg-tts-api'); const ttsVal = ttsInput ? ttsInput.value.trim() : ''; const oldTtsVal = GM_getValue('custom_tts_api', '').trim(); GM_setValue("custom_tts_api", ttsVal); if (oldTtsVal !== ttsVal) { clearEdgeTtsCache(); } const ttsSpeedSelect = popupRoot.querySelector('#cfg-tts-speed'); if (ttsSpeedSelect) { GM_setValue("custom_tts_speed", ttsSpeedSelect.getAttribute('data-value')); } const typewriterSpeedSelect = popupRoot.querySelector('#cfg-typewriter-speed'); if (typewriterSpeedSelect) { GM_setValue('deepseek_typewriter_speed', typewriterSpeedSelect.getAttribute('data-value')); } const upstashUrlInput = popupRoot.querySelector('#cfg-upstash-url'); const upstashTokenInput = popupRoot.querySelector('#cfg-upstash-token'); if (upstashUrlInput) { GM_setValue('upstash_redis_rest_url', upstashUrlInput.value.trim()); } if (upstashTokenInput) { GM_setValue('upstash_redis_rest_token', upstashTokenInput.value.trim()); } const checkboxes = popupRoot.querySelectorAll('input[name="cfg-engine"]:checked'); const selectedEngines = Array.from(checkboxes).map(cb => cb.value); if (selectedEngines.length === 0) { showToast("请至少保留一个翻译引擎!"); return; } GM_setValue('enabled_engines', selectedEngines); const ttsBtnCb = popupRoot.querySelector('#cfg-tts-btn'); if (ttsBtnCb) { GM_setValue('enable_tts_btn', ttsBtnCb.checked); } if (!selectedEngines.includes(currentEngine)) { currentEngine = selectedEngines[0]; } const showNotebookCb = popupRoot.querySelector('#cfg-show-notebook'); if (showNotebookCb) { GM_setValue('show_notebook_btn', showNotebookCb.checked); } const showWordlistCb = popupRoot.querySelector('#cfg-show-wordlist'); if (showWordlistCb) { GM_setValue('show_wordlist_btn', showWordlistCb.checked); } const disableOutsideCloseCb = popupRoot.querySelector('#cfg-disable-outside-close'); if (disableOutsideCloseCb) { GM_setValue('disable_outside_click_close', disableOutsideCloseCb.checked); } const selectedTheme = popupRoot.querySelector('input[name="cfg-theme"]:checked')?.value || 'system'; GM_setValue('theme_mode', selectedTheme); applyThemeToPopup(); const showWordForms = popupRoot.querySelector('#cfg-youdao-wordforms').checked; const showExamples = popupRoot.querySelector('#cfg-youdao-examples').checked; GM_setValue('youdao_show_wordforms', showWordForms); GM_setValue('youdao_show_examples', showExamples); const googleTtsSelected = popupRoot.querySelector('input[name="cfg_google_tts"]:checked'); if (googleTtsSelected) { GM_setValue('google_tts_engine', googleTtsSelected.value); } const promptWord = popupRoot.querySelector('#cfg-deepseek-prompt-word'); if (promptWord) GM_setValue('deepseek_prompt_word', promptWord.value); const promptSentence = popupRoot.querySelector('#cfg-deepseek-prompt-sentence'); if (promptSentence) GM_setValue('deepseek_prompt_sentence', promptSentence.value); const promptChat = popupRoot.querySelector('#cfg-deepseek-prompt-chat'); if (promptChat) GM_setValue('deepseek_prompt_chat', promptChat.value); const upstashEnabledCb = popupRoot.querySelector('#cfg-upstash-enabled'); if (upstashEnabledCb) { GM_setValue('upstash_enabled', upstashEnabledCb.checked); } const noteDisplay = popupRoot.querySelector('input[name="cfg_note_display"]:checked')?.value || 'markdown'; GM_setValue('note_display_mode', noteDisplay); if (isUpstashConfigured()) { syncFavoriteWithCloudAfterLocalChange().catch(err => console.warn('同步失败', err)); syncNotesWithCloudAfterLocalChange().catch(err => console.warn('笔记同步失败', err)); } showToast("配置保存成功"); isConfigPageOpen = false; if (lastSelectionText) { fetchAndRender(lastSelectionText); } else { hidePopup(); } }; } showPopupAt(selectionRect); } function handlePasteAndTranslate() { if (!navigator.clipboard || typeof navigator.clipboard.readText !== 'function') { showToast('当前环境不支持读取剪贴板'); return; } navigator.clipboard.readText() .then(raw => { const clipText = String(raw || '').replace(/\r\n?/g, '\n').trim(); if (!clipText) { showToast('剪贴板内容为空'); return; } if (!isValidSelection(clipText)) { showToast('剪贴板内容过长(最多 3000 字符)'); return; } if (clipText === lastSelectionText && currentPageType === 'translation') { fetchAndRender(clipText); return; } stopCurrentAudio(); clearAllTypewriters(); lastSelectionText = clipText; fetchAndRender(clipText); }) .catch(err => { console.warn('[粘贴翻译] 读取剪贴板失败:', err); showToast('读取剪贴板失败,请检查浏览器权限'); }); } function onPopupClick(event) { const popupRoot = getPopupRoot(); const togglePwdBtn = getClosestElement(event.target, '.toggle-pwd-btn'); if (togglePwdBtn) { event.stopPropagation(); event.preventDefault(); const wrapper = togglePwdBtn.closest('.input-with-eye'); const inputField = wrapper ? wrapper.querySelector('.key-input-field') : null; if (inputField) { const isPassword = inputField.type === 'password'; inputField.type = isPassword ? 'text' : 'password'; togglePwdBtn.innerHTML = isPassword ? getTrustedHtml(EYE_SVG_OPEN) : getTrustedHtml(EYE_SVG_CLOSED); togglePwdBtn.title = isPassword ? '隐藏' : '显示'; } return; } const popoverTrigger = getClosestElement(event.target, '.popover-trigger'); const popoverOption = getClosestElement(event.target, '.popover-option'); if (popoverTrigger) { event.stopPropagation(); const parentSelect = popoverTrigger.closest('.custom-popover-select'); const wasOpen = parentSelect.classList.contains('open'); popupRoot.querySelectorAll('.custom-popover-select.open').forEach(el => el.classList.remove('open')); if (!wasOpen) { parentSelect.classList.add('open'); } return; } if (popoverOption) { event.stopPropagation(); const parentSelect = popoverOption.closest('.custom-popover-select'); const val = popoverOption.getAttribute('data-value'); const labelText = popoverOption.querySelector('span')?.textContent || ''; parentSelect.setAttribute('data-value', val); const labelEl = parentSelect.querySelector('.popover-label'); if (labelEl) labelEl.textContent = labelText; parentSelect.querySelectorAll('.popover-option').forEach(opt => opt.classList.remove('selected')); popoverOption.classList.add('selected'); parentSelect.classList.remove('open'); if (parentSelect.id === 'target-lang-popover') { targetLang = val; if (lastSelectionText) fetchAndRender(lastSelectionText); return; } if (parentSelect.id === 'src-tts-speed-popover') { microsoftSrcTtsSpeed = val; const last = microsoftLastPlayed; stopCurrentAudio(); if (last && last.type === 'src' && (Date.now() - last.time) < 8000) { playTTS(last.text, last.voice, last.container, val); } return; } if (parentSelect.id === 'target-tts-speed-popover') { microsoftTargetTtsSpeed = val; const last = microsoftLastPlayed; stopCurrentAudio(); if (last && last.type === 'target' && (Date.now() - last.time) < 8000) { playTTS(last.text, last.voice, last.container, val); } return; } return; } popupRoot.querySelectorAll('.custom-popover-select.open').forEach(el => el.classList.remove('open')); event.stopPropagation(); const configBtn = getClosestElement(event.target, '.config-btn'); if (configBtn) { event.preventDefault(); const rect = getSelectionRect() || { left: popup.offsetLeft, top: popup.offsetTop, width: popup.offsetWidth, height: popup.offsetHeight }; renderConfigPage(rect); return; } const wordListBtn = getClosestElement(event.target, '.word-list-btn'); if (wordListBtn) { event.preventDefault(); const rect = getSelectionRect() || { left: popup.offsetLeft, top: popup.offsetTop, width: popup.offsetWidth, height: popup.offsetHeight }; renderWordListPage(rect); return; } const notesBtn = getClosestElement(event.target, '.notes-btn'); if (notesBtn) { event.preventDefault(); const rect = getSelectionRect() || { left: popup.offsetLeft, top: popup.offsetTop, width: popup.offsetWidth, height: popup.offsetHeight }; renderNotesPage(rect); return; } const saveKeyBtn = getClosestElement(event.target, '.save-key-btn'); if (saveKeyBtn && !isConfigPageOpen) { const inputEl = popupRoot.querySelector('.key-input-field'); const keyVal = inputEl ? inputEl.value.trim() : ''; if (keyVal) { GM_setValue("deepseek_api_key", keyVal); showToast("API Key 已保存,正在重新查询..."); currentCache.deepseek = { data: null, history: [] }; if (lastSelectionText) fetchAndRender(lastSelectionText); } else { showToast("请输入有效 API Key"); } return; } const closeBtn = getClosestElement(event.target, '.close-btn'); if (closeBtn) { if (isConfigPageOpen && lastSelectionText) { isConfigPageOpen = false; fetchAndRender(lastSelectionText); } else { hidePopup(); } return; } const engineBtn = getClosestElement(event.target, '.engine-icon-btn, .engine-btn'); if (engineBtn) { const targetEngine = engineBtn.getAttribute('data-engine'); if (targetEngine && targetEngine !== currentEngine) { microsoftLastPlayed = null; if (currentEngine === 'deepseek') { saveDeepSeekCache(); } stopCurrentAudio(); clearAllTypewriters(); currentEngine = targetEngine; if (targetEngine === 'deepseek') { const cache = currentCache.deepseek; if (cache && cache.data) { deepseekState.history = cache.history.slice(); } else { deepseekState = { history: [] }; } } else { deepseekState = { history: [] }; } if (lastSelectionText) fetchAndRender(lastSelectionText); } return; } const pasteBtn = getClosestElement(event.target, '.paste-btn'); if (pasteBtn) { event.stopPropagation(); event.preventDefault(); handlePasteAndTranslate(); return; } const copyBtn = getClosestElement(event.target, '.copy-btn'); if (copyBtn) { const text = decodeURIComponent(copyBtn.getAttribute('data-text') || ''); if (text) { navigator.clipboard.writeText(text).then(() => { showToast('已复制到剪贴板'); const originalColor = copyBtn.style.color; copyBtn.style.color = '#1a73e8'; setTimeout(() => { copyBtn.style.color = originalColor; }, 800); }).catch(e => { console.warn('复制失败', e); showToast('复制失败'); }); } return; } const playBtn = getClosestElement(event.target, '.play-btn'); if (playBtn && playBtn.classList.contains('edge-tts-audio')) { const messageWrapper = playBtn.closest('.ds-message-wrapper'); const span = messageWrapper ? messageWrapper.querySelector('.ds-message.assistant .message-text') : null; let text = span ? span.textContent : ''; if (!text) { text = decodeURIComponent(playBtn.getAttribute('data-text') || ''); } const voice = playBtn.getAttribute('data-voice') || 'zh-CN-XiaoxiaoNeural'; if (text) { playTTS(text, voice); return; } } const favBtn = getClosestElement(event.target, '.favorite-btn'); if (favBtn) { const word = favBtn.getAttribute('data-word'); const newFav = toggleFavoriteWord(word); favBtn.setAttribute('data-fav', newFav ? '1' : '0'); favBtn.innerHTML = newFav ? getTrustedHtml(``) : getTrustedHtml(``); favBtn.setAttribute('data-custom-tooltip', newFav ? '移入回收站' : '添加到单词本'); showToast(newFav ? `已添加到单词本` : `已移入回收站`); return; } const sortOption = getClosestElement(event.target, '.sort-option'); if (sortOption) { const mode = sortOption.getAttribute('data-sort'); wordListPageState.sortMode = mode; popupRoot.querySelectorAll('.sort-option').forEach(opt => opt.classList.remove('active')); sortOption.classList.add('active'); renderWordListContent(); return; } const pronounceBtn = getClosestElement(event.target, '.word-list-pronounce'); if (pronounceBtn) { const word = pronounceBtn.getAttribute('data-word'); const isChinese = /[\u4e00-\u9fa5]/.test(word); const voice = isChinese ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; playTTS(word, voice); return; } const searchBtn = getClosestElement(event.target, '.word-list-search'); if (searchBtn) { const word = searchBtn.getAttribute('data-word'); lastSelectionText = word; currentEngine = 'youdao'; currentCache.text = ''; currentCache.youdao = null; fetchAndRender(word); return; } const copyWordBtn = getClosestElement(event.target, '.word-list-copy'); if (copyWordBtn) { const word = copyWordBtn.getAttribute('data-word'); if (word) { navigator.clipboard.writeText(word) .then(() => showToast(`已复制到剪贴板`)) .catch(() => showToast('复制失败,请检查权限')); const originalColor = copyWordBtn.style.color; copyWordBtn.style.color = '#1a73e8'; setTimeout(() => { copyWordBtn.style.color = originalColor; }, 800); } return; } const toggleFavBtn = getClosestElement(event.target, '.word-list-toggle-fav'); if (toggleFavBtn) { const word = toggleFavBtn.getAttribute('data-word'); removeFavoriteWord(word); wordListPageState.favorites = getFavoriteWords().filter(item => !item.deleted); renderWordListContent(); showToast(`已移入回收站`); return; } const saveExitBtn = getClosestElement(event.target, '.word-list-save-exit'); if (saveExitBtn) { if (lastSelectionText) { fetchAndRender(lastSelectionText); } else { hidePopup(); } return; } const cancelListBtn = getClosestElement(event.target, '.word-list-cancel'); if (cancelListBtn) { const words = getFavoriteWords().filter(item => !item.deleted); if (words.length === 0) { showToast('没有收藏的单词'); } else { const text = words.map(item => item.word).join('\n'); navigator.clipboard.writeText(text) .then(() => { showToast(`已复制 ${words.length} 个单词`); }) .catch(() => { showToast('复制失败,请检查权限'); }); } return; } const pronounce = getClosestElement(event.target, '.pronounce-button, .google-audio, .edge-tts-audio'); if (pronounce) { if (pronounce.classList.contains('edge-tts-audio')) { const text = decodeURIComponent(pronounce.getAttribute('data-text') || ''); const voice = pronounce.getAttribute('data-voice') || 'zh-CN-XiaoxiaoNeural'; const highlightContainer = findTtsHighlightContainer(pronounce, text); const scope = pronounce.getAttribute('data-tts-speed-scope'); let speedKey = null; if (scope === 'src') { speedKey = microsoftSrcTtsSpeed; microsoftLastPlayed = { type: 'src', text, voice, container: highlightContainer, time: Date.now() }; } else if (scope === 'target') { speedKey = microsoftTargetTtsSpeed; microsoftLastPlayed = { type: 'target', text, voice, container: highlightContainer, time: Date.now() }; } playTTS(text, voice, highlightContainer, speedKey); } else if (pronounce.classList.contains('google-audio')) { const text = decodeURIComponent(pronounce.getAttribute('data-text') || ''); const lang = pronounce.getAttribute('data-lang') || 'en'; playGoogleAudio(text, lang); } else { const item = getClosestElement(pronounce, '.phonetic-item'); const accent = item?.getAttribute('data-type') || 'uk'; const audioUrl = item?.getAttribute('data-audio'); const word = getPopupRoot().querySelector('.editable-word')?.textContent || lastSelectionText; if (word) playPronunciation(word, accent, audioUrl); } return; } const restoreBtn = getClosestElement(event.target, '.trash-restore-btn'); if (restoreBtn) { const word = restoreBtn.getAttribute('data-word'); if (word) { const words = getFavoriteWords(); const item = words.find(w => w.word === word); if (item && item.deleted) { const now = Date.now(); item.deleted = false; item.deletedAt = 0; item.time = now; item.syncAt = now; item.permanentlyDeleted = false; saveFavoriteWords(words); showToast(`已移出回收站`); const popupRootNow = getPopupRoot(); if (popupRootNow && typeof popupRootNow.__updateTrashList === 'function') { const filterSelect = popupRootNow.querySelector('#trash-filter-popover'); const days = filterSelect ? parseInt(filterSelect.getAttribute('data-value'), 10) : 1; const sortMode = popupRootNow.__currentTrashSortMode || 'time'; popupRootNow.__updateTrashList(days, sortMode); } else { const rect = getCurrentPopupRect(); if (rect) renderTrashPage(rect); } } } return; } const addNoteBtn = getClosestElement(event.target, '.add-note-btn'); if (addNoteBtn) { event.stopPropagation(); const index = addNoteBtn.getAttribute('data-index'); if (index !== null) { const idx = parseInt(index, 10); const history = deepseekState.history; if (idx > 0 && idx < history.length && history[idx].role === 'assistant') { const userMsg = history[idx - 1]; if (userMsg && userMsg.role === 'user') { const title = userMsg.content; const content = `${userMsg.content}\n${history[idx].content}`; addNoteFromTranslation(title, content); } else { showToast('无法获取对话内容'); } } else { showToast('无效的对话索引'); } } else { const title = decodeURIComponent(addNoteBtn.getAttribute('data-title') || ''); const content = decodeURIComponent(addNoteBtn.getAttribute('data-content') || ''); if (title && content) { addNoteFromTranslation(title, content); } else { showToast('内容为空'); } } return; } } function hidePopup() { if (window.__translookup_confirm_showing) return; if (!popup) return; if (typeof globalNoteTtsStop === 'function') { try { globalNoteTtsStop(); } catch (e) {} } globalNoteTtsStop = null; hideCustomTooltip(); stopCurrentAudio(); clearAllTypewriters(); popup.style.display = 'none'; popup.classList.remove('mini-mode'); savedRange = null; isConfigPageOpen = false; } function showMiniLookupPopup(selectionRect, text) { if (window.__translookup_confirm_showing) return; if (!selectionRect || !isValidSelection(text)) { hidePopup(); lastSelectionText = ''; return; } const popupEl = createPopup(); popupEl.classList.add('mini-mode'); const contentWrapper = getPopupRoot().querySelector('.popup-content'); const enabledEngines = getEnabledEngines(); const enableTtsBtn = isTTSBtnEnabled(); if (contentWrapper) { let miniBtnsHtml = ''; if (enabledEngines.includes('youdao')) { miniBtnsHtml += ` `; } if (enabledEngines.includes('microsoft')) { miniBtnsHtml += ` `; } if (enabledEngines.includes('google')) { miniBtnsHtml += ` `; } if (enabledEngines.includes('deepseek')) { miniBtnsHtml += ` `; } if (enableTtsBtn) { const isChinese = /[\u4e00-\u9fa5]/.test(text); const voice = isChinese ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; miniBtnsHtml += ` `; } setElementHtml(contentWrapper, miniBtnsHtml); const btns = contentWrapper.querySelectorAll('.mini-icon-btn'); btns.forEach(btn => { if (btn.classList.contains('edge-tts-audio')) return; btn.onclick = (event) => { event.stopPropagation(); event.preventDefault(); if (window.getSelection) { window.getSelection().removeAllRanges(); } clearTimeout(selectionTimeout); const engine = btn.getAttribute('data-engine'); if (engine) { currentEngine = engine; if (engine === 'deepseek') { const cache = currentCache.deepseek; if (cache && cache.data) { deepseekState.history = cache.history.slice(); } else { deepseekState = { history: [] }; } } else { deepseekState = { history: [] }; } } showTranslationForSelection(selectionRect, text, { force: true }); }; }); } showPopupAt(selectionRect); } function repositionPopup() { if (!popup || popup.style.display !== 'block') return; const selectionRect = getSelectionRect(); if (!selectionRect) { return; } popup.style.maxWidth = `${document.documentElement.clientWidth - 20}px`; showPopupAt(selectionRect); } function showPopupAt(selectionRect) { if (!popup || !selectionRect) return; popup.style.display = 'block'; const container = popup.shadowRoot?.querySelector('.popup-container'); let originalTransition = ''; if (container) { originalTransition = container.style.transition; container.style.transition = 'none'; } const rect = popup.getBoundingClientRect(); if (container) container.style.transition = originalTransition; const clientWidth = document.documentElement.clientWidth; const clientHeight = document.documentElement.clientHeight; const offsetY = 18; const targetCenterX = selectionRect.left + selectionRect.width / 2; let left = targetCenterX - rect.width / 2; left = Math.max(10, Math.min(left, clientWidth - rect.width - 10)); let top = selectionRect.bottom + offsetY; popup.classList.remove('above-selection'); if (top + rect.height > clientHeight - 10) { top = Math.max(10, selectionRect.top - rect.height - offsetY); popup.classList.add('above-selection'); } const scrollX = window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft || 0; const scrollY = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0; popup.style.left = `${left + scrollX}px`; popup.style.top = `${top + scrollY}px`; const arrow = getPopupRoot().querySelector('.popup-arrow'); if (arrow) { let arrowLeft = targetCenterX - left; arrowLeft = Math.max(18, Math.min(arrowLeft, rect.width - 18)); arrow.style.left = `${arrowLeft}px`; } } function buildPopupHeader() { let webUrl = '#'; const encodedText = encodeURIComponent(lastSelectionText || ''); const isZh = targetLang === 'zh-Hans' || targetLang === 'zh-CN'; const enabledEngines = getEnabledEngines(); if (!enabledEngines.includes(currentEngine)) { currentEngine = enabledEngines[0] || 'youdao'; } if (currentEngine === 'youdao') { webUrl = `https://www.youdao.com/w/eng/${encodedText}`; } else if (currentEngine === 'microsoft') { const targetLangCode = isZh ? 'zh-Hans' : 'en'; webUrl = `https://www.bing.com/translator?from=auto&to=${targetLangCode}&text=${encodedText}`; } else if (currentEngine === 'google') { const targetLangCode = isZh ? 'zh-CN' : 'en'; webUrl = `https://translate.google.com/?sl=auto&tl=${targetLangCode}&text=${encodedText}&op=translate`; } else if (currentEngine === 'deepseek') { webUrl = `https://chat.deepseek.com/`; } const engineNameMap = { 'youdao': '有道词典', 'microsoft': '微软翻译', 'google': '谷歌翻译', 'deepseek': 'DeepSeek' }; const currentEngineName = engineNameMap[currentEngine] || '官网'; const title = `打开「${currentEngineName}」官网`; const externalIconSvg = ``; const configIconSvg = ``; const closeIconSvg = ``; const wordListIconSvg = ``; let engineBtnsHtml = ''; if (enabledEngines.includes('youdao')) { engineBtnsHtml += ` `; } if (enabledEngines.includes('microsoft')) { engineBtnsHtml += ` `; } if (enabledEngines.includes('google')) { engineBtnsHtml += ` `; } if (enabledEngines.includes('deepseek')) { engineBtnsHtml += ` `; } let headerActionsHtml = ` ${externalIconSvg} `; if (isNotebookEnabled()) { headerActionsHtml += ` `; } if (isWordlistEnabled()) { headerActionsHtml += ` `; } headerActionsHtml += ` `; return `
${engineBtnsHtml}
${headerActionsHtml}
`; } function parseYoudaoHtml(html, word) { const template = document.createElement('template'); template.innerHTML = getTrustedHtml(typeof html === 'string' ? html : String(html)); const doc = template.content; const result = { word, phonetics: [], audio: {}, meanings: [], wordForms: [], examples: [] }; const phoneCon = doc.querySelector('.word-head .phone_con'); if (phoneCon) { const perPhones = phoneCon.querySelectorAll('.per-phone'); perPhones.forEach(el => { const labelSpan = el.querySelector('span:first-child'); const phoneticSpan = el.querySelector('span.phonetic'); if (!labelSpan) return; const label = labelSpan.textContent.trim(); const type = label === '英' ? 'uk' : 'us'; const phonetic = phoneticSpan ? phoneticSpan.textContent.replace(/[\/\s]/g, '').trim() : ''; const audioUrl = `https://dict.youdao.com/dictvoice?type=${type === 'uk' ? 1 : 2}&audio=${encodeURIComponent(word)}`; result.audio[type] = audioUrl; result.phonetics.push({ label, phonetic, type }); }); } if (!result.audio.uk) result.audio.uk = `https://dict.youdao.com/dictvoice?type=1&audio=${encodeURIComponent(word)}`; if (!result.audio.us) result.audio.us = `https://dict.youdao.com/dictvoice?type=2&audio=${encodeURIComponent(word)}`; const basicUl = doc.querySelector('.simple ul.basic'); if (basicUl) { const items = basicUl.querySelectorAll('li.word-exp'); items.forEach(li => { const posSpan = li.querySelector('span.pos'); const transSpan = li.querySelector('span.trans'); let pos = posSpan ? posSpan.textContent.trim() : ''; let trans = transSpan ? transSpan.textContent.trim() : ''; const full = pos ? `${pos} ${trans}` : trans; if (full) result.meanings.push(full); }); } const wfsUl = doc.querySelector('ul.word-wfs-less'); if (wfsUl) { const items = wfsUl.querySelectorAll('li.word-wfs-cell-less'); items.forEach(li => { const greyP = li.querySelector('p.grey'); const transSpan = li.querySelector('span.transformation'); if (greyP && transSpan) { const name = greyP.textContent.trim().replace(/[::]/g, '').trim(); const value = transSpan.textContent.trim(); if (name && value) { result.wordForms.push({ name, value }); } } }); } if (result.meanings.length === 0) { const text = doc.body ? doc.body.textContent.replace(/\s+/g, ' ').trim() : ''; if (text) result.meanings.push(text.substring(0, 500)); } const sentContainer = doc.querySelector('.blng_sents_part .trans-container ul'); if (sentContainer) { const items = sentContainer.querySelectorAll('li.mcols-layout'); items.forEach(li => { const col2 = li.querySelector('.col2'); if (!col2) return; const exps = col2.querySelectorAll('.word-exp'); if (exps.length >= 2) { const engDiv = exps[0].querySelector('.sen-eng'); const chDiv = exps[1].querySelector('.sen-ch'); let source = ''; if (exps.length >= 3) { const srcDiv = exps[2].querySelector('.secondary'); if (srcDiv) source = srcDiv.textContent.trim(); } if (engDiv && chDiv) { result.examples.push({ en: engDiv.innerHTML.trim(), zh: chDiv.textContent.trim() }); } } }); } if (result.examples.length > 5) result.examples = result.examples.slice(0, 5); return result; } function buildYoudaoBody(word, data) { const safeWord = escapeHtml(word); const phonetics = data?.phonetics || []; const hasMeaning = Array.isArray(data?.meanings) && data.meanings.length > 0; const isFav = isWordFavorite(word); const phoneticItems = phonetics.length > 0 ? phonetics : [{ label: '英', phonetic: '', type: 'uk' }, { label: '美', phonetic: '', type: 'us' }]; phoneticItems.sort((a, b) => (a.type === b.type ? 0 : a.type === 'uk' ? -1 : 1)); const showWordForms = GM_getValue('youdao_show_wordforms', true); const showExamples = GM_getValue('youdao_show_examples', true); const audioIconSvg = ``; const phoneticsHtml = phonetics.length ? phonetics.map(item => { const normalizedPhonetic = escapeHtml((item.phonetic || '').replace(/^\[+/, '').replace(/\]+$/, '')); return `
${escapeHtml(item.label)} ${normalizedPhonetic ? `/${normalizedPhonetic}/` : ''} ${audioIconSvg}
`; }).join('') : ''; const meaningsHtml = hasMeaning ? data.meanings.map(line => `
${escapeHtml(line)}
`).join('') : '
未找到该单词释义
'; let examplesHtml = ''; if (showExamples && data.examples && data.examples.length) { const exampleItems = data.examples.map(exp => { const cleanText = exp.en.replace(/<[^>]*>/g, ''); return `
${exp.en}
${escapeHtml(exp.zh)}
${exp.source ? `
${escapeHtml(exp.source)}
` : ''}
`; }).join(''); examplesHtml = `
双语例句
${exampleItems}
`; } let wordFormsHtml = ''; if (showWordForms && data.wordForms && data.wordForms.length) { const items = data.wordForms.map(wf => `${escapeHtml(wf.name)}:${escapeHtml(wf.value)}` ).join(''); wordFormsHtml = `
${items}
`; } const tabsHtml = DICTIONARY_SOURCES.map(source => `${escapeHtml(source.label)}`).join(''); const favButtonHtml = hasMeaning ? ` ` : ''; return `
${safeWord} ${favButtonHtml}
${phoneticsHtml ? `
${phoneticsHtml}
` : ''}
${meaningsHtml}${wordFormsHtml}${examplesHtml}
${tabsHtml}
`; } function buildTranslationBody(word, data, engine) { const safeWord = escapeHtml(word); const srcLangCode = data?.srcLang || 'en'; let srcLangName = escapeHtml((LANG_MAP[srcLangCode] || srcLangCode.toUpperCase()) + ' - 检测到的语言'); if (data && data.apiSource) { srcLangName += data.apiSource === 'primary' ? '' : ' [备]'; } const translationText = data?.translation || '未找到译文'; const safeTranslationText = escapeHtml(translationText); const srcPinyinText = escapeHtml(data?.srcPinyin || ''); const pinyinText = escapeHtml(data?.pinyin || ''); const pasteIconSvg = ``; const copyIconSvg = ``; const audioIconSvg = ``; const targetLangCodeGoogle = targetLang === 'en' ? 'en' : 'zh-CN'; let audioBtnSrcHTML = ''; let audioBtnTargetHTML = ''; let speedPopoverSrcHtml = ''; let speedPopoverTargetHtml = ''; if (engine === 'google') { const useEdge = GM_getValue('google_tts_engine', 'google') === 'edge'; if (useEdge) { const srcVoice = (srcLangCode === 'zh-CN' || srcLangCode === 'zh-Hans') ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; const targetVoice = (targetLang === 'en') ? 'en-US-JennyNeural' : 'zh-CN-XiaoxiaoNeural'; audioBtnSrcHTML = ``; audioBtnTargetHTML = ``; } else { audioBtnSrcHTML = ``; audioBtnTargetHTML = ``; } } else if (engine === 'microsoft') { const srcVoice = (srcLangCode === 'zh-CN' || srcLangCode === 'zh-Hans') ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; const targetVoice = (targetLang === 'en') ? 'en-US-JennyNeural' : 'zh-CN-XiaoxiaoNeural'; audioBtnSrcHTML = ``; audioBtnTargetHTML = ``; const defaultSpeedKey = getTTSSpeed(); if (microsoftSrcTtsSpeed === null) microsoftSrcTtsSpeed = defaultSpeedKey; if (microsoftTargetTtsSpeed === null) microsoftTargetTtsSpeed = defaultSpeedKey; const speedOptions = Object.keys(TTS_SPEED_MAP).map(key => ({ value: key, label: TTS_SPEED_MAP[key].label })); speedPopoverSrcHtml = createCustomPopoverHtml('src-tts-speed-popover', speedOptions, microsoftSrcTtsSpeed); speedPopoverTargetHtml = createCustomPopoverHtml('target-tts-speed-popover', speedOptions, microsoftTargetTtsSpeed); } const addNoteTitle = encodeURIComponent(word); const addNoteContent = encodeURIComponent(`${word}\n\n${translationText}`); const langOptions = [ { value: 'zh-Hans', label: '中文 (简体)' }, { value: 'en', label: '英文' } ]; const targetLangPopoverHtml = createCustomPopoverHtml('target-lang-popover', langOptions, targetLang); return `
${srcLangName}
${audioBtnSrcHTML} ${speedPopoverSrcHtml}
${safeWord}
${srcPinyinText ? `
${srcPinyinText}
` : ''}
${targetLangPopoverHtml}
${audioBtnTargetHTML} ${speedPopoverTargetHtml}
${safeTranslationText}
${pinyinText ? `
${pinyinText}
` : ''}
`; } function queryWord(word) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: API_URL + encodeURIComponent(word) + '&lang=en', timeout: 8000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'Referer': 'https://dict.youdao.com/' }, onload(res) { if (res.status >= 200 && res.status < 300) { resolve(parseYoudaoHtml(res.responseText || res.response, word)); } else { reject(new Error(`有道响应失败 (HTTP ${res.status})`)); } }, ontimeout() { reject(new Error('有道查词超时,请检查网络')); }, onerror() { reject(new Error('有道网络连接错误')); } }); }); } function queryGoogle(word, tl = 'zh-CN') { const targetLang = tl === 'en' ? 'en' : 'zh-Hans'; return GoogleService.translate(word, tl).then(result => { const cacheKey = getCacheKey('google', word, targetLang); setTranslationCache(cacheKey, { data: result }); return result; }); } function queryMicrosoft(text, tl = 'zh-Hans') { const targetTl = (tl === 'en') ? 'en' : 'zh-CN'; const url = `https://edge.microsoft.com/translate/translatetext?from=&to=${targetTl}&isEnterpriseClient=false`; const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; const requestBody = JSON.stringify([text]); const parseResult = (data) => { let translation = ''; let srcLang = 'en'; if (Array.isArray(data) && data[0]) { if (data[0].translations && data[0].translations[0]) { translation = data[0].translations[0].text; } else if (typeof data[0] === 'string') { translation = data[0]; } if (data[0].detectedLanguage && data[0].detectedLanguage.language) { srcLang = data[0].detectedLanguage.language; } } if (!translation) { throw new Error('微软翻译未返回有效译文'); } return { word: text, translation, pinyin: '', srcPinyin: '', srcLang }; }; const performGMRequest = () => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url, headers, data: requestBody, timeout: 8000, onload(res) { if (res.status < 200 || res.status >= 300) { reject(new Error(`微软翻译请求失败 (HTTP ${res.status})`)); return; } try { const data = JSON.parse(res.responseText); resolve(parseResult(data)); } catch (e) { reject(e instanceof Error ? e : new Error('微软翻译数据解析失败')); } }, ontimeout() { reject(new Error('微软翻译请求超时')); }, onerror() { reject(new Error('微软翻译网络连接失败')); } }); }); }; const isEdge = /Edg\//i.test(navigator.userAgent) || /EdgA\//i.test(navigator.userAgent) || /EdgiOS\//i.test(navigator.userAgent); if (!isEdge) { return performGMRequest(); } return fetch(url, { method: 'POST', headers, body: requestBody }) .then(response => { if (!response.ok) { throw new Error(`微软翻译请求失败 (HTTP ${response.status})`); } return response.json(); }) .then(parseResult) .catch(() => performGMRequest()); } function getSelectedText() { const selection = window.getSelection(); return (selection && !selection.isCollapsed) ? selection.toString().trim() : ''; } function isValidSelection(text) { return !!text && text.length <= 3000; } function renderTranslationResult(text, data, engine, history, skipTypewriter = false) { const contentWrapper = getPopupRoot()?.querySelector('.popup-content'); if (!contentWrapper) return; if (engine === 'youdao') { const body = buildYoudaoBody(text, data); setElementHtml(contentWrapper, buildPopupHeader() + body); } else if (engine === 'deepseek') { const hist = history || currentCache.deepseek?.history || []; deepseekState.history = hist.slice(); const headerHtml = buildPopupHeader(); const conversationHtml = `
`; setElementHtml(contentWrapper, headerHtml + conversationHtml); const conv = contentWrapper.querySelector('#ds-conversation'); if (conv) { setElementHtml(conv, ''); const speedMs = getTypewriterSpeedMs(); hist.forEach((msg, idx) => { if (msg.role === 'assistant') { const wrapper = document.createElement('div'); wrapper.className = 'ds-message-wrapper'; const div = document.createElement('div'); div.className = 'ds-message assistant'; const span = document.createElement('span'); span.className = 'message-text'; div.appendChild(span); wrapper.appendChild(div); const actionsRow = document.createElement('div'); actionsRow.className = 'ds-actions-row'; const copyBtn = document.createElement('button'); copyBtn.className = 'gt-icon-btn copy-btn'; copyBtn.setAttribute('data-text', encodeURIComponent(msg.content)); copyBtn.title = '复制'; copyBtn.innerHTML = getTrustedHtml(``); copyBtn.style.opacity = '0'; actionsRow.appendChild(copyBtn); const playBtn = document.createElement('button'); playBtn.className = 'gt-icon-btn play-btn edge-tts-audio'; const isChinese = /[\u4e00-\u9fa5]/.test(msg.content); const voice = isChinese ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; playBtn.setAttribute('data-text', encodeURIComponent(msg.content)); playBtn.setAttribute('data-voice', voice); playBtn.title = '播放'; playBtn.innerHTML = getTrustedHtml(``); playBtn.style.opacity = '0'; actionsRow.appendChild(playBtn); const addNoteBtn = document.createElement('button'); addNoteBtn.className = 'gt-icon-btn add-note-btn'; addNoteBtn.setAttribute('data-index', idx); addNoteBtn.title = '添加到笔记本'; addNoteBtn.innerHTML = getTrustedHtml(NOTEBOOK_ICON_SVG_ADD); addNoteBtn.style.opacity = '0'; actionsRow.appendChild(addNoteBtn); wrapper.appendChild(actionsRow); conv.appendChild(wrapper); if (idx === hist.length - 1) { if (skipTypewriter) { if (window.marked) { setDeepSeekMessageContent(span, msg.content); } else { span.textContent = msg.content; } copyBtn.style.opacity = '1'; playBtn.style.opacity = '1'; addNoteBtn.style.opacity = '1'; } else { typewriter(span, msg.content, speedMs, true, () => { copyBtn.style.opacity = '1'; playBtn.style.opacity = '1'; addNoteBtn.style.opacity = '1'; }); } } else { if (window.marked) { setDeepSeekMessageContent(span, msg.content); } else { span.textContent = msg.content; } copyBtn.style.opacity = '1'; playBtn.style.opacity = '1'; addNoteBtn.style.opacity = '1'; } } else { const wrapper = document.createElement('div'); wrapper.className = 'ds-message-wrapper user-wrapper'; const div = document.createElement('div'); div.className = 'ds-message user'; div.textContent = msg.content; wrapper.appendChild(div); const actionsRow = document.createElement('div'); actionsRow.className = 'ds-actions-row'; const copyBtn = document.createElement('button'); copyBtn.className = 'gt-icon-btn copy-btn'; copyBtn.setAttribute('data-text', encodeURIComponent(msg.content)); copyBtn.title = '复制'; copyBtn.innerHTML = getTrustedHtml(``); copyBtn.style.opacity = '1'; actionsRow.appendChild(copyBtn); const playBtn = document.createElement('button'); playBtn.className = 'gt-icon-btn play-btn edge-tts-audio'; const isChineseUser = /[\u4e00-\u9fa5]/.test(msg.content); const voiceUser = isChineseUser ? 'zh-CN-XiaoxiaoNeural' : 'en-US-JennyNeural'; playBtn.setAttribute('data-text', encodeURIComponent(msg.content)); playBtn.setAttribute('data-voice', voiceUser); playBtn.title = '播放'; playBtn.innerHTML = getTrustedHtml(``); playBtn.style.opacity = '1'; actionsRow.appendChild(playBtn); wrapper.appendChild(actionsRow); conv.appendChild(wrapper); } }); attachDeepSeekEvents(); } } else { const langKey = targetLang === 'en' ? 'en' : 'zh-Hans'; const body = buildTranslationBody(text, data, engine); setElementHtml(contentWrapper, buildPopupHeader() + body); } afterRenderBindScrollbars(); bindPopupEvents(); requestAnimationFrame(() => showPopupAt(getSelectionRect())); } function renderCachedContent(text) { const engine = currentEngine; let data, history; if (engine === 'youdao') { data = currentCache.youdao; } else if (engine === 'deepseek') { data = currentCache.deepseek?.data; history = currentCache.deepseek?.history; } else { const langKey = targetLang === 'en' ? 'en' : 'zh-Hans'; data = currentCache[engine]?.[langKey]; } if (data) { renderTranslationResult(text, data, engine, history, true); } } function fetchAndRender(text) { microsoftLastPlayed = null; currentPageType = 'translation'; hideCustomTooltip(); isConfigPageOpen = false; applyThemeToPopup(); const contentWrapper = getPopupRoot()?.querySelector('.popup-content'); if (!contentWrapper) return; const requestId = ++currentRequestId; const reqEngine = currentEngine; const langKey = targetLang === 'en' ? 'en' : 'zh-Hans'; const cacheKey = getCacheKey(reqEngine, text, targetLang); const requestKey = `${reqEngine}:${text}:${targetLang}`; if (currentCache.text !== text) { currentCache = { text, youdao: null, microsoft: {}, google: {}, deepseek: { data: null, history: [] } }; deepseekState = { history: [] }; } const cached = translationCache.get(cacheKey); if (cached) { if (currentPageType !== 'translation') return; if (reqEngine === 'deepseek') { currentCache.deepseek.data = cached.data; currentCache.deepseek.history = cached.history || []; deepseekState.history = cached.history || []; } else { if (reqEngine === 'youdao') currentCache.youdao = cached.data; else if (reqEngine === 'microsoft') currentCache.microsoft[langKey] = cached.data; else if (reqEngine === 'google') currentCache.google[langKey] = cached.data; } renderCachedContent(text); return; } let engineCache = null; if (reqEngine === 'youdao') engineCache = currentCache.youdao; else if (reqEngine === 'deepseek') engineCache = currentCache.deepseek?.data || null; else engineCache = currentCache[reqEngine]?.[langKey]; if (engineCache) { const history = reqEngine === 'deepseek' ? currentCache.deepseek?.history : undefined; renderTranslationResult(text, engineCache, reqEngine, history, true); return; } const safeText = escapeHtml(text); const loadingTip = (reqEngine === 'deepseek') ? '思考中... ( 请勿离开此页面 )' : '正在查询...'; setElementHtml(contentWrapper, ` ${buildPopupHeader()}
${safeText}
${loadingTip}
`); requestAnimationFrame(() => showPopupAt(getSelectionRect())); const existingRequest = pendingRequests.get(requestKey); let queryPromise; if (existingRequest) { queryPromise = existingRequest; } else { if (reqEngine === 'youdao') { queryPromise = queryWord(text); } else if (reqEngine === 'deepseek') { queryPromise = queryDeepSeek(text, targetLang); } else if (reqEngine === 'microsoft') { queryPromise = queryMicrosoft(text, targetLang); } else { queryPromise = queryGoogle(text, targetLang); } pendingRequests.set(requestKey, queryPromise); queryPromise.finally(() => { if (pendingRequests.get(requestKey) === queryPromise) { pendingRequests.delete(requestKey); } }); } queryPromise .then(data => { if (reqEngine === 'youdao') { currentCache.youdao = data; setTranslationCache(cacheKey, { data }); } else if (reqEngine === 'deepseek') { const history = [{ role: 'user', content: text }, { role: 'assistant', content: data.translation }]; currentCache.deepseek.data = data; currentCache.deepseek.history = history; setTranslationCache(cacheKey, { data, history }); } else { if (!currentCache[reqEngine]) currentCache[reqEngine] = {}; currentCache[reqEngine][langKey] = data; setTranslationCache(cacheKey, { data }); } if (requestId === currentRequestId && lastSelectionText === text && currentEngine === reqEngine && currentPageType === 'translation') { const history = reqEngine === 'deepseek' ? currentCache.deepseek.history : undefined; renderTranslationResult(text, data, reqEngine, history); } }) .catch(error => { if (requestId === currentRequestId && lastSelectionText === text && currentEngine === reqEngine && currentPageType === 'translation') { if (error && error.isHtml) { setElementHtml(contentWrapper, ` ${buildPopupHeader()}
${escapeHtml(text)}
${error.message} `); } else { const errorMsg = error?.message || '查询失败'; setElementHtml(contentWrapper, ` ${buildPopupHeader()}
${escapeHtml(text)}
${escapeHtml(errorMsg)}
`); } requestAnimationFrame(() => showPopupAt(getSelectionRect())); } }); } function showTranslationForSelection(selectionRect, text, options = {}) { if (window.__translookup_confirm_showing) return; const currentRect = getSelectionRect() || selectionRect; if (!currentRect || !isValidSelection(text)) { lastSelectionText = ''; hidePopup(); return; } if (!options.force && lastSelectionText === text && popup && !popup.classList.contains('mini-mode')) return; lastSelectionText = text; const popupEl = createPopup(); popupEl.classList.remove('mini-mode'); fetchAndRender(text); } function getSelectionRect() { const selection = window.getSelection(); if (selection && !selection.isCollapsed && selection.rangeCount > 0) { savedRange = selection.getRangeAt(0).cloneRange(); } if (!savedRange) return null; const range = savedRange; let rect = range.getBoundingClientRect(); if (rect && rect.width > 0 && rect.height > 0) return rect; const clientRects = Array.from(range.getClientRects()).filter(r => r.width > 0 && r.height > 0); if (clientRects.length === 0) return null; const left = Math.min(...clientRects.map(r => r.left)); const top = Math.min(...clientRects.map(r => r.top)); const right = Math.max(...clientRects.map(r => r.right)); const bottom = Math.max(...clientRects.map(r => r.bottom)); return { left, top, width: right - left, height: bottom - top, right, bottom }; } document.addEventListener('dblclick', event => { if (window.__translookup_confirm_showing) return; if (isEventInsidePopup(event)) return; const disableOutsideClose = GM_getValue('disable_outside_click_close', false); if (disableOutsideClose && popup && popup.style.display === 'block' && !popup.classList.contains('mini-mode')) { return; } const text = getSelectedText(); if (!text) return; const selectionRect = getSelectionRect(); if (!selectionRect) return; showMiniLookupPopup(selectionRect, text); }, true); function handlePendingSelection() { if (window.__translookup_confirm_showing) return; if (interactingWithPopup) return; const text = getSelectedText(); const disableOutsideClose = GM_getValue('disable_outside_click_close', false); if (disableOutsideClose && popup && popup.style.display === 'block' && !popup.classList.contains('mini-mode')) { return; } if (!text) { if (popup && popup.style.display === 'block') { if (popup.classList.contains('mini-mode')) { hidePopup(); lastSelectionText = ''; } else { return; } } return; } if (popup && popup.style.display === 'block' && !popup.classList.contains('mini-mode')) { if (text === lastSelectionText) return; } const active = getClosestElement(document.activeElement, 'input, textarea, [contenteditable="true"]'); if (active) return; const selectionRect = getSelectionRect(); if (!selectionRect) return; showMiniLookupPopup(selectionRect, text); } function isTextInputTarget(element) { return getClosestElement(element, 'input, textarea, [contenteditable="true"]'); } function scheduleSelectionCheck() { if (window.__translookup_confirm_showing) return; clearTimeout(selectionTimeout); selectionTimeout = setTimeout(handlePendingSelection, 150); } function selectionChanged(event) { if (window.__translookup_confirm_showing) return; if ( interactingWithPopup || isEventInsidePopup(event) || isTextInputTarget(event.target) ) { return; } if (pointerIsDown && touchScrollActive) { return; } if (pointerIsDown) { touchSelectionPending = true; clearTimeout(selectionTimeout); selectionTimeout = setTimeout(() => { if (touchScrollActive) return; touchSelectionPending = false; handlePendingSelection(); }, 120); return; } scheduleSelectionCheck(); } function selectionMouseUp(event) { if (window.__translookup_confirm_showing) return; pointerIsDown = false; if ( event.type === 'touchend' && touchSource === 'popup' ) { clearTimeout(selectionTimeout); touchSelectionPending = false; touchScrollActive = false; if (interactingWithPopup) { clearTimeout(popupInteractionTimeout); popupInteractionTimeout = setTimeout(() => { interactingWithPopup = false; mouseDownInPopup = false; }, 300); } touchSource = null; return; } if ( event.type === 'touchend' && touchSource === 'page' && touchScrollActive ) { clearTimeout(selectionTimeout); touchSelectionPending = false; setTimeout(() => { touchScrollActive = false; }, 80); touchSource = null; return; } if (interactingWithPopup) { clearTimeout(popupInteractionTimeout); popupInteractionTimeout = setTimeout(() => { interactingWithPopup = false; }, 300); } if ( event.type === 'mouseup' && event.button !== 0 ) { return; } if ( isEventInsidePopup(event) || isTextInputTarget(event.target) ) { return; } if (event.type === 'touchend') { if (touchSelectionPending) { clearTimeout(selectionTimeout); touchSelectionPending = false; selectionTimeout = setTimeout(() => { if (!touchScrollActive) { handlePendingSelection(); } }, 80); } else { scheduleSelectionCheck(); } touchSource = null; return; } scheduleSelectionCheck(); } document.addEventListener('mousedown', event => { if (event.button === 2) return; pointerIsDown = true; clearTimeout(selectionTimeout); if (isEventInsidePopup(event)) { interactingWithPopup = true; mouseDownInPopup = true; clearTimeout(popupInteractionTimeout); } else { interactingWithPopup = false; mouseDownInPopup = true; const disableOutsideClose = GM_getValue('disable_outside_click_close', false); if (popup && (!disableOutsideClose || popup.classList.contains('mini-mode'))) { hidePopup(); } } }, true); document.addEventListener('touchstart', event => { pointerIsDown = true; clearTimeout(selectionTimeout); touchScrollActive = false; touchSelectionPending = false; suppressNextOutsideClick = false; const touch = event.touches && event.touches[0]; if (touch) { touchStartX = touch.clientX; touchStartY = touch.clientY; } if (isEventInsidePopup(event)) { touchSource = 'popup'; interactingWithPopup = true; mouseDownInPopup = true; clearTimeout(popupInteractionTimeout); return; } touchSource = 'page'; interactingWithPopup = false; }, { capture: true, passive: true }); document.addEventListener('touchmove', event => { const touch = event.touches && event.touches[0]; if (!touch) return; const dx = touch.clientX - touchStartX; const dy = touch.clientY - touchStartY; if ((dx * dx + dy * dy) <= 64) { return; } if (touchSource === 'popup') { touchScrollActive = false; touchSelectionPending = false; clearTimeout(selectionTimeout); return; } if (touchSource === 'page') { touchScrollActive = true; touchSelectionPending = false; clearTimeout(selectionTimeout); if ( popup && popup.style.display === 'block' && !popup.classList.contains('mini-mode') ) { suppressNextOutsideClick = true; } } }, { capture: true, passive: true }); document.addEventListener('selectionchange', selectionChanged, true); document.addEventListener('mouseup', selectionMouseUp, true); document.addEventListener('touchend', selectionMouseUp, true); document.addEventListener('touchcancel', selectionMouseUp, true); let repositionTimeout = null; document.addEventListener('scroll', () => { clearTimeout(repositionTimeout); repositionTimeout = setTimeout(repositionPopup, 50); }, { capture: true, passive: true }); window.addEventListener('resize', () => { clearTimeout(repositionTimeout); repositionTimeout = setTimeout(repositionPopup, 50); }); document.addEventListener('click', event => { if (window.__translookup_confirm_showing) return; if (suppressNextOutsideClick) { suppressNextOutsideClick = false; return; } if (mouseDownInPopup) { mouseDownInPopup = false; return; } if (popup && popup.style.display === 'block') { const rect = popup.getBoundingClientRect(); if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) { return; } } if (!popup || !isEventInsidePopup(event)) { const disableOutsideClose = GM_getValue('disable_outside_click_close', false); if ( !disableOutsideClose || popup.classList.contains('mini-mode') ) { hidePopup(); } } }, true); window.addEventListener('keydown', event => { if (event.altKey && event.key.toLowerCase() === 'q') { const text = getSelectedText(); const selectionRect = getSelectionRect(); if (text && selectionRect) { showTranslationForSelection(selectionRect, text, { force: true }); } } }, true); function ensureDataIntegrity() { let words = GM_getValue('favorite_words', []); if (!Array.isArray(words)) { GM_setValue('favorite_words', []); } else { migrateFavoriteData(); } let notes = GM_getValue(NOTES_STORAGE_KEY, []); if (!Array.isArray(notes)) { GM_setValue(NOTES_STORAGE_KEY, []); } } migrateFavoriteData(); cleanOldDeletedItems(); setInterval(cleanOldDeletedItems, 24 * 60 * 60 * 1000); ensureDataIntegrity(); })();