Greasy Fork

来自缓存

Greasy Fork is available in English.

X/Twitter & Discord 实时翻译插件

支持推特实时翻译,Discord 翻译,支持翻译字体大小颜色可调整。精简版,仅保留翻译功能。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         X/Twitter & Discord 实时翻译插件
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  支持推特实时翻译,Discord 翻译,支持翻译字体大小颜色可调整。精简版,仅保留翻译功能。
// @author       Antigravity
// @match        *://twitter.com/*
// @match        *://x.com/*
// @match        *://pro.x.com/*
// @match        *://discord.com/*
// @match        *://www.patreon.com/*
// @match        *://ko-fi.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      translate.googleapis.com
// ==/UserScript==

(function () {
    'use strict';
    console.log("🚀 翻译插件已启动...");

    const __SystemConfig = {
        decode: (str) => {
            try { return decodeURIComponent(escape(window.atob(str))); }
            catch (e) { return ""; }
        },
        params: {
            svc_trans: "aHR0cHM6Ly90cmFuc2xhdGUuZ29vZ2xlYXBpcy5jb20vdHJhbnNsYXRlX2Evc2luZ2xl"
        }
    };

    const Storage = {
        getConfig: () => ({
            transColor: '#00E676',
            transFontSize: '14px',
            floatTop: '60%',
            transMode: 'below', // 'below', 'hover', 'bilingual'
            ...JSON.parse(GM_getValue('ling_config_simple', '{}'))
        }),
        setConfig: (cfg) => {
            GM_setValue('ling_config_simple', JSON.stringify(cfg));
            updateStyles();
        }
    };

    function updateStyles() {
        const cfg = Storage.getConfig();
        const oldStyle = document.getElementById('ling-style');
        if (oldStyle) oldStyle.remove();
        const isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        const css = `
            .ling-trans-box { margin-top: 6px; padding: 8px 10px; background: #ffffff; border-left: 3px solid ${cfg.transColor}; border-radius: 4px; color: #000000; font-size: ${cfg.transFontSize}; line-height: 1.5; font-family: "Consolas", monospace; white-space: normal; }
            .ling-trans-label { display: block; margin-bottom: 4px; opacity: 0.6; font-size: 10px; line-height: 1.2; }
            .ling-trans-text { white-space: pre-wrap; overflow-wrap: anywhere; }
            .ling-trans-line { display: block; margin: 0 0 3px; }
            .ling-trans-line:last-child { margin-bottom: 0; }
            .ling-discord-box { margin-top: 4px; padding: 4px 8px; opacity: 0.9; background: rgba(255,255,255,0.9); border-left: 2px solid ${cfg.transColor}; color: #000000; }
            .ling-hover-tooltip {
                position: absolute;
                background: rgba(255,255,255,0.9);
                color: #000000,
                padding: 8px 12px;
                border-radius: 6px;
                font-size: ${cfg.transFontSize};
                line-height: 1.4;
                z-index: 2147483647;
                max-width: 300px;
                word-wrap: break-word;
                box-shadow: 0 4px 12px rgba(0,0,0,0.5);
                pointer-events: none;
                opacity: 0;
                transition: opacity 0.2s;
            }
            .ling-hover-tooltip.show { opacity: 1; }
            .ling-bilingual { position: relative; }
            .ling-bilingual .original { opacity: 0.6; text-decoration: line-through; }
            .ling-bilingual .translation { color: #000000; font-size: ${cfg.transFontSize}; }
            .ling-dashboard {
                position: fixed;
                top: 15%;
                right: ${isMobile ? '10px' : '20px'};
                background: #111;
                border: 1px solid #333;
                border-radius: 12px;
                padding: ${isMobile ? '10px' : '15px'};
                z-index: 2147483646;
                box-shadow: 0 10px 30px rgba(0,0,0,0.8);
                min-width: ${isMobile ? '180px' : '200px'};
                opacity: 0;
                visibility: hidden;
                transform: translateY(-10px);
                transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
            }
            .ling-dashboard.active {
                opacity: 1;
                visibility: visible;
                transform: translateY(0);
            }
            .ling-float-toggle {
                position: fixed;
                right: ${isMobile ? '5px' : '10px'};
                top: ${cfg.floatTop};
                width: ${isMobile ? '35px' : '45px'};
                height: ${isMobile ? '35px' : '45px'};
                border-radius: 50%;
                background: #000;
                border: 2px solid #00E676;
                color: #fff;
                display: flex;
                justify-content: center;
                align-items: center;
                cursor: ${isMobile ? 'pointer' : 'grab'};
                z-index: 2147483645;
                box-shadow: 0 4px 10px rgba(0,0,0,0.5);
                transition: transform 0.1s, opacity 0.2s;
                opacity: 0.8;
                user-select: none;
                font-size: ${isMobile ? '10px' : '14px'};
            }
            .ling-float-toggle:hover { opacity: 1; transform: scale(1.05); }
            .ling-logo-text { font-family: 'Arial Black', sans-serif; font-weight: 900; font-size: ${isMobile ? '12px' : '14px'}; }
            #ling-settings-overlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0,0,0,0.8);
                z-index: 2147483647;
                display: flex;
                justify-content: center;
                align-items: center;
            }
            #ling-settings-box {
                background: #16181c;
                border: 1px solid #333;
                border-radius: 12px;
                padding: ${isMobile ? '15px' : '20px'};
                width: ${isMobile ? '90%' : '300px'};
                max-width: 400px;
                color: #fff;
            }
            .ling-row { margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; }
            .ling-row label { flex: 1; margin-bottom: ${isMobile ? '5px' : '0'}; }
            .ling-row input { flex: 1; max-width: 100px; }
            .ling-btn {
                background: #00E676;
                color: #000;
                border: none;
                padding: ${isMobile ? '10px' : '8px'};
                border-radius: 5px;
                width: 100%;
                font-weight: bold;
                cursor: pointer;
                margin-top: 10px;
                font-size: ${isMobile ? '14px' : '16px'};
            }
            @media (max-width: 768px) {
                .ling-dashboard { right: 10px; min-width: 180px; padding: 10px; }
                .ling-float-toggle { right: 5px; width: 40px; height: 40px; }
                .ling-hover-tooltip { max-width: 250px; font-size: 12px; }
            }
        `;
        const node = document.createElement('style');
        node.id = 'ling-style';
        node.innerHTML = css;
        document.head.appendChild(node);
    }

    function createProxyRequest(options) {
        return GM_xmlhttpRequest(options);
    }

    function getTranslateApiBase() {
        let apiBase = "https://translate.googleapis.com/translate_a/single";
        try { apiBase = __SystemConfig.decode(__SystemConfig.params.svc_trans) || apiBase; } catch (e) { }
        return apiBase;
    }

    function readTranslateResponse(responseText) {
        const data = JSON.parse(responseText);
        const parts = [];
        if (data && data[0]) {
            data[0].forEach(i => {
                if (i[0]) parts.push(i[0]);
            });
        }
        return parts.join('');
    }

    function translateText(text, callback) {
        const url = `${getTranslateApiBase()}?client=gtx&sl=auto&tl=zh-CN&dt=t&q=${encodeURIComponent(text)}`;
        createProxyRequest({
            method: "GET",
            url: url,
            timeout: 5000,
            onload: (res) => {
                try {
                    callback(readTranslateResponse(res.responseText));
                } catch (e) {
                    callback('');
                }
            },
            onerror: () => callback(''),
            ontimeout: () => callback('')
        });
    }

    function splitTranslatableLines(text) {
        return (text || '')
            .split(/\n+/)
            .map(line => line.trim())
            .filter(Boolean);
    }

    function processContent(element, text, platform) {
        if (!text || element.dataset.lingProcessed) return;
        element.dataset.lingProcessed = "true";

        const isForeign = !/[\u4e00-\u9fa5]/.test(text) || (text.match(/[\u4e00-\u9fa5]/g) || []).length / text.length < 0.3;
        if (isForeign && text.length > 3) {
            const textShort = text.length > 2000 ? text.substring(0, 2000) : text;
            const sourceLines = splitTranslatableLines(textShort);

            if (platform === 'twitter' && sourceLines.length > 1) {
                const translatedLines = new Array(sourceLines.length);
                let doneCount = 0;

                sourceLines.forEach((line, index) => {
                    translateText(line, (translated) => {
                        translatedLines[index] = translated;
                        doneCount += 1;
                        if (doneCount === sourceLines.length) {
                            const visibleLines = translatedLines.filter(Boolean);
                            if (visibleLines.length) renderBox(element, visibleLines, platform);
                        }
                    });
                });
            } else {
                translateText(textShort, (transResult) => {
                    if (transResult) renderBox(element, transResult, platform);
                });
            }
        }
    }

    function appendTranslationContent(container, transText) {
        const label = document.createElement('span');
        label.className = 'ling-trans-label';
        label.textContent = '[翻译]';
        container.appendChild(label);

        const content = document.createElement('div');
        content.className = 'ling-trans-text';
        const lines = Array.isArray(transText) ? transText : String(transText || '').split(/\n+/);

        lines.forEach((line) => {
            const lineNode = document.createElement('span');
            lineNode.className = 'ling-trans-line';
            lineNode.textContent = line;
            content.appendChild(lineNode);
        });

        container.appendChild(content);
    }

   function renderBox(element, transText, platform) {
        const cfg = Storage.getConfig();
        const isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

        // 获取元素的背景色和字体
        const computedStyle = window.getComputedStyle(element);
        const bgColor = computedStyle.backgroundColor || 'rgba(0,0,0,0.8)';
        const fontFamily = computedStyle.fontFamily || 'inherit';
        const fontSize = computedStyle.fontSize || cfg.transFontSize;
        const lineHeight = computedStyle.lineHeight || '1.5';

        if (cfg.transMode === 'hover' && !isMobile) {
            // 悬浮模式:鼠标悬停显示翻译
            const tooltip = document.createElement('div');
            tooltip.className = 'ling-hover-tooltip';
            tooltip.textContent = Array.isArray(transText) ? transText.join('\n') : transText;
            tooltip.style.backgroundColor = 'rgba(255,255,255,0.9)';
            tooltip.style.fontFamily = fontFamily;
            tooltip.style.fontSize = fontSize;
            tooltip.style.lineHeight = lineHeight;
            element.style.position = 'relative';
            element.appendChild(tooltip);

            element.addEventListener('mouseenter', () => {
                tooltip.classList.add('show');
            });
            element.addEventListener('mouseleave', () => {
                tooltip.classList.remove('show');
            });
        } else if (cfg.transMode === 'bilingual') {
            // 双语模式:原文和译文交替显示
            element.classList.add('ling-bilingual');
            const original = document.createElement('span');
            original.className = 'original';
            original.textContent = element.textContent;
            const translation = document.createElement('div');
            translation.className = 'translation';
            translation.textContent = Array.isArray(transText) ? transText.join('\n') : transText;
            translation.style.whiteSpace = 'pre-wrap';
            translation.style.fontFamily = fontFamily;
            translation.style.fontSize = fontSize;
            translation.style.lineHeight = lineHeight;
            element.innerHTML = '';
            element.appendChild(original);
            element.appendChild(translation);
        } else {
            // 默认下方显示模式
            const container = document.createElement('div');
            container.className = (platform === 'discord' || platform === 'patreon' || platform === 'kofi') ? 'ling-trans-box ling-discord-box' : 'ling-trans-box';
            appendTranslationContent(container, transText);
            container.style.backgroundColor = 'rgba(255,255,255,0.9)';
            container.style.fontFamily = fontFamily;
            container.style.fontSize = fontSize;
            container.style.lineHeight = lineHeight;

            const isXMessage = element.getAttribute('data-testid')?.startsWith('message-text-');
            const isKofiComment = platform === 'kofi' && element.classList.contains('kfds-top-mrgn-8');

            if (((platform === 'twitter' && !isXMessage) || platform === 'patreon' || platform === 'kofi') && !isKofiComment) {
                element.insertAdjacentElement('afterend', container);
            } else {
                element.appendChild(container);
            }
        }
    }

    function toggleDashboard() {
        let dashboard = document.querySelector('.ling-dashboard');
        if (!dashboard) {
            initDashboard();
            dashboard = document.querySelector('.ling-dashboard');
        }
        if (dashboard.classList.contains('active')) {
            dashboard.classList.remove('active');
            setTimeout(() => { dashboard.style.display = 'none'; }, 200);
        } else {
            dashboard.style.display = 'block';
            void dashboard.offsetWidth;
            setTimeout(() => { dashboard.classList.add('active'); }, 10);
        }
    }

    function initDashboard() {
        const cfg = Storage.getConfig();
        const modeText = cfg.transMode === 'below' ? '下方显示' : cfg.transMode === 'hover' ? '悬浮显示' : '双语模式';
        const div = document.createElement('div');
        div.className = 'ling-dashboard';
        div.innerHTML = `
            <div style="color:#00E676;font-weight:bold;margin-bottom:10px;display:flex;justify-content:space-between;border-bottom:1px solid #333;padding-bottom:5px;">
                <span>翻译助手精简版</span><span style="cursor:pointer;" id="ling-close-dash">✖</span>
            </div>
            <div style="margin-bottom:10px;font-size:12px;color:#ccc;">当前模式: ${modeText}</div>
            <button class="ling-btn" id="ling-btn-set">⚙️ 翻译设置</button>
            <div style="margin-top:10px;font-size:10px;color:#666;text-align:center;">仅保留 X & Discord 翻译功能</div>
        `;
        document.body.appendChild(div);
        document.getElementById('ling-close-dash').onclick = toggleDashboard;
        document.getElementById('ling-btn-set').onclick = openSettings;
    }

    function openSettings() {
        const cfg = Storage.getConfig();
        const div = document.createElement('div');
        div.id = 'ling-settings-overlay';
        div.innerHTML = `
            <div id="ling-settings-box">
                <h3 style="margin-top:0;color:#00E676;">⚙️ 翻译设置</h3>
                <div class="ling-row"><label>翻译颜色</label><input type="color" id="c-tc" value="${cfg.transColor}"></div>
                <div class="ling-row"><label>翻译字号</label><input type="text" id="c-ts" value="${cfg.transFontSize}" style="width:60px;background:#222;border:1px solid #444;color:#fff;"></div>
                <div class="ling-row"><label>翻译模式</label>
                    <select id="c-tm" style="background:#222;border:1px solid #444;color:#fff;">
                        <option value="below" ${cfg.transMode === 'below' ? 'selected' : ''}>下方显示</option>
                        <option value="hover" ${cfg.transMode === 'hover' ? 'selected' : ''}>悬浮显示</option>
                        <option value="bilingual" ${cfg.transMode === 'bilingual' ? 'selected' : ''}>双语模式</option>
                    </select>
                </div>
                <button class="ling-btn" id="ling-save">保存</button>
                <button class="ling-btn" id="ling-close" style="background:#333;color:#fff;margin-top:10px">关闭</button>
            </div>
        `;
        document.body.appendChild(div);
        document.getElementById('ling-close').onclick = () => div.remove();
        document.getElementById('ling-save').onclick = () => {
            Storage.setConfig({
                transColor: document.getElementById('c-tc').value,
                transFontSize: document.getElementById('c-ts').value,
                transMode: document.getElementById('c-tm').value,
                floatTop: cfg.floatTop
            });
            div.remove();
        };
    }

    function createFloatingToggle() {
        if (document.querySelector('.ling-float-toggle')) return;
        const div = document.createElement('div');
        div.className = 'ling-float-toggle';
        div.innerHTML = `<span class="ling-logo-text">Tran</span>`;
        div.onclick = toggleDashboard;

        const isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        let isDragging = false;
        let startY, startTop;

        if (isMobile) {
            // 移动设备使用触摸事件
            div.addEventListener('touchstart', (e) => {
                isDragging = false;
                const touch = e.touches[0];
                startY = touch.clientY;
                startTop = div.offsetTop;
            });

            div.addEventListener('touchmove', (e) => {
                const touch = e.touches[0];
                if (Math.abs(touch.clientY - startY) > 10) isDragging = true;
                if (isDragging) {
                    e.preventDefault();
                    let newTop = startTop + (touch.clientY - startY);
                    div.style.top = Math.max(10, Math.min(window.innerHeight - 50, newTop)) + 'px';
                }
            });

            div.addEventListener('touchend', () => {
                if (isDragging) {
                    const cfg = Storage.getConfig();
                    cfg.floatTop = div.style.top;
                    Storage.setConfig(cfg);
                }
            });
        } else {
            // 桌面设备使用鼠标事件
            div.onmousedown = (e) => {
                isDragging = false;
                startY = e.clientY;
                startTop = div.offsetTop;
                document.onmousemove = (ev) => {
                    if (Math.abs(ev.clientY - startY) > 3) isDragging = true;
                    if (isDragging) {
                        let newTop = startTop + (ev.clientY - startY);
                        div.style.top = Math.max(10, Math.min(window.innerHeight - 50, newTop)) + 'px';
                    }
                };
                document.onmouseup = () => {
                    document.onmousemove = null;
                    document.onmouseup = null;
                    if (isDragging) {
                        const cfg = Storage.getConfig();
                        cfg.floatTop = div.style.top;
                        Storage.setConfig(cfg);
                    }
                };
            };
        }
        document.body.appendChild(div);
    }

    function getTextCompact(el) {
        return (el?.innerText || el?.textContent || '').replace(/\s+/g, '').trim();
    }

    function isSortMenu(menu) {
        const txt = getTextCompact(menu);
        return txt.includes('排序方式') || txt.includes('Sortby') || txt.includes('Sortorder');
    }

    let twitterLatestSortLocked = false;

    function isMenuItemSelected(menuItem) {
        return menuItem.getAttribute('aria-checked') === 'true' || !!menuItem.querySelector('svg');
    }

    function enforceTwitterLatestSort(root = document) {
        let applied = false;
        const menus = root.querySelectorAll ? root.querySelectorAll('div[role="menu"]') : [];
        menus.forEach(menu => {
            if (!isSortMenu(menu) || menu.dataset.lingSortHandled === '1') return;

            const menuItems = menu.querySelectorAll('div[role="menuitem"]');
            const latestItem = Array.from(menuItems).find(item => {
                const txt = getTextCompact(item);
                return txt.includes('最近') || txt.includes('Latest') || txt.includes('Recent');
            });

            if (!latestItem) return;
            if (!isMenuItemSelected(latestItem)) latestItem.click();
            menu.dataset.lingSortHandled = '1';
            applied = true;
        });

        if (applied) twitterLatestSortLocked = true;
        return applied;
    }

    function forceSearchLiveTimeline() {
        if (!/x\.com$|twitter\.com$/.test(window.location.host)) return false;
        if (!window.location.pathname.startsWith('/search')) return false;

        const params = new URLSearchParams(window.location.search);
        if (params.get('f') === 'live') return false;
        params.set('f', 'live');
        const next = `${window.location.pathname}?${params.toString()}${window.location.hash}`;
        window.location.replace(next);
        return true;
    }

    function findTwitterSortTrigger(root = document) {
        const selector = 'button[aria-haspopup="menu"], div[role="button"][aria-haspopup="menu"]';
        const candidates = root.querySelectorAll ? Array.from(root.querySelectorAll(selector)) : [];
        for (const item of candidates) {
            const attrs = `${item.getAttribute('aria-label') || ''} ${item.getAttribute('title') || ''} ${getTextCompact(item)}`.toLowerCase();
            if (
                attrs.includes('sort') ||
                attrs.includes('order') ||
                attrs.includes('timeline') ||
                attrs.includes('排序') ||
                attrs.includes('选项') ||
                attrs.includes('选项')
            ) return item;
        }
        return null;
    }

    function scheduleTwitterLatestSort(maxRetry = 16, delay = 500) {
        let retry = 0;
        const runner = () => {
            if (twitterLatestSortLocked) return;
            if (enforceTwitterLatestSort(document)) return;

            const trigger = findTwitterSortTrigger(document);
            if (trigger) {
                trigger.click();
                setTimeout(() => enforceTwitterLatestSort(document), 120);
            }

            retry += 1;
            if (!twitterLatestSortLocked && retry < maxRetry) setTimeout(runner, delay);
        };
        runner();
    }

    function scan(node, siteType) {
        if (!node) return;
        if (node.nodeType !== 1 && node.nodeType !== 11) return; // Element or DocumentFragment (ShadowRoot)

        if (siteType === 'twitter') {
            const elements = node.querySelectorAll ? node.querySelectorAll('div[data-testid="tweetText"], div[data-testid^="message-text-"]') : [];
            elements.forEach(t => processContent(t, t.innerText, 'twitter'));
            enforceTwitterLatestSort(node);
        } else if (siteType === 'discord') {
            // 更新选择器以包括嵌入描述
            const elements = node.querySelectorAll ? node.querySelectorAll('div[id^="message-content"], div.embedDescription__623de') : [];
            elements.forEach(msg => processContent(msg, msg.innerText, 'discord'));
        } else if (siteType === 'patreon') {
            const elements = node.querySelectorAll ? node.querySelectorAll('[data-tag="post-title"], [data-tag="post-content"] p, .cm-gBCCZY p') : [];
            elements.forEach(i => processContent(i, i.innerText, 'patreon'));
        } else if (siteType === 'kofi') {
           // 原有内容:文章标题、正文
            // 新增内容:.kfds-top-mrgn-8 (评论区内容)
            const elements = node.querySelectorAll ? node.querySelectorAll('.caption-pdg, .post-story-text, .kfds-top-mrgn-8') : [];
            elements.forEach(i => {
                // 排除掉不是评论内容的通用边距容器(可选:检查其父级是否有评论特征)
                if (i.classList.contains('kfds-top-mrgn-8') && !i.closest('.kfds-lyt-column')) return;

                processContent(i, i.innerText, 'kofi');
            });

            // Shadow DOM 内容保持不变
            const hosts = node.querySelectorAll ? node.querySelectorAll('.article-host') : [];
            hosts.forEach(h => {
                if (h.shadowRoot && !h.dataset.lingObserved) {
                    h.dataset.lingObserved = "true";
                    scan(h.shadowRoot, 'kofi-shadow');
                    startObserver(h.shadowRoot, 'kofi-shadow');
                }
            });
        } else if (siteType === 'kofi-shadow') {
            const elements = node.querySelectorAll ? node.querySelectorAll('.fr-view p') : [];
            elements.forEach(p => processContent(p, p.innerText, 'kofi'));
        }
    }

    function startObserver(target, siteType) {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(m => {
                m.addedNodes.forEach(n => {
                    if (n.nodeType === 1) {
                        scan(n, siteType);
                        if (siteType === 'twitter') enforceTwitterLatestSort(n);
                        if (siteType === 'twitter' && (n.matches?.('div[data-testid="tweetText"]') || n.getAttribute?.('data-testid')?.startsWith('message-text-'))) {
                            processContent(n, n.innerText, 'twitter');
                        }
                    }
                });
            });
        });
        observer.observe(target, { childList: true, subtree: true });

        // X/Twitter 修复:监听“显示更多”展开长推文
        if (siteType === 'twitter') {
            document.addEventListener('click', (e) => {
                // 1. 扩大匹配范围:检查点击目标或其父级是否包含关键文本
                const btn = e.target.closest('button, a, [role="button"]');
                if (!btn) return;

                const targetText = (btn.innerText || btn.textContent || "").trim();
                const isShowMore = targetText.includes('显示更多') ||
                                 targetText.includes('Show more') ||
                                 targetText.includes('Show additional');

                if (isShowMore) {
                    // 2. 找到对应的推文容器
                    const article = btn.closest('article') || btn.closest('[data-testid="tweet"]');
                    if (article) {
                        const tweetText = article.querySelector('div[data-testid="tweetText"]');
                        if (tweetText) {
                            // 3. 强制重置状态
                            delete tweetText.dataset.lingProcessed;

                            // 移除现有的翻译框防止重复显示
                            const oldBoxes = article.querySelectorAll('.ling-trans-box');
                            oldBoxes.forEach(box => box.remove());

                            // 4. 适当延长延迟,确保 X 完成 DOM 展开渲染(800ms -> 1200ms)
                            setTimeout(() => {
                                // 重新抓取最新的全文内容
                                const newText = tweetText.innerText || tweetText.textContent;
                                processContent(tweetText, newText, 'twitter');
                            }, 1200);
                        }
                    }
                }
            }, true);
        }
    }

    function init() {
        updateStyles();
        createFloatingToggle();
        const host = window.location.host;
        let siteType = null;
        if (host.includes('twitter.com') || host.includes('x.com')) siteType = 'twitter';
        else if (host.includes('discord.com')) siteType = 'discord';
        else if (host.includes('patreon.com')) siteType = 'patreon';
        else if (host.includes('ko-fi.com')) siteType = 'kofi';

        if (siteType) {
            startObserver(document.body, siteType);
            scan(document.body, siteType);
            if (siteType === 'twitter') {
                if (forceSearchLiveTimeline()) return;
                scheduleTwitterLatestSort();
            }
        }
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();

    GM_registerMenuCommand("打开/关闭翻译设置", toggleDashboard);
})();