X 複製推文連結助手

透過右鍵、喜歡或按鈕複製推文鏈接,並支援fixvx模式,可在油猴介面中直接開關指定功能,中英語言顯示切換。

目前為 2025-04-12 提交的版本,檢視 最新版本

// ==UserScript==
// @name         X Copy Tweet Link Helper
// @name:zh-TW   X 複製推文連結助手
// @name:zh-CN   X 复制推文连结助手
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Copy tweet links via right-click, like button, or dedicated button. Supports Fixvx mode, allows toggling specific features directly in the Tampermonkey interface, and offers Chinese/English language switching.
// @description:zh-TW 透過右鍵、喜歡或按鈕複製推文鏈接,並支援fixvx模式,可在油猴介面中直接開關指定功能,中英語言顯示切換。
// @description:zh-CN 透过右键、喜欢或按钮复制推文链接,并支援fixvx模式,可在油猴介面中直接开关指定功能,中英语言显示切换。
// @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,
        useFixvx: 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',
            useFixvx: 'Use Fixvx',
            language: 'Language'
        },
        ZH: {
            copySuccess: "已複製鏈結!",
            copyButton: "🔗",
            rightClickCopy: '右鍵複製',
            likeCopy: '喜歡時複製',
            showCopyButton: '顯示複製按鈕',
            useFixvx: '使用 Fixvx',
            language: '語言'
        }
    };

    // 取出目前語系,可提升部分效能(重載前的快取)
    const currentLang = settings.get('language');
    const getText = (key) => lang[currentLang][key];

    function copyTweetLink(tweet) {
        const anchor = tweet.querySelector('a[href*="/status/"]');
        if (!anchor) return;
        let url = new URL(anchor.href);
        url.search = '';
        if (settings.get('useFixvx')) {
            url.hostname = 'fixvx.com';
        }
        navigator.clipboard.writeText(url.toString()).then(() => {
            showToast(getText('copySuccess'));
        });
    }

    function showToast(msg) {
        const toast = document.createElement('div');
        toast.innerText = msg;
        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'
        });
        document.body.appendChild(toast);
        setTimeout(() => toast.remove(), 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
        }
    }

    // 優化 MutationObserver:僅處理新增的節點
    const tweetObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeType !== Node.ELEMENT_NODE) return;
                let tweets = [];
                if (node.matches && node.matches('article')) {
                    tweets.push(node);
                } else {
                    tweets = Array.from(node.querySelectorAll('article'));
                }
                tweets.forEach((tweet) => {
                    if (settings.get('showCopyButton')) insertCopyButton(tweet);

                    if (settings.get('rightClickCopy') && !tweet.hasAttribute('data-rightclick')) {
                        tweet.setAttribute('data-rightclick', 'true');
                        tweet.addEventListener('contextmenu', (e) => {
                            if (tweet.querySelector('img, video')) {
                                copyTweetLink(tweet);
                            }
                        });
                    }

                    if (settings.get('likeCopy') && !tweet.hasAttribute('data-likecopy')) {
                        tweet.setAttribute('data-likecopy', 'true');
                        const likeBtn = tweet.querySelector('[data-testid="like"]');
                        if (likeBtn) {
                            likeBtn.addEventListener('click', () => {
                                copyTweetLink(tweet);
                            });
                        }
                    }
                });
            });
        });
    });

    tweetObserver.observe(document.body, { childList: true, subtree: true });

    // 簡化布林設定的切換
    function toggleBooleanSetting(key) {
        settings.set(key, !settings.get(key));
        reloadPage();
    }

    function updateMenuCommands() {
        GM_unregisterMenuCommand();
        GM_registerMenuCommand(
            `${getText('rightClickCopy')} ( ${settings.get('rightClickCopy') ? '✅' : '❌'} )`,
            () => toggleBooleanSetting('rightClickCopy')
        );
        GM_registerMenuCommand(
            `${getText('likeCopy')} ( ${settings.get('likeCopy') ? '✅' : '❌'} )`,
            () => toggleBooleanSetting('likeCopy')
        );
        GM_registerMenuCommand(
            `${getText('showCopyButton')} ( ${settings.get('showCopyButton') ? '✅' : '❌'} )`,
            () => toggleBooleanSetting('showCopyButton')
        );
        GM_registerMenuCommand(
            `${getText('useFixvx')} ( ${settings.get('useFixvx') ? '✅' : '❌'} )`,
            () => toggleBooleanSetting('useFixvx')
        );
        GM_registerMenuCommand(
            `${getText('language')} ( ${settings.get('language') === 'EN' ? 'EN' : 'ZH'} )`,
            toggleLanguage
        );
    }

    updateMenuCommands();

    function toggleLanguage() {
        const currentValue = settings.get('language');
        const newLang = currentValue === 'EN' ? 'ZH' : 'EN';
        settings.set('language', newLang);
        reloadPage();
    }

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