// ==UserScript==
// @name X Copy Tweet Link Helper
// @name:zh-TW X 複製推文連結助手
// @name:zh-CN X 复制推文连结助手
// @namespace http://tampermonkey.net/
// @version 2.2
// @description Copy tweet links via right-click, like button, or dedicated button. Supports Fixupx mode, allows toggling specific features directly in the Tampermonkey interface, and offers Chinese/English language switching.
// @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,
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 getText = (key) => lang[settings.get('language')][key];
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'));
});
}
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('.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
}
}
// 防止重複綁定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);
}
});
}
// 只處理新增的article節點
function processTweetNode(node) {
if (!(node instanceof HTMLElement)) return;
if (node.tagName === 'ARTICLE') {
if (settings.get('showCopyButton')) insertCopyButton(node);
if (settings.get('rightClickCopy')) bindRightClickCopy(node);
if (settings.get('likeCopy')) bindLikeCopy(node);
} else {
node.querySelectorAll && node.querySelectorAll('article').forEach(article => {
if (settings.get('showCopyButton')) insertCopyButton(article);
if (settings.get('rightClickCopy')) bindRightClickCopy(article);
if (settings.get('likeCopy')) bindLikeCopy(article);
});
}
}
// 初始處理現有推文
document.querySelectorAll('article').forEach(processTweetNode);
// 只處理新增節點
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));
// 支援多語言自動切換
const langs = Object.keys(lang);
const currentLangIdx = langs.indexOf(settings.get('language'));
const nextLang = langs[(currentLangIdx + 1) % langs.length];
// 這裡根據目前語言顯示「語言 中文」或「語言 EN」
let langDisplay = settings.get('language');
if (langDisplay === 'ZH') langDisplay = '中文';
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 toggleLanguage(nextLang) {
settings.set('language', nextLang);
reloadPage();
}
function reloadPage() {
location.reload();
}
})();