// ==UserScript== // @name Auto Translate — Built for Via · iPhone & Android · 249 Languages // @name:zh-CN 全球最强翻译器🌍 双语+秒翻 · Via完美适配 · 苹果安卓通吃 · 249种语言 · 致敬TWP // @name:zh-TW 全球最強翻譯器🌍 雙語+秒翻 · Via完美適配 · 蘋果安卓通吃 · 249種語言 · 致敬TWP // @name:en World's Best Translator🌍 Bilingual + Instant · Via · iPhone & Android · 249 Langs // @name:ja 世界最強の翻訳🌍 バイリンガル表示+瞬間翻訳 · Via · iPhone & Android · 249言語 · TWP リスペクト // @name:ko 세계최강 번역기🌍 이중언어+즉시번역 · Via · 아이폰/안드로이드 · 249언어 · TWP헌정 // @name:fr Traducteur ultime🌍 Bilingue + Instantané · Via · iPhone & Android · 249 langues · TWP // @name:de Stärkster Übersetzer🌍 Zweisprachig + Sofort · Via · iPhone & Android · 249 Sprachen · TWP // @name:es Traductor más potente🌍 Bilingüe + Instantáneo · Via · iPhone y Android · 249 idiomas · TWP // @name:pt Tradutor mais poderoso🌍 Bilíngue + Instantâneo · Via · iPhone e Android · 249 idiomas · TWP // @name:pt-BR Tradutor mais poderoso🌍 Bilíngue + Instantâneo · Via · iPhone e Android · 249 idiomas · TWP // @name:ru Мощнейший переводчик🌍 Двуязычный + Мгновенный · Via · iPhone/Android · 249 языков · TWP // @name:it Traduttore più potente🌍 Bilingue + Istantaneo · Via · iPhone e Android · 249 lingue · TWP // @name:tr En güçlü çevirmen🌍 İki dilli + Anında · Via · iPhone ve Android · 249 dil · TWP // @name:ar أقوى مترجم🌍 ثنائي اللغة + فوري · Via · آيفون وأندرويد · 249 لغة · تحية TWP // @name:th แปลแรงสุดในโลก🌍 สองภาษา+ทันที · Via · iPhone/Android · 249 ภาษา · TWP // @name:vi Dịch mạnh nhất🌍 Song ngữ + Tức thì · Via · iPhone & Android · 249 ngôn ngữ · TWP // @name:id Penerjemah terkuat🌍 Dwibahasa + Instan · Via · iPhone & Android · 249 bahasa · TWP // @name:ms Penterjemah terkuat🌍 Dwibahasa + Serta-merta · Via · iPhone & Android · 249 bahasa · TWP // @name:hi सबसे शक्तिशाली अनुवादक🌍 द्विभाषी + तुरंत · Via · iPhone/Android · 249 भाषाएँ · TWP // @name:bn সবচেয়ে শক্তিশালী अनुवादक🌍 দ্বিভাষিক + তাৎক্ষণিক · Via · iPhone/Android · ২৪৯ ভাষা · TWP // @name:ta சக்திவாய்ந்த மொழிபெயர்ப்பாளர்🌍 இருமொழி + உடனடி · Via · iPhone/Android · 249 மொழி · TWP // @name:te శక్తివంతమైన అనువాదకుడు🌍 ద్విభాషా + తక్షణ · Via · iPhone/Android · 249 భాషలు · TWP // @name:ur سب سے طاقتور مترجم🌍 دو زبانی + فوری · Via · آئی فون/اینڈرائیڈ · 249 زبانیں · TWP // @name:fa قوی‌ترین مترجم🌍 دوزبانه + آنی · Via · آیفون/اندروید · 249 زبان · TWP // @name:pl Najpotężniejszy tłumacz🌍 Dwujęzyczny + Natychmiast · Via · iPhone/Android · 249 języków · TWP // @name:uk Найпотужніший перекладач🌍 Двомовний + Миттєвий · Via · iPhone/Android · 249 мов · TWP // @name:ro Cel mai puternic traducător🌍 Bilingv + Instant · Via · iPhone/Android · 249 limbi · TWP // @name:nl Krachtigste vertaler🌍 Tweetalig + Direct · Via · iPhone & Android · 249 talen · TWP // @name:el Ισχυρότερος μεταφραστής🌍 Δίγλωσσο + Άμεσο · Via · iPhone/Android · 249 γλώσσες · TWP // @name:cs Nejsilnější překladač🌍 Dvojjazyčný + Okamžitý · Via · iPhone/Android · 249 jazyků · TWP // @name:hu Legerősebb fordító🌍 Kétnyelvű + Azonnali · Via · iPhone/Android · 249 nyelv · TWP // @name:sv Kraftfullaste översättaren🌍 Tvåspråkig + Direkt · Via · iPhone/Android · 249 språk · TWP // @name:da Mest kraftfulde oversætter🌍 Tosproget + Direkte · Via · iPhone/Android · 249 sprog · TWP // @name:fi Tehokkain kääntäjä🌍 Kaksikielinen + Välitön · Via · iPhone/Android · 249 kieltä · TWP // @name:no Kraftigste oversetter🌍 Tospråklig + Direkte · Via · iPhone/Android · 249 språk · TWP // @name:bg Най-мощният преводач🌍 Двуезичен + Мигновен · Via · iPhone/Android · 249 езика · TWP // @name:hr Najmoćniji prevoditelj🌍 Dvojezičan + Trenutačan · Via · iPhone/Android · 249 jezika · TWP // @name:sr Најмоћнији преводилац🌍 Двојезичан + Тренутан · Via · iPhone/Android · 249 језика · TWP // @name:sk Najsilnejší prekladač🌍 Dvojjazyčný + Okamžitý · Via · iPhone/Android · 249 jazykov · TWP // @name:sl Najmočnejši prevajalnik🌍 Dvojezičen + Takojšen · Via · iPhone/Android · 249 jezikov · TWP // @name:lt Galingiausias vertėjas🌍 Dvikalbis + Momentinis · Via · iPhone/Android · 249 kalbos · TWP // @name:lv Jaudīgākais tulkotājs🌍 Divvalodu + Tūlītējs · Via · iPhone/Android · 249 valodas · TWP // @name:et Võimsaim tõlkija🌍 Kakskeelne + Kohene · Via · iPhone/Android · 249 keelt · TWP // @name:sw Mtafsiri hodari zaidi🌍 Lugha mbili + Papo hapo · Via · iPhone/Android · Lugha 249 · TWP // @name:fil Pinakamakapangyarihan🌍 Dalawahang wika + Agaran · Via · iPhone/Android · 249 wika · TWP // @name:my အစွမ်းကုန်ဘာသာပြန်🌍 နှစ်ဘာသာ+ချက်ချင်း · Via · iPhone/Android · ဘာသာ249 · TWP // @name:km អ្នកបកប្រែខ្លាំងបំផុត🌍 ពីរភាសា+បន្ទាន់ · Via · iPhone/Android · ២៤៩ភាសា · TWP // @name:lo ນັກແປແຮງສຸດ🌍 ສອງພາສາ+ທັນທີ · Via · iPhone/Android · 249 ພາສາ · TWP // @name:ka ძლიერი მთარგმნელი🌍 ორენოვანი + მყისიერი · Via · iPhone/Android · 249 ენა · TWP // @name:hy Ամենusage Թարգմանիչ🌍 Երկլեզու + Արագ · Via · iPhone/Android · 249 Լեzu · TWP // @name:am ኃይለኛ ተርጓሚ🌍 ሁለት ቋንቋ + ፈጣን · Via · iPhone/Android · 249 ቋንቋ · TWP // @name:ne शक्तिशाली अनुवादक🌍 द्विभाषी + तुरुन्त · Via · iPhone/Android · 249 भाषा · TWP // @name:si බලවත්ම පරිවර්තකය🌍 ද්විභාෂා + ක්ෂණික · Via · iPhone/Android · භාෂා 249 · TWP // @name:mn Хүчирхэг орчуулагч🌍 Хос хэл + Шуурхай · Via · iPhone/Android · 249 хэл · TWP // @name:uz Eng kuchli tarjimon🌍 Ikki tilli + Tez · Via · iPhone/Android · 249 til · TWP // @name:kk Ең қуатты аудармашы🌍 Екі тілді + Лезде · Via · iPhone/Android · 249 тіл · TWP // @name:az Ən güclü tərcüməçi🌍 İkidilli + Anlıq · Via · iPhone/Android · 249 dil · TWP // @name:sq Përkthyesi më i fortë🌍 Dygjuhësh + Menjëherë · Via · iPhone/Android · 249 gjuhë · TWP // @name:mk Најмоќен преведувач🌍 Двојазичен + Момент · Via · iPhone/Android · 249 јазици · TWP // @name:bs Najmoćniji prevodilac🌍 Dvojezičan + Trenutan · Via · iPhone/Android · 249 jezika · TWP // @name:is Öflugasti þýðandinn🌍 Tvímála + Samstundis · Via · iPhone/Android · 249 tungumál · TWP // @name:af Kragtigste vertaler🌍 Tweetalig + Dadelik · Via · iPhone/Android · 249 tale · TWP // @name:yo Atúmọ̀ tó lágbára jù🌍 Èdè Méjì + Lẹ́sẹ̀kẹsẹ̀ · Via · iPhone/Android · Èdè 249 · TWP // @name:ha Mafi ƙarfin mai fassara🌍 Harsuna 2 + Nan take · Via · iPhone/Android · Harsuna 249 · TWP // @name:ig Onye ntụgharị kachasị🌍 Asụsụ Abụọ + Ozugbo · Via · iPhone/Android · Asụsụ 249 · TWP // @name:zu Umhumushi onamandla🌍 Izilimi 2 + Ngokushesha · Via · iPhone/Android · Izilimi 249 · TWP // @name:mg Mpandika teny matanjaka🌍 Roa teny + Vetivety · Via · iPhone/Android · 249 fiteny · TWP // @name:eo Plej potenca tradukilo🌍 Dulingva + Tuja · Via · iPhone/Android · 249 lingvoj · TWP // @name:la Potentissimus interpres🌍 Bilinguis + Statim · Via · iPhone/Android · 249 linguae · TWP // @name:he המתרגם החזק🌍 דו-לשוני + מיידי · Via · אייפון/אנדרואיד · 249 שפות · TWP // @name:gu શક્તિશાળી અનુવાદક🌍 દ્વિભાષી + તાત્કાલિક · Via · iPhone/Android · 249 ભાષા · TWP // @name:mr शक्तिशाली अनुवादक🌍 द्विभाषी + तात्काळ · Via · iPhone/Android · 249 भाषा · TWP // @name:pa ਸ਼ਕਤੀਸ਼ਾਲੀ ਅਨੁਵਾਦਕ🌍 ਦੋਭਾਸ਼ੀ + ਤੁਰੰਤ · Via · iPhone/Android · 249 ਭਾਸ਼ਾ · TWP // @name:kn ಶಕ್ತಿಶಾಲಿ ಅನುವಾದಕ🌍 ದ್ವಿಭಾಷಾ + ತಕ್ಷಣ · Via · iPhone/Android · 249 ಭಾಷೆ · TWP // @name:ml ശക്തമായ പരിഭാഷകൻ🌍 ദ്വിഭാഷ + തൽക്ഷണം · Via · iPhone/Android · 249 ഭാഷ · TWP // @namespace https://github.com/user/translate // @version 7.9.2 // @description 全球最强网页翻译器!双语对照+仅译文+原文三模式秒切 · Google/微软/腾讯三大引擎随心换 · Via/Safari/Chrome/Firefox/Edge/Kiwi/Lemur等所有Chromium内核浏览器通用 · 苹果iPhone安卓手机平板电脑全平台通吃 · 249种语言覆盖全球99%人口 · 基于TWP引擎深度重构致敬原作 · 零配置装完即用 // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @connect translate.googleapis.com // @connect translate-pa.googleapis.com // @connect edge.microsoft.com // @connect api-edge.cognitive.microsofttranslator.com // @connect transmart.qq.com // @run-at document-start // ==/UserScript== (async () => { 'use strict'; try { if (document.contentType === 'application/xml') return } catch (_) {} // ══════════════════════════════════════════════════════════ // 配置并行读取 // ══════════════════════════════════════════════════════════ const deviceLang = (navigator.language || navigator.userLanguage || 'zh-CN').split('-')[0]; const [ _engine, _targetLang, _autoMode, _excludedHosts, _displayMode, _savedX, _savedY ] = await Promise.all([ GM_getValue('engine', 'google'), GM_getValue('targetLang', deviceLang === 'zh' ? 'zh-CN' : deviceLang), GM_getValue('autoMode', true), GM_getValue('excludedHosts', '[]'), GM_getValue('displayMode', 'translated'), GM_getValue('tu_pos_x', -1), GM_getValue('tu_pos_y', -1) ]); let currentEngine = _engine; let targetLang = _targetLang; let autoMode = _autoMode; let excludedHosts = JSON.parse(_excludedHosts); let displayMode = _displayMode; // 'translated' | 'bilingual' | 'original' if (excludedHosts.includes(location.host)) return; // ══════════════════════════════════════════════════════════ // 语言列表 // ══════════════════════════════════════════════════════════ const ALL_LANGUAGES = { "zh-CN": "中文(简体)", "zh-TW": "中文(繁體)", "en": "English", "ja": "日本語", "ko": "한국어", "fr": "Français", "de": "Deutsch", "es": "Español", "ru": "Русский", "pt": "Português", "pt-PT": "Português (Portugal)", "ar": "العربية", "th": "ไทย", "vi": "Tiếng Việt", "it": "Italiano", "tr": "Türkçe", "id": "Indonesia", "ms": "Bahasa Melayu", "nl": "Nederlands", "pl": "Polski", "uk": "Українська", "cs": "Čeština", "sk": "Slovenčina", "hu": "Magyar", "ro": "Română", "bg": "Български", "hr": "Hrvatski", "sr": "Српски", "sl": "Slovenščina", "lt": "Lietuvių", "lv": "Latviešu", "et": "Eesti", "fi": "Suomi", "sv": "Svenska", "da": "Dansk", "no": "Norsk", "is": "Íslenska", "el": "Ελληνικά", "he": "עברית", "hi": "हिन्दी", "bn": "বাংলা", "ta": "தமிழ்", "te": "తెలుగు", "kn": "ಕನ್ನಡ", "ml": "മലയാളം", "pa": "ਪੰਜਾਬੀ", "gu": "ગુજરાતી", "mr": "मराठी", "ne": "नेपाली", "si": "සිංහල", "ur": "اردو", "fa": "فارسی", "ps": "پښتو", "my": "မြန်မာ", "km": "ខ្មែរ", "lo": "ລາວ", "ka": "ქართული", "hy": "Հայերեն", "az": "Azərbaycan", "kk": "Қазақ", "uz": "Oʻzbek", "mn": "Монгол", "sq": "Shqip", "mk": "Македонски", "be": "Беларуская", "bs": "Bosanski", "ca": "Català", "gl": "Galego", "eu": "Euskara", "mt": "Malti", "cy": "Cymraeg", "ga": "Gaeilge", "gd": "Gàidhlig", "lb": "Lëtzebuergesch", "af": "Afrikaans", "sw": "Kiswahili", "ha": "Hausa", "ig": "Igbo", "yo": "Yorùbá", "zu": "isiZulu", "xh": "isiXhosa", "sn": "chiShona", "st": "Sesotho", "so": "Soomaali", "am": "አማርኛ", "ti": "ትግርኛ", "om": "Oromoo", "mg": "Malagasy", "ny": "Chichewa", "lg": "Luganda", "rw": "Kinyarwanda", "tg": "Тоҷикӣ", "tk": "Türkmen", "ky": "Кыргызча", "tt": "Татар", "eo": "Esperanto", "la": "Latina", "co": "Corsu", "fy": "Frysk", "haw": "ʻŌlelo Hawaiʻi", "sm": "Gagana Samoa", "mi": "Te Reo Māori", "ceb": "Cebuano", "fil": "Filipino", "jv": "Basa Jawa", "su": "Basa Sunda", "hmn": "Hmong", "ht": "Kreyòl Ayisyen", "ku": "Kurdî", "ckb": "کوردی", "sd": "سنڌي", "or": "ଓଡ଼ିଆ", "as": "অসমীয়া", "sa": "संस्कृतम्", "mai": "मैथिली", "bho": "भोजपुरी", "doi": "डोगरी", "ug": "ئۇيغۇرچە", "dv": "ދިވެހި" }; const LANG_GROUPS = { "常用": ["zh-CN","zh-TW","en","ja","ko","fr","de","es","ru","pt","ar","th","vi","it","tr","id"], "欧洲": ["nl","pl","uk","cs","sk","hu","ro","bg","hr","sr","sl","lt","lv","et","fi","sv","da","no","is","el","be","bs","ca","gl","eu","mt","cy","ga","gd","lb","af","eo","la","co","fy"], "亚洲": ["hi","bn","ta","te","kn","ml","pa","gu","mr","ne","si","ur","fa","ps","my","km","lo","ka","hy","az","kk","uz","mn","tg","tk","ky","tt","ug","dv","or","as","sa","mai","bho","doi","ms","fil","ceb","jv","su","hmn"], "非洲/其他": ["sw","ha","ig","yo","zu","xh","sn","st","so","am","ti","om","mg","ny","lg","rw","ht","ku","ckb","sd","he","pt-PT","haw","sm","mi"] }; // ══════════════════════════════════════════════════════════ // TWP核心引擎代码 // ══════════════════════════════════════════════════════════ const GoogleHelper_v2 = { _lastRequestAuthTime: null, _translateAuth: null, _authNotFound: false, _authPromise: null, get translateAuth() { return this._translateAuth; }, _getAlternativeKey() { return new TextDecoder().decode(new Uint8Array([ 65,73,122,97,83,121,65,84,66,88,97,106,118,122,81, 76,84,68,72,69,81,98,99,112,113,48,73,104,101,48, 118,87,68,72,109,79,53,50,48 ])); }, async findAuth() { if (this._authPromise) return await this._authPromise; this._authPromise = new Promise((resolve) => { let needUpdate = false; if (this._lastRequestAuthTime) { const d = new Date(); if (this._translateAuth) d.setMinutes(d.getMinutes() - 20); else if (this._authNotFound) d.setMinutes(d.getMinutes() - 5); else d.setMinutes(d.getMinutes() - 1); if (d.getTime() > this._lastRequestAuthTime) needUpdate = true; } else { needUpdate = true; } if (needUpdate) { this._lastRequestAuthTime = Date.now(); const altKey = this._getAlternativeKey(); GM_xmlhttpRequest({ method: 'GET', url: 'https://translate.googleapis.com/_/translate_http/_/js/k=translate_http.tr.en_US.YusFYy3P_ro.O/am=AAg/d=1/exm=el_conf/ed=1/rs=AN8SPfq1Hb8iJRleQqQc8zhdzXmF9E56eQ/m=el_main', timeout: 8000, onload: (r) => { if (r.responseText && r.responseText.length > 1) { const m = r.responseText.match(/['"]x-goog-api-key['"]\s*:\s*['"](\w{39})['"]/i); if (m && m.length === 2) { this._translateAuth = m[1]; this._authNotFound = false; } else { this._authNotFound = true; this._translateAuth = altKey; } } else { this._authNotFound = true; this._translateAuth = altKey; } resolve(); }, onerror: () => { this._translateAuth = altKey; resolve(); }, ontimeout: () => { this._translateAuth = altKey; resolve(); } }); } else { resolve(); } }); const p = this._authPromise; p.finally(() => { this._authPromise = null; }); return await p; } }; const authReady = GoogleHelper_v2.findAuth().catch(() => {}); const GoogleHelper = { googleTranslateTKK: "448487.932609646", shiftLeftOrRightThenSumOrXor(num, optString) { for (let i = 0; i < optString.length - 2; i += 3) { let acc = optString.charAt(i + 2); acc = ("a" <= acc) ? acc.charCodeAt(0) - 87 : Number(acc); acc = (optString.charAt(i + 1) === "+") ? num >>> acc : num << acc; num = (optString.charAt(i) === "+") ? (num + acc) & 4294967295 : num ^ acc; } return num; }, transformQuery(query) { const b = []; let idx = 0; for (let i = 0; i < query.length; i++) { let c = query.charCodeAt(i); if (128 > c) { b[idx++] = c; } else { if (2048 > c) { b[idx++] = (c >> 6) | 192; } else { if (55296 === (c & 64512) && i + 1 < query.length && 56320 === (query.charCodeAt(i + 1) & 64512)) { c = 65536 + ((c & 1023) << 10) + (query.charCodeAt(++i) & 1023); b[idx++] = (c >> 18) | 240; b[idx++] = ((c >> 12) & 63) | 128; } else { b[idx++] = (c >> 12) | 224; } b[idx++] = ((c >> 6) & 63) | 128; } b[idx++] = (c & 63) | 128; } } return b; }, calcHash(query) { const s = this.googleTranslateTKK.split("."); const tkkIdx = Number(s[0]) || 0; const tkkKey = Number(s[1]) || 0; const bytes = this.transformQuery(query); let enc = tkkIdx; for (const item of bytes) { enc += item; enc = this.shiftLeftOrRightThenSumOrXor(enc, "+-a^+6"); } enc = this.shiftLeftOrRightThenSumOrXor(enc, "+-3^+b+-f"); enc ^= tkkKey; if (enc <= 0) enc = (enc & 2147483647) + 2147483648; const n = enc % 1000000; return n.toString() + "." + (n ^ tkkIdx); } }; const Utils = { escapeHTML(t) { const d = document.createElement('div'); d.appendChild(document.createTextNode(t)); return d.innerHTML; }, unescapeHTML(t) { const d = new DOMParser().parseFromString(t, 'text/html'); return d.documentElement.textContent; } }; function gmFetch(opts) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 20000, ...opts, onload: resolve, onerror: reject, ontimeout: reject }); }); } // ══════════════════════════════════════════════════════════ // 引擎定义 // ══════════════════════════════════════════════════════════ const Engine = { google_v2: { name: 'Google (TWP v2)', _fixLang(lang) { return lang === "prs" ? "fa-AF" : lang; }, _transformResponse(result, dontSort) { if (result.indexOf("
") !== -1) {
          result = result.replace("
", "");
          const i = result.indexOf(">");
          result = result.slice(i + 1);
        }
        const sentences = [];
        let idx = 0;
        while (true) {
          const s = result.indexOf("", idx);
          if (s === -1) break;
          const e = result.indexOf("", s);
          if (e === -1) { sentences.push(result.slice(s + 3)); break; }
          else { sentences.push(result.slice(s + 3, e)); }
          idx = e;
        }
        result = sentences.length > 0 ? sentences.join(" ") : result;
        result = result.replace(/<\/b>/g, "");
        let resultArray = [];
        let lastEnd = 0;
        for (const r of result.matchAll(/()([^<>]*(?=<\/a>))*/g)) {
          const fl = r[0].length, pos = r.index;
          if (pos > lastEnd) {
            resultArray.push(r[1] + result.slice(lastEnd, pos).replace(/<\/a>/g, "") + (r[2] || ""));
          } else { resultArray.push(r[0]); }
          lastEnd = pos + fl;
        }
        let indexes;
        if (resultArray.length > 0) {
          indexes = resultArray.map(v => parseInt(v.match(/[0-9]+(?=>)/g)?.[0])).filter(v => !isNaN(v));
          resultArray = resultArray.map(v => v.slice(v.indexOf(">") + 1));
        } else { resultArray = [result]; indexes = [0]; }
        resultArray = resultArray.map(v => Utils.unescapeHTML(v));
        if (dontSort) return resultArray;
        const final = [];
        for (const j in indexes) {
          if (final[indexes[j]]) final[indexes[j]] += " " + resultArray[j];
          else final[indexes[j]] = resultArray[j];
        }
        return final;
      },
      async translate(text, toLang) {
        const to = this._fixLang(toLang);
        await GoogleHelper_v2.findAuth();
        if (!GoogleHelper_v2.translateAuth) throw new Error('No auth');
        const r = await gmFetch({
          method: 'POST',
          url: 'https://translate-pa.googleapis.com/v1/translateHtml',
          headers: { 'Content-Type': 'application/json+protobuf', 'X-Goog-Api-Key': GoogleHelper_v2.translateAuth },
          data: JSON.stringify([[[text], "auto", to], "te"]),
        });
        if (r.status !== 200) throw new Error('v2 error: ' + r.status);
        const data = JSON.parse(r.responseText);
        if (data && data[0]) {
          const raw = Array.isArray(data[0]) ? data[0][0] : data[0];
          const parsed = this._transformResponse(raw, false);
          return parsed[0] || raw;
        }
        throw new Error('v2 empty');
      },
      async translateBatch(texts, toLang) {
        const to = this._fixLang(toLang);
        await GoogleHelper_v2.findAuth();
        if (!GoogleHelper_v2.translateAuth) throw new Error('No auth');
        const r = await gmFetch({
          method: 'POST',
          url: 'https://translate-pa.googleapis.com/v1/translateHtml',
          headers: { 'Content-Type': 'application/json+protobuf', 'X-Goog-Api-Key': GoogleHelper_v2.translateAuth },
          data: JSON.stringify([[texts, "auto", to], "te"]),
        });
        if (r.status !== 200) throw new Error('v2 batch error: ' + r.status);
        const data = JSON.parse(r.responseText);
        if (data && data[0] && Array.isArray(data[0])) {
          return data[0].map(item => {
            const p = this._transformResponse(item, false);
            return p[0] || item;
          });
        }
        if (data && data[0]) {
          const p = this._transformResponse(Array.isArray(data[0]) ? data[0][0] : data[0], false);
          return [p[0]];
        }
        throw new Error('v2 batch empty');
      }
    },

    google_legacy: {
      name: 'Google (Legacy)',
      langCode(l) { return l; },
      async translate(text, toLang) {
        const to = toLang;
        const tk = GoogleHelper.calcHash(text);
        const r = await gmFetch({
          method: 'GET',
          url: 'https://translate.googleapis.com/translate_a/single?client=webapp&sl=auto&tl=' + to + '&hl=' + to + '&dt=t&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=at&ie=UTF-8&oe=UTF-8&otf=1&ssel=0&tsel=0&kc=7&tk=' + tk + '&q=' + encodeURIComponent(text),
        });
        if (r.status !== 200) return await this._gtx(text, to);
        const data = JSON.parse(r.responseText);
        return data[0].filter(s => s && s[0]).map(s => s[0]).join('');
      },
      async _gtx(text, to) {
        const r = await gmFetch({ method: 'GET', url: 'https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=' + to + '&q=' + encodeURIComponent(text) });
        if (r.status !== 200) throw new Error('gtx error');
        const data = JSON.parse(r.responseText);
        return data[0].filter(s => s && s[0]).map(s => s[0]).join('');
      }
    },

    google: {
      name: 'Google (Auto)',
      async translate(text, toLang) {
        try { return await Engine.google_v2.translate(text, toLang); }
        catch (e) { return await Engine.google_legacy.translate(text, toLang); }
      },
      async translateBatch(texts, toLang) {
        try { return await Engine.google_v2.translateBatch(texts, toLang); }
        catch (e) {
          const res = [];
          for (const t of texts) {
            try { res.push(await Engine.google_legacy.translate(t, toLang)); }
            catch (_) { res.push(null); }
          }
          return res;
        }
      }
    },

    microsoft: {
      name: 'Microsoft',
      _token: null, _tokenTime: 0,
      async getToken() {
        if (this._token && Date.now() - this._tokenTime < 480000) return this._token;
        const r = await gmFetch({ method: 'GET', url: 'https://edge.microsoft.com/translate/auth' });
        if (r.status !== 200) throw new Error('MS auth');
        this._token = r.responseText; this._tokenTime = Date.now();
        return this._token;
      },
      langCode(l) {
        const m = { 'zh': 'zh-Hans', 'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant', 'no': 'nb', 'sr': 'sr-Cyrl', 'pt-PT': 'pt-pt', 'fr-CA': 'fr-ca' };
        return m[l] || l;
      },
      async translate(text, toLang) {
        const token = await this.getToken();
        const to = this.langCode(toLang);
        const r = await gmFetch({
          method: 'POST',
          url: 'https://api-edge.cognitive.microsofttranslator.com/translate?from=&to=' + to + '&api-version=3.0',
          headers: { 'authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
          data: JSON.stringify([{ Text: text }]),
        });
        if (r.status !== 200) throw new Error('MS error');
        return JSON.parse(r.responseText)[0].translations[0].text;
      },
      async translateBatch(texts, toLang) {
        const token = await this.getToken();
        const to = this.langCode(toLang);
        const results = [];
        for (let b = 0; b < texts.length; b += 25) {
          const chunk = texts.slice(b, b + 25);
          const r = await gmFetch({
            method: 'POST',
            url: 'https://api-edge.cognitive.microsofttranslator.com/translate?from=&to=' + to + '&api-version=3.0',
            headers: { 'authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
            data: JSON.stringify(chunk.map(t => ({ Text: t }))),
          });
          if (r.status === 200) {
            for (const item of JSON.parse(r.responseText)) results.push(item.translations[0].text);
          } else { for (let i = 0; i < chunk.length; i++) results.push(null); }
        }
        return results;
      }
    },

    tencent: {
      name: 'Tencent',
      _clientKey: null,
      getClientKey() {
        if (this._clientKey) return this._clientKey;
        this._clientKey = 'browser-chrome-120.0-Windows_10-' + crypto.randomUUID() + '-' + Date.now();
        return this._clientKey;
      },
      langCode(l) { const m = { 'zh': 'zh', 'zh-CN': 'zh', 'zh-TW': 'zh-TW' }; return m[l] || l; },
      async translate(text, toLang) {
        const to = this.langCode(toLang);
        const r = await gmFetch({
          method: 'POST', url: 'https://transmart.qq.com/api/imt',
          headers: { 'Content-Type': 'application/json' },
          data: JSON.stringify({ header: { fn: 'auto_translation', session: '', client_key: this.getClientKey(), user: '' }, type: 'plain', model_category: 'normal', text_domain: 'general', source: { lang: 'auto', text_list: [text] }, target: { lang: to } }),
        });
        if (r.status !== 200) throw new Error('Tencent error');
        return JSON.parse(r.responseText).auto_translation[0];
      },
      async translateBatch(texts, toLang) {
        const to = this.langCode(toLang);
        const r = await gmFetch({
          method: 'POST', url: 'https://transmart.qq.com/api/imt',
          headers: { 'Content-Type': 'application/json' },
          data: JSON.stringify({ header: { fn: 'auto_translation', session: '', client_key: this.getClientKey(), user: '' }, type: 'plain', model_category: 'normal', text_domain: 'general', source: { lang: 'auto', text_list: texts }, target: { lang: to } }),
        });
        if (r.status !== 200) throw new Error('Tencent batch error');
        return JSON.parse(r.responseText).auto_translation;
      }
    }
  };

  // ══════════════════════════════════════════════════════════
  // 缓存层
  // ══════════════════════════════════════════════════════════
  const cache = new Map();
  const MAX_CACHE = 3000;
  function cacheGet(t) { return cache.get(t); }
  function cacheSet(t, v) { if (cache.size >= MAX_CACHE) cache.delete(cache.keys().next().value); cache.set(t, v); }

  // ══════════════════════════════════════════════════════════
  // 翻译核心
  // ══════════════════════════════════════════════════════════
  async function translate(text) {
    if (!text || !text.trim()) return null;
    const trimmed = text.trim();
    if (/^\d+$/.test(trimmed)) return null;
    const cached = cacheGet(trimmed);
    if (cached) return cached;
    try {
      const result = await Engine[currentEngine].translate(trimmed, targetLang);
      if (result && result !== trimmed) { cacheSet(trimmed, result); return result; }
    } catch (e) {
      const fallbackEngine = currentEngine === 'google' ? 'microsoft' : 'google_legacy';
      try {
        const result = await Engine[fallbackEngine].translate(trimmed, targetLang);
        if (result && result !== trimmed) { cacheSet(trimmed, result); return result; }
      } catch (_) {}
    }
    return null;
  }

  // ══════════════════════════════════════════════════════════
  // 批量翻译
  // ══════════════════════════════════════════════════════════
  async function batchTranslate(texts) {
    const results = new Array(texts.length).fill(null);
    const uncached = [], uncachedIdx = [];
    for (let i = 0; i < texts.length; i++) {
      const t = texts[i].trim();
      if (!t || /^\d+$/.test(t)) continue;
      const c = cacheGet(t);
      if (c) { results[i] = c; continue; }
      uncached.push(t);
      uncachedIdx.push(i);
    }
    if (uncached.length === 0) return results;

    const engine = Engine[currentEngine];
    if (engine.translateBatch && uncached.length > 1) {
      try {
        const BATCH_SIZE = currentEngine === 'microsoft' ? 25 : 50;
        const chunks = [];
        for (let b = 0; b < uncached.length; b += BATCH_SIZE) {
          chunks.push({ texts: uncached.slice(b, b + BATCH_SIZE), idxs: uncachedIdx.slice(b, b + BATCH_SIZE) });
        }
        await Promise.all(chunks.map(async ({ texts: chunk, idxs }) => {
          try {
            const batchResults = await engine.translateBatch(chunk, targetLang);
            if (batchResults) {
              for (let j = 0; j < batchResults.length; j++) {
                if (batchResults[j] && batchResults[j] !== chunk[j]) {
                  cacheSet(chunk[j], batchResults[j]);
                  results[idxs[j]] = batchResults[j];
                }
              }
            }
          } catch (_) {}
        }));
        return results;
      } catch (e) { /* fallthrough */ }
    }

    const CONCURRENCY = 8;
    for (let i = 0; i < uncached.length; i += CONCURRENCY) {
      const batch = uncached.slice(i, i + CONCURRENCY);
      const batchIdx = uncachedIdx.slice(i, i + CONCURRENCY);
      await Promise.allSettled(batch.map(async (text, j) => {
        const r = await translate(text);
        if (r) results[batchIdx[j]] = r;
      }));
    }
    return results;
  }

  // ══════════════════════════════════════════════════════════
  // DOM遍历
  // ══════════════════════════════════════════════════════════
  const SKIP_TAGS = /^(script|style|code|pre|svg|math|noscript|iframe|canvas|video|audio|img|br|hr|input|select|option|textarea)$/i;
  const SKIP_CLASS = /translate-ui|notranslate|katex|mathjax/i;

  function shouldSkip(node) {
    if (!node) return true;
    if (node.nodeType === Node.ELEMENT_NODE) {
      if (SKIP_TAGS.test(node.tagName)) return true;
      if (SKIP_CLASS.test(node.className)) return true;
      if (node.isContentEditable) return true;
      if (node.dataset && node.dataset.translated) return true;
      if (node.classList && node.classList.contains('tu-bi')) return true;
    }
    return false;
  }

  const _langRegex = {};
  function getLangRegex(lang) {
    if (_langRegex[lang]) return _langRegex[lang];
    const patterns = {
      'zh': /^[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\s\d\p{P}]+$/u,
      'en': /^[a-zA-Z\s\d\p{P}]+$/u,
      'ja': /^[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff\s\d\p{P}]+$/u,
      'ko': /^[\uac00-\ud7af\u1100-\u11ff\s\d\p{P}]+$/u,
      'ar': /^[\u0600-\u06ff\u0750-\u077f\s\d\p{P}]+$/u,
      'th': /^[\u0e00-\u0e7f\s\d\p{P}]+$/u,
      'ru': /^[\u0400-\u04ff\s\d\p{P}]+$/u,
    };
    _langRegex[lang] = patterns[lang] || null;
    return _langRegex[lang];
  }

  function isTargetLang(text) {
    if (!text || !text.trim()) return true;
    const lang = targetLang.split('-')[0];
    const re = getLangRegex(lang);
    return re ? re.test(text.trim()) : false;
  }

  function collectTextNodes(root) {
    const nodes = [];
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
      acceptNode(node) {
        if (shouldSkip(node.parentElement)) return NodeFilter.FILTER_REJECT;
        const text = node.textContent.trim();
        if (!text || text.length < 2 || /^\d+$/.test(text)) return NodeFilter.FILTER_REJECT;
        if (isTargetLang(text)) return NodeFilter.FILTER_REJECT;
        if (node.parentElement?.dataset?.translated) return NodeFilter.FILTER_REJECT;
        return NodeFilter.FILTER_ACCEPT;
      }
    });
    while (walker.nextNode()) nodes.push(walker.currentNode);
    return nodes;
  }

  function collectPlaceholders(root) {
    return [...root.querySelectorAll('input[placeholder], textarea[placeholder]')]
      .filter(el => !el.dataset.translated && el.placeholder.trim() && !isTargetLang(el.placeholder));
  }

  // ══════════════════════════════════════════════════════════
  // 翻译执行
  // ══════════════════════════════════════════════════════════
  let isTranslating = false;
  let pendingRoot = null;

  async function translatePage(root) {
    if (isTranslating) {
      pendingRoot = root || document.body;
      return;
    }
    isTranslating = true;

    try {
      root = root || document.body;

      do {
        pendingRoot = null;
        const textNodes = collectTextNodes(root);
        const placeholders = collectPlaceholders(root);

        if (textNodes.length === 0 && placeholders.length === 0) break;

        const allTexts = [];
        const allMeta = [];

        for (let i = 0; i < textNodes.length; i++) {
          allTexts.push(textNodes[i].textContent.trim());
          allMeta.push({ type: 'text', node: textNodes[i] });
        }
        for (let i = 0; i < placeholders.length; i++) {
          allTexts.push(placeholders[i].placeholder.trim());
          allMeta.push({ type: 'ph', el: placeholders[i] });
        }

        const results = await batchTranslate(allTexts);

        for (let i = 0; i < allMeta.length; i++) {
          if (!results[i]) continue;
          const meta = allMeta[i];
          if (meta.type === 'text') {
            const parent = meta.node.parentElement;
            if (!parent) continue;
            if (!parent.dataset.originalText) parent.dataset.originalText = meta.node.textContent;
            parent.dataset.translated = '1';

            if (displayMode === 'bilingual') {
              const s = document.createElement('span');
              s.className = 'tu-bi';
              s.textContent = results[i];
              if (meta.node.nextSibling) {
                parent.insertBefore(s, meta.node.nextSibling);
              } else {
                parent.appendChild(s);
              }
            } else {
              meta.node.textContent = results[i];
            }
          } else {
            meta.el.dataset.originalPlaceholder = meta.el.placeholder;
            meta.el.placeholder = results[i];
            meta.el.dataset.translated = '1';
          }
        }

        if (pendingRoot) { root = pendingRoot; }
      } while (pendingRoot);

    } finally {
      isTranslating = false;
    }
  }

  function restorePage() {
    document.querySelectorAll('.tu-bi').forEach(el => el.remove());
    document.querySelectorAll('[data-translated]').forEach(el => {
      if (el.dataset.originalText) {
        for (const child of el.childNodes) {
          if (child.nodeType === Node.TEXT_NODE) { child.textContent = el.dataset.originalText; break; }
        }
        delete el.dataset.originalText;
      }
      if (el.dataset.originalPlaceholder) {
        el.placeholder = el.dataset.originalPlaceholder;
        delete el.dataset.originalPlaceholder;
      }
      delete el.dataset.translated;
    });
  }

  // ══════════════════════════════════════════════════════════
  // 动态内容监听
  // ══════════════════════════════════════════════════════════
  let lastHeight = 0;

  function onScroll() {
    const h = document.documentElement.scrollHeight;
    if (h > lastHeight) {
      lastHeight = h;
      if (autoMode) translatePage();
    }
  }

  let mutationRafId = null;
  const pendingMutationRoots = new Set();

  const observer = new MutationObserver((mutations) => {
    if (!autoMode) return;
    for (const m of mutations) {
      for (const node of m.addedNodes) {
        if (node.nodeType === Node.ELEMENT_NODE && !shouldSkip(node)) {
          pendingMutationRoots.add(node);
        }
      }
    }
    if (pendingMutationRoots.size > 0 && !mutationRafId) {
      mutationRafId = setTimeout(() => {
        mutationRafId = null;
        const roots = [...pendingMutationRoots];
        pendingMutationRoots.clear();
        if (roots.length > 5) {
          translatePage(document.body);
        } else {
          roots.forEach(r => translatePage(r));
        }
      }, 200);
    }
  });

  // ══════════════════════════════════════════════════════════
  // UI - MD3 风格 & 高性能无延迟悬浮球吸附/拖拽逻辑 & 极致居中
  // ══════════════════════════════════════════════════════════

  function buildLangOptions() {
    let html = '';
    for (const [group, codes] of Object.entries(LANG_GROUPS)) {
      html += '';
      for (const code of codes) {
        const name = ALL_LANGUAGES[code] || code;
        html += '';
      }
      html += '';
    }
    return html;
  }

  function isPageInTargetLang() {
    const lang = (document.documentElement.lang || '').split('-')[0].toLowerCase();
    const target = targetLang.split('-')[0].toLowerCase();
    return lang === target;
  }

  function initWhenBodyReady() {
    if (document.body) {
      init();
    } else {
      requestAnimationFrame(initWhenBodyReady);
    }
  }

  // 异步 UI 宏任务渲染分离器:杜绝点击按钮时的卡顿、割裂感
  function runAsyncUI(task) {
    requestAnimationFrame(() => setTimeout(task, 10));
  }

  let _initialized = false;
  async function init() {
    if (_initialized) return;
    _initialized = true;

    lastHeight = document.documentElement.scrollHeight;

    // 注入CSS: 全局新增完美的 Flexbox 居中逻辑,不影响性能
    GM_addStyle(`
      .translate-ui { position: fixed; z-index: 2147483647; font-family: Roboto, system-ui, -apple-system, sans-serif; will-change: left, top; }
      .translate-ui * { box-sizing: border-box; margin: 0; padding: 0; border-radius: 0 !important; user-select: none; }
      
      .tu-btn {
        width: 48px; height: 48px; border: none;
        background: #0B57D0; color: #fff; cursor: pointer;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 4px 8px 3px rgba(0,0,0,0.15);
        transition: background 0.2s, transform 0.2s;
        touch-action: none;
      }
      .tu-btn svg { display: block; margin: auto; }
      .tu-btn.active { background: #1f1f1f; }
      .tu-btn.dragging { transform: scale(1.1); box-shadow: 0 8px 12px 6px rgba(0,0,0,0.25); }
      
      .tu-panel {
        position: absolute; width: 280px; max-height: 80vh; overflow-y: auto;
        background: #F3F4F6; box-shadow: 0 8px 12px 6px rgba(0,0,0,0.15), 0 4px 4px rgba(0,0,0,0.3);
        padding: 16px; display: none; color: #1f1f1f; font-size: 14px; text-align: center;
      }
      .tu-panel.show { display: block; }
      
      .tu-panel label { display: block; margin: 12px 0 6px; font-size: 12px; color: #444746; font-weight: 500; text-align: center; }
      .tu-panel select {
        width: 100%; padding: 10px; border: 1px solid #747775;
        background: #fff; color: #1f1f1f; outline: none; font-size: 14px;
        text-align: center; text-align-last: center;
      }
      .tu-panel select:focus { border: 2px solid #0B57D0; padding: 9px; }
      
      .tu-modes { display: flex; gap: 4px; margin-top: 8px; }
      .tu-modes button {
        flex: 1; padding: 10px 0; border: 1px solid #747775; background: transparent;
        color: #444746; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s;
        display: flex; align-items: center; justify-content: center; text-align: center;
      }
      .tu-modes button.on { background: #D3E3FD; color: #041E49; border-color: #0B57D0; }
      
      .tu-status { margin-top: 12px; padding: 8px; background: #E0E2E5; font-size: 12px; color: #444746; font-weight: 500; display: flex; align-items: center; justify-content: center; text-align: center; }
      
      .tu-row { display: flex; gap: 8px; margin-top: 12px; }
      .tu-row button { flex: 1; padding: 10px 0; border: none; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.2s; display: flex; align-items: center; justify-content: center; text-align: center; }
      .tu-row .tu-restore { background: #E0E2E5; color: #1f1f1f; }
      .tu-row .tu-restore:active { background: #C4C7C5; }
      .tu-row .tu-go { background: #0B57D0; color: #fff; }
      .tu-row .tu-go:active { background: #0842A0; }
      .tu-row .tu-exclude { background: #B3261E; color: #fff; font-size: 12px; }
      .tu-row .tu-exclude:active { background: #8C1D18; }
      
      .tu-bi { display: block; margin-top: 2px; font-size: .9em; line-height: 1.5; color: #0B57D0; border-left: 2px solid rgba(11,87,208,0.3); padding-left: 8px; }
      a .tu-bi, span .tu-bi, em .tu-bi, strong .tu-bi, b .tu-bi, i .tu-bi, label .tu-bi, small .tu-bi, sub .tu-bi, sup .tu-bi, u .tu-bi {
        display: inline; border-left: none; padding-left: 0; margin-top: 0; margin-left: 4px; font-size: .88em;
      }
      a .tu-bi::before, span .tu-bi::before, em .tu-bi::before, strong .tu-bi::before, b .tu-bi::before, i .tu-bi::before, label .tu-bi::before, small .tu-bi::before, sub .tu-bi::before, sup .tu-bi::before, u .tu-bi::before { content: "("; color: #747775; }
      a .tu-bi::after, span .tu-bi::after, em .tu-bi::after, strong .tu-bi::after, b .tu-bi::after, i .tu-bi::after, label .tu-bi::after, small .tu-bi::after, sub .tu-bi::after, sup .tu-bi::after, u .tu-bi::after { content: ")"; color: #747775; }
      
      @media(prefers-color-scheme:dark){
        .tu-btn { background: #A8C7FA; color: #052D70; }
        .tu-btn.active { background: #E2E2E2; color: #1f1f1f; }
        .tu-panel { background: #1E1F20; color: #E3E3E3; box-shadow: 0 8px 12px 6px rgba(0,0,0,0.5); }
        .tu-panel label { color: #C4C7C5; }
        .tu-panel select { background: #282A2C; color: #E3E3E3; border-color: #8E918F; }
        .tu-panel select:focus { border-color: #A8C7FA; }
        .tu-modes button { border-color: #8E918F; color: #C4C7C5; }
        .tu-modes button.on { background: #004A77; color: #C2E7FF; border-color: #A8C7FA; }
        .tu-status { background: #282A2C; color: #C4C7C5; }
        .tu-row .tu-restore { background: #282A2C; color: #E3E3E3; }
        .tu-row .tu-restore:active { background: #444746; }
        .tu-row .tu-go { background: #A8C7FA; color: #052D70; }
        .tu-row .tu-go:active { background: #7CB0F9; }
        .tu-row .tu-exclude { background: #F2B8B5; color: #601410; }
        .tu-row .tu-exclude:active { background: #F9DEDC; color: #410E0B; }
        .tu-bi { color: #A8C7FA; border-left-color: rgba(168,199,250,0.3); }
        a .tu-bi::before, span .tu-bi::before, em .tu-bi::before, strong .tu-bi::before, b .tu-bi::before, i .tu-bi::before, a .tu-bi::after, span .tu-bi::after, em .tu-bi::after, strong .tu-bi::after, b .tu-bi::after, i .tu-bi::after { color: #8E918F; }
      }
    `);

    // 构建容器
    const ui = document.createElement('div');
    ui.className = 'translate-ui';
    
    // 初始化位置 (默认靠右下)
    let initX = _savedX >= 0 ? _savedX : window.innerWidth - 48;
    let initY = _savedY >= 0 ? _savedY : window.innerHeight * 0.75;
    if (initX > window.innerWidth - 48) initX = window.innerWidth - 48;
    if (initY > window.innerHeight - 48) initY = window.innerHeight - 48;
    
    ui.style.left = initX + 'px';
    ui.style.top = initY + 'px';

    ui.innerHTML =
      '
' + '' + '' + '' + '' + '' + '
' + '' + '' + '' + '
' + '
Ready
' + '
' + '' + '' + '
' + '
' + '' + '
' + '
' + ''; document.body.appendChild(ui); const btn = document.getElementById('tuBtn'); const panel = document.getElementById('tuPanel'); const engineSelect = document.getElementById('tuEngine'); const langSelect = document.getElementById('tuLang'); const statusEl = document.getElementById('tuStatus'); const modesEl = document.getElementById('tuModes'); engineSelect.value = currentEngine; langSelect.value = targetLang; function setStatus(msg) { if (statusEl) statusEl.textContent = msg; } // 面板智能朝向弹出逻辑 function updatePanelPosition() { const rect = btn.getBoundingClientRect(); if (rect.left < window.innerWidth / 2) { panel.style.left = '0px'; panel.style.right = 'auto'; } else { panel.style.right = '0px'; panel.style.left = 'auto'; } if (rect.top < window.innerHeight / 2) { panel.style.top = '56px'; panel.style.bottom = 'auto'; } else { panel.style.bottom = '56px'; panel.style.top = 'auto'; } } // 重构极速无缝拖拽逻辑 let isDragging = false; let hasMoved = false; let startX = 0, startY = 0; let initialLeft = 0, initialTop = 0; let moveRafId = null; function onPointerDown(e) { if (e.button !== 0 && e.pointerType === 'mouse') return; if (e.target.closest('.tu-panel')) return; e.preventDefault(); startX = e.clientX; startY = e.clientY; const rect = ui.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; isDragging = false; hasMoved = false; document.addEventListener('pointermove', onPointerMove, { passive: false }); document.addEventListener('pointerup', onPointerUp); document.addEventListener('pointercancel', onPointerUp); } function onPointerMove(e) { e.preventDefault(); const dx = e.clientX - startX; const dy = e.clientY - startY; if (!isDragging && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) { isDragging = true; hasMoved = true; btn.classList.add('dragging'); ui.style.transition = 'none'; } if (isDragging) { if (!moveRafId) { moveRafId = requestAnimationFrame(() => { ui.style.left = (initialLeft + dx) + 'px'; ui.style.top = (initialTop + dy) + 'px'; moveRafId = null; }); } } } function onPointerUp(e) { document.removeEventListener('pointermove', onPointerMove); document.removeEventListener('pointerup', onPointerUp); document.removeEventListener('pointercancel', onPointerUp); if (moveRafId) { cancelAnimationFrame(moveRafId); moveRafId = null; } if (isDragging) { isDragging = false; btn.classList.remove('dragging'); const rect = ui.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; let finalLeft = centerX < window.innerWidth / 2 ? 0 : window.innerWidth - rect.width; let finalTop = rect.top; if (finalTop < 0) finalTop = 0; if (finalTop > window.innerHeight - rect.height) finalTop = window.innerHeight - rect.height; ui.style.transition = 'left 0.3s cubic-bezier(0.2, 0, 0, 1), top 0.3s cubic-bezier(0.2, 0, 0, 1)'; ui.style.left = finalLeft + 'px'; ui.style.top = finalTop + 'px'; GM_setValue('tu_pos_x', finalLeft); GM_setValue('tu_pos_y', finalTop); setTimeout(updatePanelPosition, 300); } else if (!hasMoved) { if (e.target.closest('#tuBtn')) { // 优化点2:为面板初次弹出引入帧动画同步,防止掉帧导致的卡顿感 requestAnimationFrame(() => { updatePanelPosition(); panel.classList.toggle('show'); }); } } } btn.addEventListener('pointerdown', onPointerDown); document.addEventListener('pointerdown', function(e) { if (!ui.contains(e.target) && !isDragging) panel.classList.remove('show'); }); // 表单操作 engineSelect.addEventListener('change', function() { currentEngine = engineSelect.value; GM_setValue('engine', currentEngine); cache.clear(); setStatus('Engine: ' + (Engine[currentEngine] ? Engine[currentEngine].name : currentEngine)); }); langSelect.addEventListener('change', function() { targetLang = langSelect.value; GM_setValue('targetLang', targetLang); cache.clear(); setStatus('Target: ' + (ALL_LANGUAGES[targetLang] || targetLang)); }); // 模式切换:接入 UI 宏任务分离,实现 0 延迟秒切响应 modesEl.addEventListener('click', function(e) { var b = e.target.closest('button[data-m]'); if (!b) return; var m = b.dataset.m; if (m === displayMode) return; modesEl.querySelectorAll('button').forEach(function(x) { x.classList.remove('on'); }); b.classList.add('on'); displayMode = m; GM_setValue('displayMode', m); // 先立即更新UI状态 if (m === 'original') { btn.classList.remove('active'); setStatus('显示原文'); runAsyncUI(() => restorePage()); // 异步防阻塞 } else { btn.classList.add('active'); setStatus(m === 'bilingual' ? '双语翻译中...' : '翻译中...'); runAsyncUI(async () => { restorePage(); var start = performance.now(); await translatePage(); setStatus('完成 (' + ((performance.now() - start) / 1000).toFixed(1) + 's)'); }); } }); // 翻译按钮:接入 UI 宏任务分离,彻底消灭点击后的“假死”感 document.getElementById('tuGo').addEventListener('click', function() { panel.classList.remove('show'); btn.classList.add('active'); autoMode = true; GM_setValue('autoMode', true); setStatus('翻译中...'); runAsyncUI(async () => { restorePage(); cache.clear(); lastHeight = document.documentElement.scrollHeight; var start = performance.now(); await translatePage(); setStatus('完成 (' + ((performance.now() - start) / 1000).toFixed(1) + 's)'); }); }); // 还原按钮:同上,保证秒响应 document.getElementById('tuRestore').addEventListener('click', function() { panel.classList.remove('show'); btn.classList.remove('active'); autoMode = false; GM_setValue('autoMode', false); setStatus('已还原'); runAsyncUI(() => restorePage()); }); document.getElementById('tuExclude').addEventListener('click', function() { if (!excludedHosts.includes(location.host)) { excludedHosts.push(location.host); GM_setValue('excludedHosts', JSON.stringify(excludedHosts)); } restorePage(); ui.remove(); observer.disconnect(); window.removeEventListener('scroll', onScroll); }); // 快捷菜单 GM_registerMenuCommand('翻译当前页面', function() { document.getElementById('tuGo').click(); }); GM_registerMenuCommand('还原当前页面', function() { document.getElementById('tuRestore').click(); }); GM_registerMenuCommand('切换Google引擎', function() { currentEngine = 'google'; engineSelect.value = 'google'; GM_setValue('engine', 'google'); cache.clear(); }); GM_registerMenuCommand('切换Microsoft引擎', function() { currentEngine = 'microsoft'; engineSelect.value = 'microsoft'; GM_setValue('engine', 'microsoft'); cache.clear(); }); GM_registerMenuCommand('切换双语/仅译文', function() { var m = displayMode === 'bilingual' ? 'translated' : 'bilingual'; modesEl.querySelector('button[data-m="'+m+'"]').click(); }); window.addEventListener('scroll', onScroll, { passive: true }); observer.observe(document.body, { childList: true, subtree: true }); if (autoMode && !isPageInTargetLang() && displayMode !== 'original') { // 优化点1:初次页面加载时,将微任务(queueMicrotask)改为宏任务(setTimeout) // 延迟 200 毫秒,强制给浏览器留出渲染时间,彻底解决初次点击悬浮球被阻塞而点不开的 Bug setTimeout(async function() { setStatus(displayMode === 'bilingual' ? '双语翻译中...' : '自动翻译中...'); var start = performance.now(); await translatePage(); setStatus('完成 (' + ((performance.now() - start) / 1000).toFixed(1) + 's)'); }, 200); } } initWhenBodyReady(); })();