// ==UserScript== // @name X Copy Tweet Link Helper // @name:zh-TW X 複製推文連結助手 // @name:zh-CN X 复制推文连结助手 // @namespace http://tampermonkey.net/ // @version 1.7 // @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 // @downloadURL none // ==/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 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('useFixupx')) { url.hostname = 'fixupx.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('useFixupx')} ( ${settings.get('useFixupx') ? '✅' : '❌'} )`, () => toggleBooleanSetting('useFixupx') ); 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(); } })();