X Copy Tweet Link Helper with Toggle Settings

Copy tweet link via right-click, like, or button, with optional fixvx and language support. Includes toggle settings in Tampermonkey UI.

当前为 2025-04-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name X Copy Tweet Link Helper with Toggle Settings
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Copy tweet link via right-click, like, or button, with optional fixvx and language support. Includes toggle settings in Tampermonkey UI.
  6. // @description:zh-TW 透過右鍵、喜歡或按鈕複製推文鏈接,並可選擇 fixvx 和語言支援。包括 Tampermonkey UI 中的切換設定。
  7. // @author ChatGPT
  8. // @match https://x.com/*
  9. // @match https://twitter.com/*
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_unregisterMenuCommand
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. // === 使用者可切換的功能開關 ===
  21. const defaultSettings = {
  22. rightClickCopy: true, // 右鍵複製
  23. likeCopy: true, // 喜歡時自動褯複製
  24. showCopyButton: true, // 顯示複製按鈕
  25. useFixvx: false, // fixvx 前綴
  26. language: 'EN' // ZH 或 EN
  27. };
  28.  
  29. const settings = {
  30. get(key) {
  31. return GM_getValue(key, defaultSettings[key]);
  32. },
  33. set(key, value) {
  34. GM_setValue(key, value);
  35. }
  36. };
  37.  
  38. // === 多語系字典 ===
  39. const lang = {
  40. EN: {
  41. copySuccess: "Link copied!",
  42. copyButton: "🔗",
  43. rightClickCopy: 'Right-click Copy',
  44. likeCopy: 'Like Copy',
  45. showCopyButton: 'Show Copy Button',
  46. useFixvx: 'Use Fixvx',
  47. language: 'Language'
  48. },
  49. ZH: {
  50. copySuccess: "已複製鏈結!",
  51. copyButton: "🔗",
  52. rightClickCopy: '右鍵複製',
  53. likeCopy: '喜歡時複製',
  54. showCopyButton: '顯示複製按鈕',
  55. useFixvx: '使用 Fixvx',
  56. language: '語言'
  57. }
  58. };
  59.  
  60. const getText = (key) => lang[settings.get('language')][key];
  61.  
  62. // === 複製推文鏈結 ===
  63. function copyTweetLink(tweet) {
  64. const anchor = tweet.querySelector('a[href*="/status/"]');
  65. if (!anchor) return;
  66. let url = new URL(anchor.href);
  67. url.search = ''; // 移除 ?s=20 等參數
  68. if (settings.get('useFixvx')) {
  69. url.hostname = 'fixvx.com';
  70. }
  71. navigator.clipboard.writeText(url.toString()).then(() => {
  72. showToast(getText('copySuccess'));
  73. });
  74. }
  75.  
  76. // === 顯示提示訊息 ===
  77. function showToast(msg) {
  78. const toast = document.createElement('div');
  79. toast.innerText = msg;
  80. Object.assign(toast.style, {
  81. position: 'fixed',
  82. bottom: '20px',
  83. left: '50%',
  84. transform: 'translateX(-50%)',
  85. background: '#1da1f2',
  86. color: '#fff',
  87. padding: '8px 16px',
  88. borderRadius: '20px',
  89. zIndex: 9999,
  90. fontSize: '14px'
  91. });
  92. document.body.appendChild(toast);
  93. setTimeout(() => toast.remove(), 1500);
  94. }
  95.  
  96. // === 插入複製按鈕 ===
  97. function insertCopyButton(tweet) {
  98. if (tweet.querySelector('.my-copy-btn')) return;
  99. const actionBar = tweet.querySelector('[role="group"]');
  100. if (!actionBar) return;
  101.  
  102. const btn = document.createElement('div');
  103. btn.className = 'my-copy-btn';
  104. btn.innerText = getText('copyButton');
  105. Object.assign(btn.style, {
  106. fontSize: '18px',
  107. cursor: 'pointer',
  108. marginLeft: '12px',
  109. userSelect: 'none'
  110. });
  111.  
  112. btn.onclick = (e) => {
  113. e.stopPropagation();
  114. copyTweetLink(tweet);
  115. };
  116.  
  117. actionBar.appendChild(btn);
  118. }
  119.  
  120. // === 監聽推文動態載入 ===
  121. const tweetObserver = new MutationObserver(() => {
  122. document.querySelectorAll('article').forEach(tweet => {
  123. if (settings.get('showCopyButton')) insertCopyButton(tweet);
  124.  
  125. if (settings.get('rightClickCopy') && !tweet.hasAttribute('data-rightclick')) {
  126. tweet.setAttribute('data-rightclick', 'true');
  127. tweet.addEventListener('contextmenu', (e) => {
  128. if (tweet.querySelector('img, video')) {
  129. copyTweetLink(tweet);
  130. }
  131. });
  132. }
  133.  
  134. if (settings.get('likeCopy') && !tweet.hasAttribute('data-likecopy')) {
  135. tweet.setAttribute('data-likecopy', 'true');
  136. const likeBtn = tweet.querySelector('[data-testid="like"]');
  137. if (likeBtn) {
  138. likeBtn.addEventListener('click', () => {
  139. copyTweetLink(tweet);
  140. });
  141. }
  142. }
  143. });
  144. });
  145.  
  146. tweetObserver.observe(document.body, { childList: true, subtree: true });
  147.  
  148. // === 在油猴菜單中創建設定選項 ===
  149. function updateMenuCommands() {
  150. GM_unregisterMenuCommand();
  151. GM_registerMenuCommand(`${getText('rightClickCopy')} ( ${settings.get('rightClickCopy') ? '✅' : '❌'} )`, toggleRightClickCopy);
  152. GM_registerMenuCommand(`${getText('likeCopy')} ( ${settings.get('likeCopy') ? '✅' : '❌'} )`, toggleLikeCopy);
  153. GM_registerMenuCommand(`${getText('showCopyButton')} ( ${settings.get('showCopyButton') ? '✅' : '❌'} )`, toggleShowCopyButton);
  154. GM_registerMenuCommand(`${getText('useFixvx')} ( ${settings.get('useFixvx') ? '✅' : '❌'} )`, toggleUseFixvx);
  155. GM_registerMenuCommand(`${getText('language')} ( ${settings.get('language') === 'EN' ? 'EN' : 'ZH'} )`, toggleLanguage);
  156. }
  157.  
  158. updateMenuCommands();
  159.  
  160. function toggleRightClickCopy() {
  161. const currentValue = settings.get('rightClickCopy');
  162. settings.set('rightClickCopy', !currentValue);
  163. reloadPage();
  164. }
  165.  
  166. function toggleLikeCopy() {
  167. const currentValue = settings.get('likeCopy');
  168. settings.set('likeCopy', !currentValue);
  169. reloadPage();
  170. }
  171.  
  172. function toggleShowCopyButton() {
  173. const currentValue = settings.get('showCopyButton');
  174. settings.set('showCopyButton', !currentValue);
  175. reloadPage();
  176. }
  177.  
  178. function toggleUseFixvx() {
  179. const currentValue = settings.get('useFixvx');
  180. settings.set('useFixvx', !currentValue);
  181. reloadPage();
  182. }
  183.  
  184. function toggleLanguage() {
  185. const currentValue = settings.get('language');
  186. const newLang = currentValue === 'EN' ? 'ZH' : 'EN';
  187. settings.set('language', newLang);
  188. reloadPage();
  189. }
  190.  
  191. // 重新整理頁面
  192. function reloadPage() {
  193. location.reload();
  194. }
  195.  
  196. })();