// ==UserScript== // @name X/Twitter Copy Tweet Link Helper // @name:zh-TW X/Twitter 複製推文連結助手 // @name:zh-CN X/Twitter 复制推文连结助手 // @namespace http://tampermonkey.net/ // @version 3.5 // @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 https://update.greasyfork.icu/scripts/532102/XTwitter%20Copy%20Tweet%20Link%20Helper.user.js // @updateURL https://update.greasyfork.icu/scripts/532102/XTwitter%20Copy%20Tweet%20Link%20Helper.meta.js // ==/UserScript== (function () { 'use strict'; // === 預設設定值 === const defaultSettings = { rightClickCopy: true, // 右鍵複製推文連結 likeCopy: true, // 按讚時自動複製連結 showCopyButton: true, // 顯示🔗複製按鈕 disableClickRedirect: true, // 禁止點擊推文跳轉 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', useFixupx: 'Use Fixupx', language: 'Language' }, ZH: { copySuccess: "已複製鏈結!", copyButton: "🔗", rightClickCopy: '右鍵複製', likeCopy: '喜歡時複製', showCopyButton: '顯示複製按鈕', disableClickRedirect: '禁止點擊跳轉', useFixupx: '使用 Fixupx', 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', 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); }; 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('a time') // 排除包含