share-link-copy

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         share-link-copy
// @namespace    http://tampermonkey.net/
// @version      1.0.9
// @description  快捷复制便于分享的页面标题和URL,支持自定义快捷键,支持设置是否保留URL参数
// @license      MIT
// @author       Lainbo
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTAgMTM0aDc3NXY1ODNIMTEweiIvPjxwYXRoIGQ9Ik02MjUgNjg0LjhjMjcuNSAwIDUxLTkuOCA3MC42LTI5LjMgMTkuNi0xOS42IDI5LjQtNDMuMSAyOS4zLTcwLjcgMC0yNy41LTkuOC01MS4xLTI5LjMtNzAuNi0xOS41LTE5LjUtNDMuMS0yOS4zLTcwLjYtMjkuNC0xMi41IDAtMjQuNCAyLjMtMzUuNiA2LjktMTEuMiA0LjYtMjEuNCAxMC42LTMwLjYgMTguMUw0MjUgNDQyLjN2LTE1bDEzMy44LTY3LjVjOS4yIDcuNSAxOS40IDEzLjUgMzAuNiAxOC4xczIzLjEgNi45IDM1LjYgNi45YzI3LjUgMCA1MS05LjggNzAuNi0yOS40IDE5LjYtMTkuNiAyOS40LTQzLjEgMjkuMy03MC42IDAtMjcuNS05LjgtNTEuMS0yOS4zLTcwLjYtMTkuNS0xOS41LTQzLjEtMjkuMy03MC42LTI5LjQtMjcuNi0uMS01MS4xIDkuNy03MC42IDI5LjQtMTkuNSAxOS43LTI5LjMgNDMuMi0yOS40IDcwLjZ2Ny41bC0xMzMuOCA2Ny41Yy05LjItNy41LTE5LjQtMTMuNi0zMC42LTE4LjFzLTIzLjEtNi45LTM1LjYtNi45Yy0yNy41IDAtNTEgOS44LTcwLjYgMjkuNC0xOS42IDE5LjYtMjkuNCA0My4xLTI5LjQgNzAuNiAwIDI3LjUgOS44IDUxIDI5LjQgNzAuNiAxOS42IDE5LjYgNDMuMiAyOS40IDcwLjYgMjkuMyAxMi41IDAgMjQuNC0yLjMgMzUuNi02LjggMTEuMy00LjYgMjEuNS0xMC42IDMwLjYtMTguMUw1MjUgNTc3LjN2Ny41YzAgMjcuNSA5LjggNTEgMjkuNCA3MC43IDE5LjYgMTkuNiA0My4xIDI5LjQgNzAuNiAyOS4zbS00MjUgMTUwbC0xMTUgMTE1Yy0xNS44IDE1LjgtMzQgMTkuNC01NC40IDEwLjZDMTAuMiA5NTEuOCAwIDkzNi4xIDAgOTEzLjZWMTM0LjhjMC0yNy41IDkuOC01MSAyOS40LTcwLjZTNzIuNSAzNC45IDEwMCAzNC44aDgwMGMyNy41IDAgNTEuMSA5LjggNzAuNiAyOS40IDE5LjYgMTkuNiAyOS40IDQzLjEgMjkuNCA3MC42djYwMGMwIDI3LjUtOS44IDUxLjEtMjkuNCA3MC42LTE5LjYgMTkuNi00My4xIDI5LjQtNzAuNiAyOS40SDIwMHoiIGZpbGw9IiMxNjVkZmYiLz48L3N2Zz4=
// ==/UserScript==

(function () {
  'use strict'

  GM_addStyle(`
    .els-notification-Pfq0X5 {
      position: fixed;
      bottom: 20px;
      right: 20px;
      background-color: #2da44e;
      color: white;
      padding: 12px 20px;
      border-radius: 4px;
      font-size: 14px;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      opacity: 0;
      transition: opacity 0.3s ease-in-out;
      z-index: 9999;
    }
    .modal-overlay-8W2Q7t {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: rgba(0, 0, 0, 0.5);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 999999;
    }
    .modal-content-1Zb3tL {
      background-color: #f6f8fa;
      padding: 24px;
      border-radius: 6px;
      width: 460px;
      box-shadow: 0 8px 24px rgba(140,149,159,0.2);
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
    }
    .modal-title-cKu8SM {
      font-size: 20px;
      font-weight: 600;
      margin-bottom: 16px;
      color: #24292f;
    }
    .modal-description-KM3XGv {
      font-size: 14px;
      color: #57606a;
      margin-bottom: 8px;
      line-height: 1.5;
    }
    .modal-content-1Zb3tL input[type="text"], .modal-content-1Zb3tL textarea {
      width: 100%;
      margin-bottom: 16px;
      padding: 5px 12px;
      font-size: 14px;
      line-height: 20px;
      color: #24292f;
      vertical-align: middle;
      background-color: #ffffff;
      background-repeat: no-repeat;
      background-position: right 8px center;
      border: 1px solid #d0d7de;
      border-radius: 6px;
      box-shadow: inset 0 1px 0 rgba(208,215,222,0.2);
      box-sizing: border-box;
    }
    .modal-content-1Zb3tL textarea {
      height: 100px;
      resize: vertical;
    }
    .modal-content-1Zb3tL button {
      color: #ffffff;
      background-color: #2da44e;
      padding: 5px 16px;
      font-size: 14px;
      font-weight: 500;
      line-height: 20px;
      white-space: nowrap;
      vertical-align: middle;
      cursor: pointer;
      border: 1px solid;
      border-radius: 6px;
      appearance: none;
      user-select: none;
      margin-left: 8px;
    }
    .modal-content-1Zb3tL button.cancel {
      color: #24292f;
      background-color: #f6f8fa;
      border-color: rgba(27,31,36,0.15);
    }
    .modal-content-1Zb3tL button:hover {
      background-color: #2c974b;
    }
    .modal-content-1Zb3tL button.cancel:hover {
      background-color: #f3f4f6;
    }
    .modal-hint-8yFDJi {
      font-size: 10px;
      color: #57606a;
      line-height: 1.1;
      font-style: italic;
      margin: 0;
      user-select: none;
    }
    #els-domains-7u6z9U {
      resize: none;
      margin-top: 12px;
    }
    .modal-buttons-L5xkyU {
      display: flex;
      justify-content: flex-end;
      margin-top: 16px;
    }
    .modal-checkbox-container-2Tgu5E {
      display: flex;
      align-items: center;
      margin-bottom: 16px;
    }
    .modal-checkbox-container-2Tgu5E input[type="checkbox"] {
      margin-right: 8px;
      width: auto;
    }
    .modal-checkbox-container-2Tgu5E label {
      font-size: 14px;
      color: #24292f;
      user-select: none;
    }
    .modal-content-1Zb3tL input[type="text"]:focus, .modal-content-1Zb3tL textarea:focus {
      outline: none;
      border-color: #0969da;
      box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.3);
    }
  `)

  // 从存储中获取保留参数的域名列表,如果没有则使用默认值
  let domainsToKeepParams = GM_getValue('domainsToKeepParams', ['youtube.com', 'google.com'])

  // 获取保存的快捷键,默认为 'alt+c'
  let shortcut = GM_getValue('easyLinkShareShortcut', 'alt+c')
  // 获取保存的 Markdown 格式快捷键,默认为 'alt+shift+c'
  let markdownShortcut = GM_getValue('easyLinkShareMarkdownShortcut', 'alt+shift+c')

  // 添加新的 GM_getValue 调用来获取列表模式
  let isBlacklistMode = GM_getValue('isBlacklistMode', false)

  // 用来跟踪模态框是否打开
  let isModalOpen = false

  // 修改 URL 处理函数
  function processUrl(url, domainsToKeepParams) {
    const urlObj = new URL(url)
    const domain = urlObj.hostname

    // 域名在列表中
    const isInList = domainsToKeepParams.some(d => domain.endsWith(d))

    if (isBlacklistMode) {
      // 黑名单模式:如果域名在列表中,则移除参数
      if (isInList) {
        return `${urlObj.origin}${urlObj.pathname}`
      }
    }
    else {
      // 白名单模式:如果域名不在列表中,则移除参数
      if (!isInList) {
        return `${urlObj.origin}${urlObj.pathname}`
      }
    }

    return url
  }

  // 创建通知功能
  function createNotification() {
    return function showNotification(message) {
      const notification = document.createElement('div')
      notification.className = 'els-notification-Pfq0X5'
      notification.textContent = message
      document.body.appendChild(notification)

      setTimeout(() => {
        notification.style.opacity = '1'
      }, 10)

      setTimeout(() => {
        notification.style.opacity = '0'
        setTimeout(() => {
          document.body.removeChild(notification)
        }, 300)
      }, 3000)
    }
  }

  const showNotification = createNotification()

  function getCodePenTitle() {
    try {
      // 尝试从父窗口获取标题
      return { title: window.parent.document.title, error: false }
    }
    catch (error) {
      // 如果无法访问父窗口,使用当前文档的标题
      return {
        title: document.title.replace(' - CodePen', ''),
        error: true,
      }
    }
  }

  // 修改复制链接的主要函数
  function copyLink(isMarkdown = false) {
    let url = window.location.href
    let title = document.title
    let showError = false

    // 特殊处理 CodePen
    if (url.includes('cdpn.io')) {
      try {
        url = window.parent.location.href
        const result = getCodePenTitle()
        title = result.title
        showError = result.error
      }
      catch (error) {
        showError = true
      }
    }

    if (showError) {
      showNotification('当前页面焦点可能在iframe中,请切换到主窗口进行复制')
      return // 如果出错,直接返回,不执行复制操作
    }

    url = processUrl(url, domainsToKeepParams)

    const text = isMarkdown ? `[${title}](${url})` : `${title}\n${url}`

    GM_setClipboard(text, 'text')
    showNotification(isMarkdown ? 'Markdown 格式链接已复制到剪贴板!' : '链接已复制到剪贴板!')
  }

  // 解析快捷键字符串
  function parseShortcut(shortcutStr) {
    const parts = shortcutStr.toLowerCase().split('+')
    return {
      altKey: parts.includes('alt') || parts.includes('option'),
      ctrlKey: parts.includes('ctrl') || parts.includes('control'),
      shiftKey: parts.includes('shift'),
      metaKey: parts.includes('meta') || parts.includes('cmd') || parts.includes('command'),
      keys: parts.filter(part => !['alt', 'ctrl', 'control', 'shift', 'meta', 'win', 'option', 'cmd', 'command'].includes(part)),
    }
  }

  // 修改设置界面创建函数
  function createSettingsUI(options) {
    const { shortcut, markdownShortcut, domainsToKeepParams, isBlacklistMode, onSave } = options

    const modalHTML = `
      <div class="modal-overlay-8W2Q7t">
        <div class="modal-content-1Zb3tL">
          <div class="modal-title-cKu8SM">设置</div>
          <div class="modal-description-KM3XGv">设置快捷键组合以快速复制页面标题和链接。按 ESC 键撤销更改。</div>
          <input type="text" id="els-shortcut" placeholder="按下快捷键组合" value="${shortcut}" readonly>
          <div class="modal-description-KM3XGv">设置快捷键组合以复制 Markdown 格式的链接:</div>
          <input type="text" id="els-markdown-shortcut" placeholder="按下快捷键组合" value="${markdownShortcut}" readonly>
          <div class="modal-description-KM3XGv">参数去除模式:</div>
          <div class="modal-checkbox-container-2Tgu5E">
            <input type="checkbox" id="els-blacklist-mode" ${isBlacklistMode ? 'checked' : ''}>
            <label for="els-blacklist-mode">黑名单模式(勾选后,列表中的域名复制时将不保留参数)</label>
          </div>
          <div class="modal-description-KM3XGv" id="els-domains-description-Cgt5uR">设置需要${isBlacklistMode ? '移除' : '保留'}参数的域名(每行一个):</div>
          <div class="modal-hint-8yFDJi" id="els-domains-hint-0DAupg">当前为${isBlacklistMode ? '黑名单' : '白名单'}模式。${isBlacklistMode ? '列表中的域名将不保留参数,其他域名将保留参数。' : '列表中的域名将保留参数,其他域名将不保留参数。'}</div>
          <textarea id="els-domains-7u6z9U">${domainsToKeepParams.join('\n')}</textarea>
          <div class="modal-buttons-L5xkyU">
           <button id="els-cancel-W3OUcP" class="cancel">取消</button>
           <button id="els-save-e8UfX6">保存</button>
          </div>
        </div>
      </div>
    `

    const modalContainer = document.createElement('div')
    modalContainer.innerHTML = modalHTML

    // 使用 top.document.body 而不是 document.body
    top.document.body.appendChild(modalContainer)

    const markdownShortcutInput = document.getElementById('els-markdown-shortcut')
    const shortcutInput = document.getElementById('els-shortcut')
    const domainsTextarea = document.getElementById('els-domains-7u6z9U')
    let listeningForShortcut = false
    let currentShortcut = []
    let currentMarkdownShortcut = []
    let listeningForMarkdownShortcut = false

    function resetShortcut() {
      shortcutInput.value = shortcut.toLowerCase()
      currentShortcut = shortcut.toLowerCase().split('+')
      listeningForShortcut = false
    }

    function resetMarkdownShortcut() {
      markdownShortcutInput.value = markdownShortcut.toLowerCase()
      currentMarkdownShortcut = markdownShortcut.toLowerCase().split('+')
      listeningForMarkdownShortcut = false
    }

    function closeSettings() {
      // 修改这里: 从 top.document.body 中移除
      top.document.body.removeChild(modalContainer)
      document.removeEventListener('keydown', escapeHandler)
      isModalOpen = false
    }

    function escapeHandler(e) {
      if (e.key === 'Escape') {
        resetShortcut()
        resetMarkdownShortcut()
        domainsTextarea.value = domainsToKeepParams.join('\n')
      }
    }

    document.addEventListener('keydown', escapeHandler)

    shortcutInput.addEventListener('focus', () => {
      if (listeningForMarkdownShortcut) {
        resetMarkdownShortcut()
      }
      listeningForShortcut = true
      currentShortcut = []
      shortcutInput.value = '按下快捷键组合...'
    })

    markdownShortcutInput.addEventListener('focus', () => {
      if (listeningForShortcut) {
        resetShortcut()
      }
      listeningForMarkdownShortcut = true
      currentMarkdownShortcut = []
      markdownShortcutInput.value = '按下快捷键组合...'
    })

    document.addEventListener('keydown', (e) => {
      if (listeningForShortcut || listeningForMarkdownShortcut) {
        e.preventDefault()
        if (e.code === 'Escape') {
          listeningForShortcut ? resetShortcut() : resetMarkdownShortcut()
          return
        }
        const currentArray = listeningForShortcut ? currentShortcut : currentMarkdownShortcut
        const inputElement = listeningForShortcut ? shortcutInput : markdownShortcutInput

        if (['AltLeft', 'AltRight', 'ControlLeft', 'ControlRight', 'ShiftLeft', 'ShiftRight', 'MetaLeft', 'MetaRight'].includes(e.code)) {
          const modifier = e.code.replace('Left', '').replace('Right', '').toLowerCase()
          if (!currentArray.includes(modifier)) {
            currentArray.push(modifier)
          }
        }
        else {
          const keyDisplay = getKeyDisplay(e.code)
          if (!currentArray.includes(keyDisplay)) {
            currentArray.push(keyDisplay)
          }
          listeningForShortcut = false
          listeningForMarkdownShortcut = false
        }
        inputElement.value = currentArray.join('+')
      }
    })

    document.addEventListener('keyup', (e) => {
      if ((listeningForShortcut || listeningForMarkdownShortcut) && ['alt', 'control', 'shift', 'meta'].includes(e.key.toLowerCase())) {
        listeningForShortcut = false
        listeningForMarkdownShortcut = false
      }
    })

    const blacklistModeCheckbox = document.getElementById('els-blacklist-mode')
    const domainsDescription = document.getElementById('els-domains-description-Cgt5uR')
    const domainsHint = document.getElementById('els-domains-hint-0DAupg')

    blacklistModeCheckbox.addEventListener('change', (e) => {
      const isBlacklist = e.target.checked
      domainsDescription.textContent = `设置需要${isBlacklist ? '移除' : '保留'}参数的域名(每行一个):`
      domainsHint.textContent = `当前为${isBlacklist ? '黑名单' : '白名单'}模式。${isBlacklist ? '列表中的域名将不保留参数,其他域名将保留参数。' : '列表中的域名将保留参数,其他域名将不保留参数。'}`
    })

    document.getElementById('els-save-e8UfX6').addEventListener('click', () => {
      const newShortcut = shortcutInput.value
      const newMarkdownShortcut = markdownShortcutInput.value
      const newDomains = domainsTextarea.value.split('\n').filter(d => d.trim() !== '')
      const newIsBlacklistMode = blacklistModeCheckbox.checked

      onSave({
        shortcut: newShortcut,
        markdownShortcut: newMarkdownShortcut,
        domainsToKeepParams: newDomains,
        isBlacklistMode: newIsBlacklistMode,
      })

      closeSettings()
    })

    document.getElementById('els-cancel-W3OUcP').addEventListener('click', closeSettings)

    return closeSettings
  }

  // 修改打开设置界面函数
  function openSettings() {
    isModalOpen = true
    createSettingsUI({
      shortcut,
      markdownShortcut,
      domainsToKeepParams,
      isBlacklistMode,
      onSave: (newSettings) => {
        shortcut = newSettings.shortcut
        markdownShortcut = newSettings.markdownShortcut
        domainsToKeepParams = newSettings.domainsToKeepParams
        isBlacklistMode = newSettings.isBlacklistMode
        GM_setValue('easyLinkShareShortcut', shortcut)
        GM_setValue('easyLinkShareMarkdownShortcut', markdownShortcut)
        GM_setValue('domainsToKeepParams', domainsToKeepParams)
        GM_setValue('isBlacklistMode', isBlacklistMode)
        isModalOpen = false
      },
    })
  }
  function getKeyDisplay(code) {
    const keyMap = {
      Digit1: '1',
      Digit2: '2',
      Digit3: '3',
      Digit4: '4',
      Digit5: '5',
      Digit6: '6',
      Digit7: '7',
      Digit8: '8',
      Digit9: '9',
      Digit0: '0',
      KeyA: 'a',
      KeyB: 'b',
      KeyC: 'c',
      KeyD: 'd',
      KeyE: 'e',
      KeyF: 'f',
      KeyG: 'g',
      KeyH: 'h',
      KeyI: 'i',
      KeyJ: 'j',
      KeyK: 'k',
      KeyL: 'l',
      KeyM: 'm',
      KeyN: 'n',
      KeyO: 'o',
      KeyP: 'p',
      KeyQ: 'q',
      KeyR: 'r',
      KeyS: 's',
      KeyT: 't',
      KeyU: 'u',
      KeyV: 'v',
      KeyW: 'w',
      KeyX: 'x',
      KeyY: 'y',
      KeyZ: 'z',
    }
    return keyMap[code] || code.toLowerCase()
  }

  function matchShortcut(e, shortcut) {
    return (e.altKey === shortcut.altKey
         && e.ctrlKey === shortcut.ctrlKey
         && e.shiftKey === shortcut.shiftKey
         && e.metaKey === shortcut.metaKey
         && shortcut.keys.every(key => getKeyDisplay(e.code) === key.toLowerCase()))
  }

  // 修改快捷键监听
  document.addEventListener('keydown', (e) => {
    // 如果模态框打开,不响应复制快捷键
    if (isModalOpen) { return }

    const normalShortcut = parseShortcut(shortcut)
    const mdShortcut = parseShortcut(markdownShortcut)

    if (matchShortcut(e, normalShortcut)) {
      e.preventDefault()
      copyLink(false)
    }
    else if (matchShortcut(e, mdShortcut)) {
      e.preventDefault()
      copyLink(true)
    }
  })

  // 注册油猴菜单命令
  GM_registerMenuCommand('设置', openSettings)
})()