// ==UserScript== // @name X/Twitter Copy Tweet Link Helper // @name:zh-TW X/Twitter 複製推文連結助手 // @name:zh-CN X/Twitter 复制推文连结助手 // @namespace http://tampermonkey.net/ // @version 2.6 // @description Copy tweet links via right-click, like button, or dedicated button. Supports fixupx mode and tweet redirect toggle. Features can be enabled or disabled directly in the Tampermonkey interface, with a switchable Chinese/English menu display. // @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 // @downloadURL none // ==/UserScript== (function () { 'use strict'; // === 預設設定值 === const defaultSettings = { rightClickCopy: true, // 右鍵複製推文連結 likeCopy: true, // 按讚時自動複製連結 showCopyButton: true, // 顯示🔗複製按鈕 useFixupx: false, // 使用 fixupx.com 格式連結 disableClickRedirect: false,// 禁止點擊推文跳轉 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', useFixupx: 'Use Fixupx', disableClickRedirect: 'Disable Tweet Click', language: 'Language' }, ZH: { copySuccess: "已複製鏈結!", copyButton: "🔗", rightClickCopy: '右鍵複製', likeCopy: '喜歡時複製', showCopyButton: '顯示複製按鈕', useFixupx: '使用 Fixupx', disableClickRedirect: '禁止點擊跳轉', language: '語言' } }; const getText = (key) => lang[settings.get('language')][key]; // === 清理推文網址,移除 photo 路徑與 query 參數 === 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')); }); } // === 顯示提示訊息(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'; }, 1500); } // === 插入🔗按鈕至推文中 === 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', color: 'rgb(83, 100, 113)', transition: 'all 0.2s', borderRadius: '9999px' }); 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' }); btn.addEventListener('mouseover', () => { btn.style.backgroundColor = 'rgba(29, 155, 240, 0.1)'; btn.style.color = 'rgb(29, 155, 240)'; }); btn.addEventListener('mouseout', () => { btn.style.backgroundColor = 'transparent'; btn.style.color = 'rgb(83, 100, 113)'; }); btn.onclick = (e) => { e.stopPropagation(); copyTweetLink(tweet); }; 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.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') // 排除複製按鈕 ) { 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('useFixupx')} ( ${settings.get('useFixupx') ? '✅' : '❌'} )`, toggleUseFixupx)); menuIds.push(GM_registerMenuCommand(`${getText('disableClickRedirect')} ( ${settings.get('disableClickRedirect') ? '✅' : '❌'} )`, toggleDisableClickRedirect)); 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 toggleUseFixupx() { settings.set('useFixupx', !settings.get('useFixupx')); reloadPage(); } function toggleDisableClickRedirect() { settings.set('disableClickRedirect', !settings.get('disableClickRedirect')); reloadPage(); } function toggleLanguage(nextLang) { settings.set('language', nextLang); reloadPage(); } // === 重新載入頁面(避免即時 DOM 綁定錯亂)=== function reloadPage() { location.reload(); } })();