X Copy Tweet Link Helper

Copy tweet links via right-click, like button, or dedicated button. Supports Fixupx mode, allows toggling specific features directly in the Tampermonkey interface, and offers Chinese/English language switching.

Instalar este script¿?
Script recomendado por el autor

Puede que también te guste X - Optimized Tweet Buttons.

Instalar este script
// ==UserScript==
// @name         X Copy Tweet Link Helper
// @name:zh-TW   X 複製推文連結助手
// @name:zh-CN   X 复制推文连结助手
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Copy tweet links via right-click, like button, or dedicated button. Supports Fixupx mode, allows toggling specific features directly in the Tampermonkey interface, and offers Chinese/English language switching.
// @description:zh-TW 透過右鍵、喜歡或按鈕複製推文鏈接,並支援fixupx模式,可在油猴介面中直接開關指定功能,中英語言顯示切換。
// @description:zh-CN 透过右键、喜欢或按钮复制推文链接,并支援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,
        useFixupx: false,
        language: 'EN'
    };

    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',
            useFixupx: 'Use Fixupx',
            language: 'Language'
        },
        ZH: {
            copySuccess: "已複製鏈結!",
            copyButton: "🔗",
            rightClickCopy: '右鍵複製',
            likeCopy: '喜歡時複製',
            showCopyButton: '顯示複製按鈕',
            useFixupx: '使用 Fixupx',
            language: '語言'
        }
    };

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

    function cleanTweetUrl(rawUrl) {
        try {
            const url = new URL(rawUrl);
            url.search = '';
            url.pathname = url.pathname.replace(/\/photo\/\d+$/, '');
            if (settings.get('useFixupx')) {
                url.hostname = 'fixupx.com';
            }
            return url.toString();
        } catch {
            return rawUrl;
        }
    }

    function copyTweetLink(tweet) {
        const anchor = tweet.querySelector('a[href*="/status/"]');
        if (!anchor) return;
        const cleanUrl = cleanTweetUrl(anchor.href);
        navigator.clipboard.writeText(cleanUrl).then(() => {
            showToast(getText('copySuccess'));
        });
    }

    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';
        }, 1500);
    }

    function insertCopyButton(tweet) {
        if (tweet.querySelector('.my-copy-btn')) return;
        const actionGroup = tweet.querySelector('[role="group"]');
        if (!actionGroup) return;

        const btn = document.createElement('div');
        btn.className = 'my-copy-btn';
        btn.innerText = getText('copyButton');
        Object.assign(btn.style, {
            fontSize: '16px',
            cursor: 'pointer',
            userSelect: 'none',
            marginLeft: '8px',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
        });

        btn.onclick = (e) => {
            e.stopPropagation();
            copyTweetLink(tweet);
        };

        const buttonContainer = actionGroup.lastElementChild;
        if (buttonContainer && buttonContainer.parentElement) {
            const wrapper = document.createElement('div');
            wrapper.style.display = 'flex';
            wrapper.style.alignItems = 'center';
            wrapper.style.marginLeft = '8px';
            wrapper.appendChild(btn);
            buttonContainer.parentElement.appendChild(wrapper);
        } else {
            actionGroup.appendChild(btn); // fallback
        }
    }

    // 防止重複綁定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);
            }
        });
    }

    // 只處理新增的article節點
    function processTweetNode(node) {
        if (!(node instanceof HTMLElement)) return;
        if (node.tagName === 'ARTICLE') {
            if (settings.get('showCopyButton')) insertCopyButton(node);
            if (settings.get('rightClickCopy')) bindRightClickCopy(node);
            if (settings.get('likeCopy')) bindLikeCopy(node);
        } else {
            node.querySelectorAll && node.querySelectorAll('article').forEach(article => {
                if (settings.get('showCopyButton')) insertCopyButton(article);
                if (settings.get('rightClickCopy')) bindRightClickCopy(article);
                if (settings.get('likeCopy')) bindLikeCopy(article);
            });
        }
    }

    // 初始處理現有推文
    document.querySelectorAll('article').forEach(processTweetNode);

    // 只處理新增節點
    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('useFixupx')} ( ${settings.get('useFixupx') ? '✅' : '❌'} )`, toggleUseFixupx));
        // 支援多語言自動切換
        const langs = Object.keys(lang);
        const currentLangIdx = langs.indexOf(settings.get('language'));
        const nextLang = langs[(currentLangIdx + 1) % langs.length];
        // 這裡根據目前語言顯示「語言 中文」或「語言 EN」
        let langDisplay = settings.get('language');
        if (langDisplay === 'ZH') langDisplay = '中文';
        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 toggleUseFixupx() {
        settings.set('useFixupx', !settings.get('useFixupx'));
        reloadPage();
    }

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

    function reloadPage() {
        location.reload();
    }
})();