X/Twitter 复制推文连结助手

通过右键、喜欢或按钮复制推文链接,並支持fixupx模式和推文跳转开关,复制按钮的fixupx开关,可在油猴界面中直接开关指定功能,中英菜单显示切换。

安装此脚本
作者推荐脚本

您可能也喜欢X/Twitter 优化推文按钮

安装此脚本
// ==UserScript==
// @name         X/Twitter Copy Tweet Link Helper
// @name:zh-TW   X/Twitter 複製推文連結助手
// @name:zh-CN   X/Twitter 复制推文连结助手
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  Copy tweet links via right-click, like button, or dedicated button. Supports fixupx mode and tweet redirect toggle. fixupx format toggle for the copy button, Features can be enabled or disabled directly in the Tampermonkey interface, with a switchable Chinese/English menu display.
// @description:zh-TW 透過右鍵、喜歡或按鈕複製推文鏈接,並支援fixupx模式和推文跳轉開關,複製按鈕的fixupx開關,可在油猴介面中直接開關指定功能,中英菜單顯示切換。
// @description:zh-CN 通过右键、喜欢或按钮复制推文链接,並支持fixupx模式和推文跳转开关,复制按钮的fixupx开关,可在油猴界面中直接开关指定功能,中英菜单显示切换。
// @author       ChatGPT
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // === 預設設定值 ===
    const defaultSettings = {
        rightClickCopy: true,       // 右鍵複製推文連結
        likeCopy: true,             // 按讚時自動複製連結
        showCopyButton: true,       // 顯示🔗複製按鈕
        disableClickRedirect: true, // 禁止點擊推文跳轉
        forceFixupxOnButton: true,  // 複製按鈕固定使用 fixupx 格式
        useFixupx: false,           // 使用 fixupx.com 格式連結
        language: 'EN'              // 語言設定:EN 或 ZH
    };

    // === 設定操作介面 ===
    const settings = {
        get(key) {
            return GM_getValue(key, defaultSettings[key]);
        },
        set(key, value) {
            GM_setValue(key, value);
        }
    };

    // === 語系 ===
    const lang = {
        EN: {
            copySuccess: "Link copied!",
            copyButton: "🔗",
            rightClickCopy: 'Right-click Copy',
            likeCopy: 'Like Copy',
            showCopyButton: 'Show Copy Button',
            disableClickRedirect: 'Disable Tweet Click',
            forceFixupxOnButton: 'Force Fixupx on Copy Button',
            useFixupx: 'Use Fixupx',
            language: 'Language'
        },
        ZH: {
            copySuccess: "已複製鏈結!",
            copyButton: "🔗",
            rightClickCopy: '右鍵複製',
            likeCopy: '喜歡時複製',
            showCopyButton: '顯示複製按鈕',
            disableClickRedirect: '禁止點擊跳轉',
            forceFixupxOnButton: '複製按鈕固定 Fixupx 模式',
            useFixupx: '使用 Fixupx',
            language: '語言'
        }
    };

    const getText = (key) => lang[settings.get('language')][key];

    // === 清理推文網址,移除 photo 路徑與 query 參數 ===
    function cleanTweetUrl(rawUrl, forceFixupx = false) {
        try {
            const url = new URL(rawUrl);
            url.search = '';
            url.pathname = url.pathname.replace(/\/photo\/\d+$/, '');
            if (settings.get('useFixupx') || forceFixupx) {
                url.hostname = 'fixupx.com';
            }
            return url.toString();
        } catch {
            return rawUrl;
        }
    }

    // === 複製推文連結 ===
    function copyTweetLink(tweet, forceFixupx = false) {
        const anchor = tweet.querySelector('a[href*="/status/"]');
        if (!anchor) return;
        const cleanUrl = cleanTweetUrl(anchor.href, forceFixupx);
        navigator.clipboard.writeText(cleanUrl).then(() => {
        showToast(getText('copySuccess'));
    });
    }

    // === 顯示提示訊息(toast) ===
    let toastTimer = null;
    function showToast(msg) {
        let toast = document.getElementById('x-copy-tweet-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'x-copy-tweet-toast';
            Object.assign(toast.style, {
                position: 'fixed',
                bottom: '20px',
                left: '50%',
                transform: 'translateX(-50%)',
                background: '#1da1f2',
                color: '#fff',
                padding: '8px 16px',
                borderRadius: '20px',
                zIndex: 9999,
                fontSize: '14px',
                pointerEvents: 'none'
            });
            document.body.appendChild(toast);
        }
        toast.innerText = msg;
        toast.style.display = 'block';
        if (toastTimer) clearTimeout(toastTimer);
        toastTimer = setTimeout(() => {
            toast.style.display = 'none';
        }, 1000);
    }

    // === 插入🔗按鈕至推文中(精簡樣式)===
    function insertCopyButton(tweet) {
        if (tweet.querySelector('.x-copy-btn')) return;

        const actionGroup = tweet.querySelector('[role="group"]');
        if (!actionGroup) return;

        const actionButtons = Array.from(actionGroup.children);
        const bookmarkContainer = actionButtons[actionButtons.length - 2];
        if (!bookmarkContainer) return;

        const btnContainer = document.createElement('div');
        btnContainer.className = 'x-copy-btn-container';
        Object.assign(btnContainer.style, {
            display: 'flex',
            flexDirection: 'row',
            alignItems: 'center',
            minHeight: '20px',
            maxWidth: '100%',
            marginRight: '8px',
            flex: '1'
    });

        const innerDiv = document.createElement('div');
        Object.assign(innerDiv.style, {
            display: 'flex',
            flexDirection: 'row',
            alignItems: 'center',
            justifyContent: 'center',
            minHeight: '20px'
    });

        const btn = document.createElement('div');
        btn.className = 'x-copy-btn';
        Object.assign(btn.style, {
            cursor: 'pointer',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            minWidth: '20px',
            minHeight: '20px',
            borderRadius: '9999px'
        // 已移除多餘的 transition、color 等特效
    });

        const btnContent = document.createElement('div');
        Object.assign(btnContent.style, {
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            minWidth: '20px',
            minHeight: '20px'
    });

        const textSpan = document.createElement('span');
        textSpan.innerText = getText('copyButton');
        Object.assign(textSpan.style, {
            fontSize: '16px',
            lineHeight: '1'
    });

    // 僅保留必要的點擊事件,不再有 hover 效果
        btn.onclick = (e) => {
            e.stopPropagation();
            copyTweetLink(tweet);
            const forceFix = settings.get('forceFixupxOnButton');
            copyTweetLink(tweet, forceFix);
    };

        btnContent.appendChild(textSpan);
        btn.appendChild(btnContent);
        innerDiv.appendChild(btn);
        btnContainer.appendChild(innerDiv);
        actionGroup.insertBefore(btnContainer, bookmarkContainer);

    // 保留彈性對齊與寬度一致性
        const computedStyle = window.getComputedStyle(bookmarkContainer);
        btnContainer.style.flex = computedStyle.flex;
        btnContainer.style.justifyContent = computedStyle.justifyContent;
    }

    // === 綁定 Like 複製事件(避免重複) ===
    function bindLikeCopy(tweet) {
        if (tweet.hasAttribute('data-likecopy')) return;
        tweet.setAttribute('data-likecopy', 'true');
        const likeBtn = tweet.querySelector('[data-testid="like"]');
        if (likeBtn && !likeBtn.hasAttribute('data-likecopy-listener')) {
            likeBtn.setAttribute('data-likecopy-listener', 'true');
            likeBtn.addEventListener('click', () => {
                copyTweetLink(tweet);
            });
        }
    }

    // === 綁定右鍵複製事件(避免重複) ===
    function bindRightClickCopy(tweet) {
        if (tweet.hasAttribute('data-rightclick')) return;
        tweet.setAttribute('data-rightclick', 'true');
        tweet.addEventListener('contextmenu', (e) => {
            if (tweet.querySelector('img, video')) {
                copyTweetLink(tweet);
            }
        });
    }

    // === 禁止整篇推文點擊進入詳細頁(阻止點擊跳轉)===
    function disableTweetClickHandler(tweet) {
        if (tweet.hasAttribute('data-disableclick')) return;
        tweet.setAttribute('data-disableclick', 'true');

    // === 當禁止點擊時,改變滑鼠游標為預設樣式(非手掌)===
    tweet.style.cursor = 'default'; // 鼠標可自行改成text、auto、not-allowed、help、grab

        tweet.addEventListener('click', (e) => {
            const target = e.target;
            // 排除功能按鈕、外部連結、輸入框
            if (
                target.closest('[role="button"]') ||
                target.closest('a[href^="http"]') ||
                target.closest('input') ||
                target.closest('textarea') ||
                target.closest('.x-copy-btn') || // 排除複製按鈕
                target.closest('[data-testid="notification"]') || // 排除通知欄
                target.closest('[data-testid="tweetPhoto"]') || // 排除推特圖片
                target.closest('[data-testid="Tweet-User-Avatar"]') || // 排除用戶頭像
                target.closest('[data-testid="User-Name"]') || // 排除用戶名稱
                target.closest('[data-testid="socialContext"]') || // 排除轉推者名稱
                target.closest('a time') // 排除包含 <time> 的時間連結
            ) {
                return;
            }
            // 其他情況一律阻止跳轉
            e.stopPropagation();
            e.preventDefault();
        }, true);
    }

    // === 處理新增的推文節點 ===
    function processTweetNode(node) {
        if (!(node instanceof HTMLElement)) return;
        const applyTo = node.tagName === 'ARTICLE' ? [node] : node.querySelectorAll?.('article') || [];
        for (const tweet of applyTo) {
            if (settings.get('showCopyButton')) insertCopyButton(tweet);
            if (settings.get('rightClickCopy')) bindRightClickCopy(tweet);
            if (settings.get('likeCopy')) bindLikeCopy(tweet);
            if (settings.get('disableClickRedirect')) disableTweetClickHandler(tweet);
        }
    }

    // === 初始處理目前所有推文 ===
    document.querySelectorAll('article').forEach(processTweetNode);

    // === 監聽 DOM 變動,只處理新增推文 ===
    const tweetObserver = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            mutation.addedNodes.forEach(processTweetNode);
        }
    });
    tweetObserver.observe(document.body, { childList: true, subtree: true });

    // === MenuCommand 註冊與更新 ===
    let menuIds = [];
    function updateMenuCommands() {
        menuIds.forEach(id => {
            try { GM_unregisterMenuCommand(id); } catch {}
        });
        menuIds = [];
        menuIds.push(GM_registerMenuCommand(`${getText('rightClickCopy')} ( ${settings.get('rightClickCopy') ? '✅' : '❌'} )`, toggleRightClickCopy));
        menuIds.push(GM_registerMenuCommand(`${getText('likeCopy')} ( ${settings.get('likeCopy') ? '✅' : '❌'} )`, toggleLikeCopy));
        menuIds.push(GM_registerMenuCommand(`${getText('showCopyButton')} ( ${settings.get('showCopyButton') ? '✅' : '❌'} )`, toggleShowCopyButton));
        menuIds.push(GM_registerMenuCommand(`${getText('disableClickRedirect')} ( ${settings.get('disableClickRedirect') ? '✅' : '❌'} )`, toggleDisableClickRedirect));
        menuIds.push(GM_registerMenuCommand(`${getText('forceFixupxOnButton')} ( ${settings.get('forceFixupxOnButton') ? '✅' : '❌'} )`, toggleForceFixupxOnButton));
        menuIds.push(GM_registerMenuCommand(`${getText('useFixupx')} ( ${settings.get('useFixupx') ? '✅' : '❌'} )`, toggleUseFixupx));
        const langs = Object.keys(lang);
        const currentLangIdx = langs.indexOf(settings.get('language'));
        const nextLang = langs[(currentLangIdx + 1) % langs.length];
        let langDisplay = settings.get('language') === 'ZH' ? '中文' : 'EN';
        menuIds.push(GM_registerMenuCommand(`${getText('language')} ( ${langDisplay} )`, () => toggleLanguage(nextLang)));
    }

    updateMenuCommands();

    // === 設定切換函式 ===
    function toggleRightClickCopy() {
        settings.set('rightClickCopy', !settings.get('rightClickCopy'));
        reloadPage();
    }

    function toggleLikeCopy() {
        settings.set('likeCopy', !settings.get('likeCopy'));
        reloadPage();
    }

    function toggleShowCopyButton() {
        settings.set('showCopyButton', !settings.get('showCopyButton'));
        reloadPage();
    }

    function toggleDisableClickRedirect() {
        settings.set('disableClickRedirect', !settings.get('disableClickRedirect'));
        reloadPage();
    }

    function toggleForceFixupxOnButton() {
    settings.set('forceFixupxOnButton', !settings.get('forceFixupxOnButton'));
    reloadPage();
    }

    function toggleUseFixupx() {
        settings.set('useFixupx', !settings.get('useFixupx'));
        reloadPage();
    }

    function toggleLanguage(nextLang) {
        settings.set('language', nextLang);
        reloadPage();
    }

    // === 重新載入頁面(避免即時 DOM 綁定錯亂)===
    function reloadPage() {
        location.reload();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址