share-link-copy

快捷复制便于分享的页面标题和URL,支持自定义快捷键,支持设置是否保留URL参数

  1. // ==UserScript==
  2. // @name share-link-copy
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.9
  5. // @description 快捷复制便于分享的页面标题和URL,支持自定义快捷键,支持设置是否保留URL参数
  6. // @license MIT
  7. // @author Lainbo
  8. // @match *://*/*
  9. // @grant GM_setClipboard
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_addStyle
  14. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTAgMTM0aDc3NXY1ODNIMTEweiIvPjxwYXRoIGQ9Ik02MjUgNjg0LjhjMjcuNSAwIDUxLTkuOCA3MC42LTI5LjMgMTkuNi0xOS42IDI5LjQtNDMuMSAyOS4zLTcwLjcgMC0yNy41LTkuOC01MS4xLTI5LjMtNzAuNi0xOS41LTE5LjUtNDMuMS0yOS4zLTcwLjYtMjkuNC0xMi41IDAtMjQuNCAyLjMtMzUuNiA2LjktMTEuMiA0LjYtMjEuNCAxMC42LTMwLjYgMTguMUw0MjUgNDQyLjN2LTE1bDEzMy44LTY3LjVjOS4yIDcuNSAxOS40IDEzLjUgMzAuNiAxOC4xczIzLjEgNi45IDM1LjYgNi45YzI3LjUgMCA1MS05LjggNzAuNi0yOS40IDE5LjYtMTkuNiAyOS40LTQzLjEgMjkuMy03MC42IDAtMjcuNS05LjgtNTEuMS0yOS4zLTcwLjYtMTkuNS0xOS41LTQzLjEtMjkuMy03MC42LTI5LjQtMjcuNi0uMS01MS4xIDkuNy03MC42IDI5LjQtMTkuNSAxOS43LTI5LjMgNDMuMi0yOS40IDcwLjZ2Ny41bC0xMzMuOCA2Ny41Yy05LjItNy41LTE5LjQtMTMuNi0zMC42LTE4LjFzLTIzLjEtNi45LTM1LjYtNi45Yy0yNy41IDAtNTEgOS44LTcwLjYgMjkuNC0xOS42IDE5LjYtMjkuNCA0My4xLTI5LjQgNzAuNiAwIDI3LjUgOS44IDUxIDI5LjQgNzAuNiAxOS42IDE5LjYgNDMuMiAyOS40IDcwLjYgMjkuMyAxMi41IDAgMjQuNC0yLjMgMzUuNi02LjggMTEuMy00LjYgMjEuNS0xMC42IDMwLjYtMTguMUw1MjUgNTc3LjN2Ny41YzAgMjcuNSA5LjggNTEgMjkuNCA3MC43IDE5LjYgMTkuNiA0My4xIDI5LjQgNzAuNiAyOS4zbS00MjUgMTUwbC0xMTUgMTE1Yy0xNS44IDE1LjgtMzQgMTkuNC01NC40IDEwLjZDMTAuMiA5NTEuOCAwIDkzNi4xIDAgOTEzLjZWMTM0LjhjMC0yNy41IDkuOC01MSAyOS40LTcwLjZTNzIuNSAzNC45IDEwMCAzNC44aDgwMGMyNy41IDAgNTEuMSA5LjggNzAuNiAyOS40IDE5LjYgMTkuNiAyOS40IDQzLjEgMjkuNCA3MC42djYwMGMwIDI3LjUtOS44IDUxLjEtMjkuNCA3MC42LTE5LjYgMTkuNi00My4xIDI5LjQtNzAuNiAyOS40SDIwMHoiIGZpbGw9IiMxNjVkZmYiLz48L3N2Zz4=
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict'
  19.  
  20. GM_addStyle(`
  21. .els-notification-Pfq0X5 {
  22. position: fixed;
  23. bottom: 20px;
  24. right: 20px;
  25. background-color: #2da44e;
  26. color: white;
  27. padding: 12px 20px;
  28. border-radius: 4px;
  29. font-size: 14px;
  30. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  31. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  32. opacity: 0;
  33. transition: opacity 0.3s ease-in-out;
  34. z-index: 9999;
  35. }
  36. .modal-overlay-8W2Q7t {
  37. position: fixed;
  38. top: 0;
  39. left: 0;
  40. right: 0;
  41. bottom: 0;
  42. background-color: rgba(0, 0, 0, 0.5);
  43. display: flex;
  44. justify-content: center;
  45. align-items: center;
  46. z-index: 999999;
  47. }
  48. .modal-content-1Zb3tL {
  49. background-color: #f6f8fa;
  50. padding: 24px;
  51. border-radius: 6px;
  52. width: 460px;
  53. box-shadow: 0 8px 24px rgba(140,149,159,0.2);
  54. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  55. }
  56. .modal-title-cKu8SM {
  57. font-size: 20px;
  58. font-weight: 600;
  59. margin-bottom: 16px;
  60. color: #24292f;
  61. }
  62. .modal-description-KM3XGv {
  63. font-size: 14px;
  64. color: #57606a;
  65. margin-bottom: 8px;
  66. line-height: 1.5;
  67. }
  68. .modal-content-1Zb3tL input[type="text"], .modal-content-1Zb3tL textarea {
  69. width: 100%;
  70. margin-bottom: 16px;
  71. padding: 5px 12px;
  72. font-size: 14px;
  73. line-height: 20px;
  74. color: #24292f;
  75. vertical-align: middle;
  76. background-color: #ffffff;
  77. background-repeat: no-repeat;
  78. background-position: right 8px center;
  79. border: 1px solid #d0d7de;
  80. border-radius: 6px;
  81. box-shadow: inset 0 1px 0 rgba(208,215,222,0.2);
  82. box-sizing: border-box;
  83. }
  84. .modal-content-1Zb3tL textarea {
  85. height: 100px;
  86. resize: vertical;
  87. }
  88. .modal-content-1Zb3tL button {
  89. color: #ffffff;
  90. background-color: #2da44e;
  91. padding: 5px 16px;
  92. font-size: 14px;
  93. font-weight: 500;
  94. line-height: 20px;
  95. white-space: nowrap;
  96. vertical-align: middle;
  97. cursor: pointer;
  98. border: 1px solid;
  99. border-radius: 6px;
  100. appearance: none;
  101. user-select: none;
  102. margin-left: 8px;
  103. }
  104. .modal-content-1Zb3tL button.cancel {
  105. color: #24292f;
  106. background-color: #f6f8fa;
  107. border-color: rgba(27,31,36,0.15);
  108. }
  109. .modal-content-1Zb3tL button:hover {
  110. background-color: #2c974b;
  111. }
  112. .modal-content-1Zb3tL button.cancel:hover {
  113. background-color: #f3f4f6;
  114. }
  115. .modal-hint-8yFDJi {
  116. font-size: 10px;
  117. color: #57606a;
  118. line-height: 1.1;
  119. font-style: italic;
  120. margin: 0;
  121. user-select: none;
  122. }
  123. #els-domains-7u6z9U {
  124. resize: none;
  125. margin-top: 12px;
  126. }
  127. .modal-buttons-L5xkyU {
  128. display: flex;
  129. justify-content: flex-end;
  130. margin-top: 16px;
  131. }
  132. .modal-checkbox-container-2Tgu5E {
  133. display: flex;
  134. align-items: center;
  135. margin-bottom: 16px;
  136. }
  137. .modal-checkbox-container-2Tgu5E input[type="checkbox"] {
  138. margin-right: 8px;
  139. width: auto;
  140. }
  141. .modal-checkbox-container-2Tgu5E label {
  142. font-size: 14px;
  143. color: #24292f;
  144. user-select: none;
  145. }
  146. .modal-content-1Zb3tL input[type="text"]:focus, .modal-content-1Zb3tL textarea:focus {
  147. outline: none;
  148. border-color: #0969da;
  149. box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.3);
  150. }
  151. `)
  152.  
  153. // 从存储中获取保留参数的域名列表,如果没有则使用默认值
  154. let domainsToKeepParams = GM_getValue('domainsToKeepParams', ['youtube.com', 'google.com'])
  155.  
  156. // 获取保存的快捷键,默认为 'alt+c'
  157. let shortcut = GM_getValue('easyLinkShareShortcut', 'alt+c')
  158. // 获取保存的 Markdown 格式快捷键,默认为 'alt+shift+c'
  159. let markdownShortcut = GM_getValue('easyLinkShareMarkdownShortcut', 'alt+shift+c')
  160.  
  161. // 添加新的 GM_getValue 调用来获取列表模式
  162. let isBlacklistMode = GM_getValue('isBlacklistMode', false)
  163.  
  164. // 用来跟踪模态框是否打开
  165. let isModalOpen = false
  166.  
  167. // 修改 URL 处理函数
  168. function processUrl(url, domainsToKeepParams) {
  169. const urlObj = new URL(url)
  170. const domain = urlObj.hostname
  171.  
  172. // 域名在列表中
  173. const isInList = domainsToKeepParams.some(d => domain.endsWith(d))
  174.  
  175. if (isBlacklistMode) {
  176. // 黑名单模式:如果域名在列表中,则移除参数
  177. if (isInList) {
  178. return `${urlObj.origin}${urlObj.pathname}`
  179. }
  180. }
  181. else {
  182. // 白名单模式:如果域名不在列表中,则移除参数
  183. if (!isInList) {
  184. return `${urlObj.origin}${urlObj.pathname}`
  185. }
  186. }
  187.  
  188. return url
  189. }
  190.  
  191. // 创建通知功能
  192. function createNotification() {
  193. return function showNotification(message) {
  194. const notification = document.createElement('div')
  195. notification.className = 'els-notification-Pfq0X5'
  196. notification.textContent = message
  197. document.body.appendChild(notification)
  198.  
  199. setTimeout(() => {
  200. notification.style.opacity = '1'
  201. }, 10)
  202.  
  203. setTimeout(() => {
  204. notification.style.opacity = '0'
  205. setTimeout(() => {
  206. document.body.removeChild(notification)
  207. }, 300)
  208. }, 3000)
  209. }
  210. }
  211.  
  212. const showNotification = createNotification()
  213.  
  214. function getCodePenTitle() {
  215. try {
  216. // 尝试从父窗口获取标题
  217. return { title: window.parent.document.title, error: false }
  218. }
  219. catch (error) {
  220. // 如果无法访问父窗口,使用当前文档的标题
  221. return {
  222. title: document.title.replace(' - CodePen', ''),
  223. error: true,
  224. }
  225. }
  226. }
  227.  
  228. // 修改复制链接的主要函数
  229. function copyLink(isMarkdown = false) {
  230. let url = window.location.href
  231. let title = document.title
  232. let showError = false
  233.  
  234. // 特殊处理 CodePen
  235. if (url.includes('cdpn.io')) {
  236. try {
  237. url = window.parent.location.href
  238. const result = getCodePenTitle()
  239. title = result.title
  240. showError = result.error
  241. }
  242. catch (error) {
  243. showError = true
  244. }
  245. }
  246.  
  247. if (showError) {
  248. showNotification('当前页面焦点可能在iframe中,请切换到主窗口进行复制')
  249. return // 如果出错,直接返回,不执行复制操作
  250. }
  251.  
  252. url = processUrl(url, domainsToKeepParams)
  253.  
  254. const text = isMarkdown ? `[${title}](${url})` : `${title}\n${url}`
  255.  
  256. GM_setClipboard(text, 'text')
  257. showNotification(isMarkdown ? 'Markdown 格式链接已复制到剪贴板!' : '链接已复制到剪贴板!')
  258. }
  259.  
  260. // 解析快捷键字符串
  261. function parseShortcut(shortcutStr) {
  262. const parts = shortcutStr.toLowerCase().split('+')
  263. return {
  264. altKey: parts.includes('alt') || parts.includes('option'),
  265. ctrlKey: parts.includes('ctrl') || parts.includes('control'),
  266. shiftKey: parts.includes('shift'),
  267. metaKey: parts.includes('meta') || parts.includes('cmd') || parts.includes('command'),
  268. keys: parts.filter(part => !['alt', 'ctrl', 'control', 'shift', 'meta', 'win', 'option', 'cmd', 'command'].includes(part)),
  269. }
  270. }
  271.  
  272. // 修改设置界面创建函数
  273. function createSettingsUI(options) {
  274. const { shortcut, markdownShortcut, domainsToKeepParams, isBlacklistMode, onSave } = options
  275.  
  276. const modalHTML = `
  277. <div class="modal-overlay-8W2Q7t">
  278. <div class="modal-content-1Zb3tL">
  279. <div class="modal-title-cKu8SM">设置</div>
  280. <div class="modal-description-KM3XGv">设置快捷键组合以快速复制页面标题和链接。按 ESC 键撤销更改。</div>
  281. <input type="text" id="els-shortcut" placeholder="按下快捷键组合" value="${shortcut}" readonly>
  282. <div class="modal-description-KM3XGv">设置快捷键组合以复制 Markdown 格式的链接:</div>
  283. <input type="text" id="els-markdown-shortcut" placeholder="按下快捷键组合" value="${markdownShortcut}" readonly>
  284. <div class="modal-description-KM3XGv">参数去除模式:</div>
  285. <div class="modal-checkbox-container-2Tgu5E">
  286. <input type="checkbox" id="els-blacklist-mode" ${isBlacklistMode ? 'checked' : ''}>
  287. <label for="els-blacklist-mode">黑名单模式(勾选后,列表中的域名复制时将不保留参数)</label>
  288. </div>
  289. <div class="modal-description-KM3XGv" id="els-domains-description-Cgt5uR">设置需要${isBlacklistMode ? '移除' : '保留'}参数的域名(每行一个):</div>
  290. <div class="modal-hint-8yFDJi" id="els-domains-hint-0DAupg">当前为${isBlacklistMode ? '黑名单' : '白名单'}模式。${isBlacklistMode ? '列表中的域名将不保留参数,其他域名将保留参数。' : '列表中的域名将保留参数,其他域名将不保留参数。'}</div>
  291. <textarea id="els-domains-7u6z9U">${domainsToKeepParams.join('\n')}</textarea>
  292. <div class="modal-buttons-L5xkyU">
  293. <button id="els-cancel-W3OUcP" class="cancel">取消</button>
  294. <button id="els-save-e8UfX6">保存</button>
  295. </div>
  296. </div>
  297. </div>
  298. `
  299.  
  300. const modalContainer = document.createElement('div')
  301. modalContainer.innerHTML = modalHTML
  302.  
  303. // 使用 top.document.body 而不是 document.body
  304. top.document.body.appendChild(modalContainer)
  305.  
  306. const markdownShortcutInput = document.getElementById('els-markdown-shortcut')
  307. const shortcutInput = document.getElementById('els-shortcut')
  308. const domainsTextarea = document.getElementById('els-domains-7u6z9U')
  309. let listeningForShortcut = false
  310. let currentShortcut = []
  311. let currentMarkdownShortcut = []
  312. let listeningForMarkdownShortcut = false
  313.  
  314. function resetShortcut() {
  315. shortcutInput.value = shortcut.toLowerCase()
  316. currentShortcut = shortcut.toLowerCase().split('+')
  317. listeningForShortcut = false
  318. }
  319.  
  320. function resetMarkdownShortcut() {
  321. markdownShortcutInput.value = markdownShortcut.toLowerCase()
  322. currentMarkdownShortcut = markdownShortcut.toLowerCase().split('+')
  323. listeningForMarkdownShortcut = false
  324. }
  325.  
  326. function closeSettings() {
  327. // 修改这里: 从 top.document.body 中移除
  328. top.document.body.removeChild(modalContainer)
  329. document.removeEventListener('keydown', escapeHandler)
  330. isModalOpen = false
  331. }
  332.  
  333. function escapeHandler(e) {
  334. if (e.key === 'Escape') {
  335. resetShortcut()
  336. resetMarkdownShortcut()
  337. domainsTextarea.value = domainsToKeepParams.join('\n')
  338. }
  339. }
  340.  
  341. document.addEventListener('keydown', escapeHandler)
  342.  
  343. shortcutInput.addEventListener('focus', () => {
  344. if (listeningForMarkdownShortcut) {
  345. resetMarkdownShortcut()
  346. }
  347. listeningForShortcut = true
  348. currentShortcut = []
  349. shortcutInput.value = '按下快捷键组合...'
  350. })
  351.  
  352. markdownShortcutInput.addEventListener('focus', () => {
  353. if (listeningForShortcut) {
  354. resetShortcut()
  355. }
  356. listeningForMarkdownShortcut = true
  357. currentMarkdownShortcut = []
  358. markdownShortcutInput.value = '按下快捷键组合...'
  359. })
  360.  
  361. document.addEventListener('keydown', (e) => {
  362. if (listeningForShortcut || listeningForMarkdownShortcut) {
  363. e.preventDefault()
  364. if (e.code === 'Escape') {
  365. listeningForShortcut ? resetShortcut() : resetMarkdownShortcut()
  366. return
  367. }
  368. const currentArray = listeningForShortcut ? currentShortcut : currentMarkdownShortcut
  369. const inputElement = listeningForShortcut ? shortcutInput : markdownShortcutInput
  370.  
  371. if (['AltLeft', 'AltRight', 'ControlLeft', 'ControlRight', 'ShiftLeft', 'ShiftRight', 'MetaLeft', 'MetaRight'].includes(e.code)) {
  372. const modifier = e.code.replace('Left', '').replace('Right', '').toLowerCase()
  373. if (!currentArray.includes(modifier)) {
  374. currentArray.push(modifier)
  375. }
  376. }
  377. else {
  378. const keyDisplay = getKeyDisplay(e.code)
  379. if (!currentArray.includes(keyDisplay)) {
  380. currentArray.push(keyDisplay)
  381. }
  382. listeningForShortcut = false
  383. listeningForMarkdownShortcut = false
  384. }
  385. inputElement.value = currentArray.join('+')
  386. }
  387. })
  388.  
  389. document.addEventListener('keyup', (e) => {
  390. if ((listeningForShortcut || listeningForMarkdownShortcut) && ['alt', 'control', 'shift', 'meta'].includes(e.key.toLowerCase())) {
  391. listeningForShortcut = false
  392. listeningForMarkdownShortcut = false
  393. }
  394. })
  395.  
  396. const blacklistModeCheckbox = document.getElementById('els-blacklist-mode')
  397. const domainsDescription = document.getElementById('els-domains-description-Cgt5uR')
  398. const domainsHint = document.getElementById('els-domains-hint-0DAupg')
  399.  
  400. blacklistModeCheckbox.addEventListener('change', (e) => {
  401. const isBlacklist = e.target.checked
  402. domainsDescription.textContent = `设置需要${isBlacklist ? '移除' : '保留'}参数的域名(每行一个):`
  403. domainsHint.textContent = `当前为${isBlacklist ? '黑名单' : '白名单'}模式。${isBlacklist ? '列表中的域名将不保留参数,其他域名将保留参数。' : '列表中的域名将保留参数,其他域名将不保留参数。'}`
  404. })
  405.  
  406. document.getElementById('els-save-e8UfX6').addEventListener('click', () => {
  407. const newShortcut = shortcutInput.value
  408. const newMarkdownShortcut = markdownShortcutInput.value
  409. const newDomains = domainsTextarea.value.split('\n').filter(d => d.trim() !== '')
  410. const newIsBlacklistMode = blacklistModeCheckbox.checked
  411.  
  412. onSave({
  413. shortcut: newShortcut,
  414. markdownShortcut: newMarkdownShortcut,
  415. domainsToKeepParams: newDomains,
  416. isBlacklistMode: newIsBlacklistMode,
  417. })
  418.  
  419. closeSettings()
  420. })
  421.  
  422. document.getElementById('els-cancel-W3OUcP').addEventListener('click', closeSettings)
  423.  
  424. return closeSettings
  425. }
  426.  
  427. // 修改打开设置界面函数
  428. function openSettings() {
  429. isModalOpen = true
  430. createSettingsUI({
  431. shortcut,
  432. markdownShortcut,
  433. domainsToKeepParams,
  434. isBlacklistMode,
  435. onSave: (newSettings) => {
  436. shortcut = newSettings.shortcut
  437. markdownShortcut = newSettings.markdownShortcut
  438. domainsToKeepParams = newSettings.domainsToKeepParams
  439. isBlacklistMode = newSettings.isBlacklistMode
  440. GM_setValue('easyLinkShareShortcut', shortcut)
  441. GM_setValue('easyLinkShareMarkdownShortcut', markdownShortcut)
  442. GM_setValue('domainsToKeepParams', domainsToKeepParams)
  443. GM_setValue('isBlacklistMode', isBlacklistMode)
  444. isModalOpen = false
  445. },
  446. })
  447. }
  448. function getKeyDisplay(code) {
  449. const keyMap = {
  450. Digit1: '1',
  451. Digit2: '2',
  452. Digit3: '3',
  453. Digit4: '4',
  454. Digit5: '5',
  455. Digit6: '6',
  456. Digit7: '7',
  457. Digit8: '8',
  458. Digit9: '9',
  459. Digit0: '0',
  460. KeyA: 'a',
  461. KeyB: 'b',
  462. KeyC: 'c',
  463. KeyD: 'd',
  464. KeyE: 'e',
  465. KeyF: 'f',
  466. KeyG: 'g',
  467. KeyH: 'h',
  468. KeyI: 'i',
  469. KeyJ: 'j',
  470. KeyK: 'k',
  471. KeyL: 'l',
  472. KeyM: 'm',
  473. KeyN: 'n',
  474. KeyO: 'o',
  475. KeyP: 'p',
  476. KeyQ: 'q',
  477. KeyR: 'r',
  478. KeyS: 's',
  479. KeyT: 't',
  480. KeyU: 'u',
  481. KeyV: 'v',
  482. KeyW: 'w',
  483. KeyX: 'x',
  484. KeyY: 'y',
  485. KeyZ: 'z',
  486. }
  487. return keyMap[code] || code.toLowerCase()
  488. }
  489.  
  490. function matchShortcut(e, shortcut) {
  491. return (e.altKey === shortcut.altKey
  492. && e.ctrlKey === shortcut.ctrlKey
  493. && e.shiftKey === shortcut.shiftKey
  494. && e.metaKey === shortcut.metaKey
  495. && shortcut.keys.every(key => getKeyDisplay(e.code) === key.toLowerCase()))
  496. }
  497.  
  498. // 修改快捷键监听
  499. document.addEventListener('keydown', (e) => {
  500. // 如果模态框打开,不响应复制快捷键
  501. if (isModalOpen) { return }
  502.  
  503. const normalShortcut = parseShortcut(shortcut)
  504. const mdShortcut = parseShortcut(markdownShortcut)
  505.  
  506. if (matchShortcut(e, normalShortcut)) {
  507. e.preventDefault()
  508. copyLink(false)
  509. }
  510. else if (matchShortcut(e, mdShortcut)) {
  511. e.preventDefault()
  512. copyLink(true)
  513. }
  514. })
  515.  
  516. // 注册油猴菜单命令
  517. GM_registerMenuCommand('设置', openSettings)
  518. })()