// ==UserScript==
// @name X Copy Tweet Link Helper
// @name:zh-TW X 複製推文連結助手
// @name:zh-CN X 复制推文连结助手
// @namespace http://tampermonkey.net/
// @version 1.6
// @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
// ==/UserScript==
(function () {
'use strict';
const defaultSettings = {
rightClickCopy: true,
likeCopy: true,
showCopyButton: true,
useFixvx: 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',
useFixvx: 'Use Fixvx',
language: 'Language'
},
ZH: {
copySuccess: "已複製鏈結!",
copyButton: "🔗",
rightClickCopy: '右鍵複製',
likeCopy: '喜歡時複製',
showCopyButton: '顯示複製按鈕',
useFixvx: '使用 Fixvx',
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('useFixvx')) {
url.hostname = 'fixvx.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('useFixvx')} ( ${settings.get('useFixvx') ? '✅' : '❌'} )`,
() => toggleBooleanSetting('useFixvx')
);
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();
}
})();