share-link-copy

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

// ==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)
})()