// ==UserScript==
// @name X/Twitter Copy Tweet Link Helper
// @name:zh-TW X/Twitter 複製推文連結助手
// @name:zh-CN X/Twitter 复制推文连结助手
// @namespace http://tampermonkey.net/
// @version 2.7
// @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
// ==/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') || // 排除複製按鈕
target.closest('[data-testid="tweetPhoto"]') // 排除推特圖片
) {
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();
}
})();