为 Motrix 添加剪贴板监听功能(实战)

Motrix 添加剪贴板监听功能。这个实现分为几个关键部分:

1. 安装依赖

首先在 Motrix 项目目录安装必要的依赖:

npm install ffi-napi ref-napi ref-struct-napi clipboardy
# 或者使用 yarn
yarn add ffi-napi ref-napi ref-struct-napi clipboardy

2. 主进程代码修改

在 Motrix 的主进程文件(通常是 src/main/index.jssrc/main/index.ts)中添加以下代码:

// 导入必要的模块
import { BrowserWindow, clipboard } from 'electron'
import ffi from 'ffi-napi'
import ref from 'ref-napi'
import StructType from 'ref-struct-napi'

// 定义 Windows API 类型
const HWND = ref.refType(ref.types.void)
const BOOL = ref.types.bool
const UINT = ref.types.uint32
const WPARAM = ref.types.uint32
const LPARAM = ref.types.uint32
const LRESULT = ref.types.uint32

// Windows 消息常量
const WM_CLIPBOARDUPDATE = 0x031D

// 加载 user32.dll
const user32 = ffi.Library('user32.dll', {
  'AddClipboardFormatListener': [BOOL, [HWND]],
  'RemoveClipboardFormatListener': [BOOL, [HWND]],
  'GetClipboardSequenceNumber': [UINT, []]
})

class ClipboardMonitor {
  constructor(mainWindow) {
    this.mainWindow = mainWindow
    this.isMonitoring = false
    this.lastSequenceNumber = 0
    this.downloadUrlPatterns = [
      // HTTP/HTTPS/FTP 下载链接
      /https?:\/\/[^\s<>"']+\.(zip|rar|7z|exe|msi|dmg|apk|iso|tar\.gz|tar\.bz2|mp4|mp3|pdf)/i,
      // 磁力链接
      /magnet:\?xt=urn:[a-z0-9]+:[a-zA-Z0-9]+/i,
      // BT种子文件链接
      /http.+\/announce/i,
      // ED2K链接
      /ed2k:\/\/\|file\|[^\|]+\|/i,
      // Thunder链接
      /thunder:\/\/[a-zA-Z0-9]+/i,
      // 通用URL(如果有文件扩展名或常见下载参数)
      /https?:\/\/[^\s<>"']+\?.*\b(?:download|file|id)=[^&\s]+/i,
      // 包含常见文件扩展名的链接(即使没有协议前缀)
      /(?:https?:\/\/)?[^\s<>"']+\.(zip|rar|7z|exe|msi|dmg|apk|iso)(?:\?[^\s]*)?/i
    ]
  }

  // 开始监听
  start() {
    if (this.isMonitoring) return
    
    const hwnd = this.getWindowHandle()
    if (!hwnd) {
      console.error('无法获取窗口句柄')
      return
    }

    // 注册剪贴板监听
    const result = user32.AddClipboardFormatListener(hwnd)
    if (result) {
      this.isMonitoring = true
      console.log('剪贴板监听已启动')
      
      // 监听窗口消息
      this.setupMessageListener()
    } else {
      console.error('启动剪贴板监听失败')
    }
  }

  // 停止监听
  stop() {
    if (!this.isMonitoring) return
    
    const hwnd = this.getWindowHandle()
    if (hwnd) {
      user32.RemoveClipboardFormatListener(hwnd)
      this.isMonitoring = false
      console.log('剪贴板监听已停止')
    }
  }

  // 获取窗口原生句柄
  getWindowHandle() {
    if (!this.mainWindow || this.mainWindow.isDestroyed()) {
      return null
    }
    
    const handleBuffer = this.mainWindow.getNativeWindowHandle()
    if (handleBuffer) {
      // 将Buffer转换为指针
      return ref.address(handleBuffer)
    }
    return null
  }

  // 设置消息监听
  setupMessageListener() {
    if (!this.mainWindow || this.mainWindow.isDestroyed()) return

    // 监听窗口消息
    this.mainWindow.hookWindowMessage(WM_CLIPBOARDUPDATE, (wParam, lParam) => {
      this.handleClipboardUpdate()
    })
  }

  // 处理剪贴板更新
  handleClipboardUpdate() {
    try {
      // 获取剪贴板序列号,避免重复处理
      const currentSeq = user32.GetClipboardSequenceNumber()
      if (currentSeq === this.lastSequenceNumber) {
        return
      }
      this.lastSequenceNumber = currentSeq

      // 读取剪贴板文本
      const clipboardText = clipboard.readText()
      if (!clipboardText) return

      // 提取下载链接
      const downloadLinks = this.extractDownloadLinks(clipboardText)
      
      if (downloadLinks.length > 0) {
        this.showDownloadDialog(downloadLinks)
      }
    } catch (error) {
      console.error('处理剪贴板更新时出错:', error)
    }
  }

  // 提取下载链接
  extractDownloadLinks(text) {
    const links = new Set() // 使用Set去重
    
    // 按行分割文本
    const lines = text.split(/\r?\n/)
    
    for (const line of lines) {
      // 跳过空行
      if (!line.trim()) continue
      
      // 检查是否匹配下载链接模式
      for (const pattern of this.downloadUrlPatterns) {
        const matches = line.match(pattern)
        if (matches) {
          // 返回完整的匹配结果
          links.add(matches[0])
          break
        }
      }
    }
    
    return Array.from(links)
  }

  // 显示下载对话框
  showDownloadDialog(links) {
    if (links.length === 1) {
      // 单个链接,直接创建下载任务
      this.createDownloadTask(links[0])
    } else if (links.length > 1) {
      // 多个链接,询问用户
      const result = this.mainWindow.webContents.executeJavaScript(`
        new Promise((resolve) => {
          const links = ${JSON.stringify(links)}
          const message = '检测到 ' + links.length + ' 个下载链接:\\n\\n' + 
                         links.join('\\n') + '\\n\\n是否全部添加?'
          
          if (confirm(message)) {
            resolve('all')
          } else {
            resolve('single')
          }
        })
      `)
      
      result.then(choice => {
        if (choice === 'all') {
          links.forEach(link => this.createDownloadTask(link))
        } else {
          // 让用户选择其中一个
          this.mainWindow.webContents.executeJavaScript(`
            new Promise((resolve) => {
              const links = ${JSON.stringify(links)}
              let list = '请选择要下载的链接:\\n'
              links.forEach((link, index) => {
                list += (index + 1) + '. ' + link + '\\n'
              })
              const choice = prompt(list, '1')
              resolve(parseInt(choice) - 1)
            })
          `).then(index => {
            if (index >= 0 && index < links.length) {
              this.createDownloadTask(links[index])
            }
          })
        }
      })
    }
  }

  // 创建下载任务
  createDownloadTask(url) {
    if (!this.mainWindow || this.mainWindow.isDestroyed()) return
    
    // 发送给渲染进程创建下载任务
    this.mainWindow.webContents.send('create-download-task', {
      url: url,
      autoStart: true
    })
    
    // 显示窗口(如果最小化或隐藏)
    if (this.mainWindow.isMinimized()) {
      this.mainWindow.restore()
    }
    this.mainWindow.show()
    this.mainWindow.focus()
  }
}

// 在 Motrix 主窗口创建后初始化
function setupClipboardMonitor(mainWindow) {
  // 创建监听器实例
  const monitor = new ClipboardMonitor(mainWindow)
  
  // 启动监听
  monitor.start()
  
  // 监听窗口关闭事件
  mainWindow.on('closed', () => {
    monitor.stop()
  })
  
  return monitor
}

// 导出供 Motrix 主进程使用
export { setupClipboardMonitor, ClipboardMonitor }

3. 在 Motrix 主进程中集成

在 Motrix 的主进程入口文件(通常是 src/main/index.js)中找到创建主窗口的位置:

// 在创建 BrowserWindow 之后
function createMainWindow() {
  const mainWindow = new BrowserWindow({
    // ... 现有配置
  })
  
  // 设置剪贴板监听
  if (process.platform === 'win32') {
    // 延迟启动,确保窗口完全创建
    setTimeout(() => {
      const { setupClipboardMonitor } = require('./clipboard-monitor')
      setupClipboardMonitor(mainWindow)
    }, 1000)
  } else {
    // macOS 和 Linux 可以使用替代方案
    setupCrossPlatformClipboardMonitor(mainWindow)
  }
  
  return mainWindow
}

4. 跨平台兼容版本

对于 macOS 和 Linux,可以使用轮询方式实现简单的剪贴板监听:

// 跨平台剪贴板监听(轮询方式)
class CrossPlatformClipboardMonitor {
  constructor(mainWindow) {
    this.mainWindow = mainWindow
    this.lastText = ''
    this.interval = null
    this.checkInterval = 1000 // 每秒检查一次
    this.downloadUrlPatterns = [
      // ... 同样的正则表达式
    ]
  }
  
  start() {
    if (this.interval) return
    
    this.lastText = clipboard.readText()
    this.interval = setInterval(() => {
      this.checkClipboard()
    }, this.checkInterval)
    
    console.log('跨平台剪贴板监听已启动')
  }
  
  stop() {
    if (this.interval) {
      clearInterval(this.interval)
      this.interval = null
      console.log('跨平台剪贴板监听已停止')
    }
  }
  
  checkClipboard() {
    try {
      const currentText = clipboard.readText()
      if (currentText && currentText !== this.lastText) {
        this.lastText = currentText
        this.handleClipboardChange(currentText)
      }
    } catch (error) {
      console.error('检查剪贴板时出错:', error)
    }
  }
  
  handleClipboardChange(text) {
    // ... 同样的链接提取和下载逻辑
  }
}

5. 渲染进程处理

在渲染进程中(例如 src/renderer/components/ 中的某个组件),添加事件监听:

// 在渲染进程中监听创建下载任务事件
import { ipcRenderer } from 'electron'

export default {
  mounted() {
    ipcRenderer.on('create-download-task', this.handleNewDownloadTask)
  },
  
  beforeDestroy() {
    ipcRenderer.removeListener('create-download-task', this.handleNewDownloadTask)
  },
  
  methods: {
    handleNewDownloadTask(event, taskInfo) {
      // 调用 Motrix 现有的新建任务方法
      this.$store.dispatch('createTask', {
        url: taskInfo.url,
        // ... 其他选项
      })
      
      // 可选:显示通知
      this.$notify({
        title: '检测到下载链接',
        message: `已添加: ${taskInfo.url.substring(0, 50)}...`,
        type: 'success'
      })
    }
  }
}

6. 配置选项

最后,可以在 Motrix 的设置界面添加开关选项:

// 在设置状态管理中添加配置
const settings = {
  // ... 现有设置
  monitorClipboard: true,
  monitorClipboardInterval: 1000,
  autoCreateTask: true,
  showNotification: true
}

// 在设置界面添加复选框
// <el-checkbox v-model="settings.monitorClipboard">启用剪贴板监听</el-checkbox>
// <el-checkbox v-model="settings.autoCreateTask">自动创建下载任务</el-checkbox>

使用说明

  1. 将以上代码添加到 Motrix 源码对应位置
  2. 安装依赖后重新构建 Motrix
  3. 启动应用后,复制任何包含下载链接的文本,Motrix 会自动检测并弹出下载对话框

这个实现包含了以下特性:

  • Windows 原生监听(实时响应)
  • 跨平台备用方案(轮询)
  • 智能链接提取(支持多种链接格式)
  • 多链接处理(可选择全部下载或单个选择)
  • 配置选项(可开关)

需要我详细解释某个部分,或者帮你调整正则表达式来匹配更多下载链接类型吗?

暂无评论