Edge-Drop
一款零点击、悬停激活的桌面剪贴板面板及原生 OS 文件传输枢纽。
它隐身于屏幕左边缘。靠近它,面板便会展开。将任何内容拖拽出去——直接放入 Photoshop、Word、Slack、Explorer 等任何位置。
快速开始 ·
演示 ·
工作原理 ·
架构 ·
安全性 ·
路线图 ·
参与贡献
使用 Electron · React · TypeScript · Framer Motion · Zustand 构建
许可证:Apache-2.0 · 状态:公测版
## 为什么开发
市面上的每一款剪贴板管理器都会打断你的工作流。你复制内容、切换应用、粘贴,然后用方向键在 `Win+V` 历史记录中翻找,或者深入研究托盘菜单。步骤繁多、模态化、速度缓慢。
**Edge-Drop 消除了这些摩擦。** 它作为一个透明、置顶、点击穿透的界面,锚定在显示器的最左边缘像素处。当光标靠近边缘时,面板会弹出。将图片、文件堆栈、富文本和 HTML 包从中拖*出*——直接放入你正在使用的任何桌面应用中。无需快捷键。无需切换窗口。无需模态对话框。
它专为开发者和创意工作流而设计,在这些场景中,你需要同时在多个窗口间频繁处理截图、代码片段、文件路径、设计素材和参考链接。
## 演示
1. 欢迎使用 Edge-Drop
|
2. 收集任何内容
|
3. 拖拽至任意位置
|
4. 探索文件堆栈
|
5. 解除分组与拆分堆栈
|
6. 组合与合并项目
|
## 快速开始
### 前置条件
- **Node.js** v18 或更高版本
- **操作系统**:Windows 10/11(使用 Win32 OLE 拖拽管道和透明窗口光标轮询)
### 从源码运行
```
git clone https://github.com/Deepender25/Edge-Drop.git
cd Edge-Drop
npm install
npm run dev # launches Electron + Vite HMR
```
### 类型检查
```
npm run typecheck # runs tsc --noEmit against both node and web configs
```
### 构建 Windows 安装包
```
npm run package # outputs an NSIS .exe to /dist
```
## 工作原理
Edge-Drop 是一个 Electron 应用,分为三个严格隔离的进程——**Main**(Node.js,OS 访问)、**Preload**(类型化沙盒桥接)和 **Renderer**(React UI)。它们通过完全类型化的 IPC 契约进行通信。没有字符串通道名称,也没有 `any` payload。
### 不可见的边缘触发器
面板作为一个无边框、透明、点击穿透的 `BrowserWindow` 隐藏并锚定在 `x=0` 处。折叠时,**所有鼠标事件都会穿透到下方的应用**——你的桌面 100% 可用。检测在 Main 进程中通过 16ms 的 `screen.getCursorScreenPoint()` 轮询完成,因为 Windows 透明窗口会静默丢弃 `pointermove` 转发。
**死区滞回状态机**可防止光标在其边界附近悬停时面板闪烁打开/关闭:
| 阈值 | 值 | 含义 |
|---|---|---|
| 触发区 | `x ≤ 3px` | 左边缘的 3 像素条带启动 120ms 的停留计时器 |
| 保持开启 | `x ≤ 255px` | 光标明确位于面板内 → 取消任何关闭计时器 |
| 死区 | `255px < x ≤ 290px` | 此处的微小颤动将被忽略——不执行任何操作 |
| 开始关闭 | `x > 290px` | 光标明确位于外部 → 启动 250ms 的宽限计时器 |
正是这种细节,将一个“看起来不错”的演示与你真正可以日常使用的工具区分开来。
### 多格式剪贴板引擎
`ClipboardWatcher` 每 600ms 轮询一次 OS 剪贴板。为了在不每次重新编码图片的情况下检测*变化*,它计算了一个低成本的哈希签名:
- **文件** → 拼接的路径列表
- **文本** → 文本本身
- **图片** → **对原始 BGRA 位图约 400 个采样字节进行 FNV-1a 哈希**(维度 + 哈希值)
以前比较 `toPNG().length` 的简单方法既昂贵(每次都会重新编码整个图片)又有缺陷(两张复杂度相似但不同的 1920×1080 截图会产生相同的字节数 → 第二张被静默丢弃)。无论图片大小如何,FNV-1a 采样器的复杂度都是 O(400),并且发生碰撞的概率极低。
它还**尊重隐私标志**。来自密码管理器和听写工具的剪贴板格式——如 `ExcludeClipboardContentFromMonitorProcessing`、`ClipboardViewerIgnore`、`CanIncludeInClipboardHistory=0`、`KeePassClipFormat`、`com.bitwarden.concealed` 等——都会进行不区分大小写的匹配,并被完全跳过。
### 原生 OS 拖出 (OLE)
标准的 HTML5 拖拽事件无法将文件句柄传递给外部桌面软件。Edge-Drop 拦截 Renderer 的 `dragstart`,向 Main 进程发送即发即忘的 IPC(`item:start-drag`),Main 进程将项目内容暂存为临时文件,并调用 `webContents.startDrag({ file, icon })`。然后,OS 会渲染原生的拖拽幻影,并处理将其放入 Photoshop、Word、Explorer 或任何其他应用的过程——就像你从 Explorer 本身拖拽文件一样。
自定义拖拽图标是动态生成的:用于文件包的层叠卡片 PNG(带数量徽章)、用于文本的毛玻璃效果引用卡片、用于图片的真实图像缩略图。它们通过 `@resvg/resvg-js` 渲染,经过缓存,并在启动时进行预热,从而确保第一次拖拽瞬间完成。
### 智能去重、堆栈与合并
当你重新复制现有内容时,Edge-Drop 不会添加重复项——它将该条目提升到位置 0,增加其 `hitCount` 徽章,并刷新其时间戳。多文件拖入和多图片复制会自动分组为可展开的 3D 卡片堆栈(每个堆栈最多 10 个)。将任何项目卡片拖拽到另一个卡片上即可将它们合并为一个包;双击展开并将子项目拖拽到左边缘即可将其重新拆分出来。
## 架构
```
graph TD
subgraph OS ["Operating System"]
WinClip["Win32 System Clipboard"]
WinFS["Local Filesystem / AppData"]
OLE["OS OLE Drag & Drop Pipeline"]
end
subgraph Main ["Electron Main Process (Node.js)"]
ClipWatch["ClipboardWatcher — 600ms poll, FNV-1a signatures"]
Store["ItemStore — atomic JSON + per-image PNG files"]
Tray["System Tray & Native Menu"]
WinMgr["BrowserWindow + 16ms Cursor Edge Poller"]
end
subgraph Bridge ["Typed Preload Bridge (contextIsolation)"]
API["contextBridge → window.edge"]
end
subgraph Renderer ["React Renderer"]
Zustand["Zustand store"]
Hooks["useEdgeHover · useDragOut"]
UI["Panel · ItemList · Fluid Bundles"]
end
WinClip <-->|"polls every 600ms"| ClipWatch
ClipWatch -->|"state:items IPC"| API
Store <-->|"atomic JSON read/write"| WinFS
WinMgr <-->|"window:cursor-edge · window:set-interactive"| API
UI -->|"item:start-drag IPC"| API
API -->|"OLE native handle"| OLE
API <-->|"typed handlers & events"| Zustand
Zustand <--> UI
```
### 边缘触发状态机
```
stateDiagram-v2
[*] --> Closed: startup, x=0, click-through
state Closed {
[*] --> PollingEdge
PollingEdge --> TriggerZone: cursor x ≤ 3px in hot zone
TriggerZone --> DwellTimer: linger ≥ 120ms
}
state Open {
[*] --> Interactive: setInteractive(true)
Interactive --> HysteresisCheck: 16ms main poll
HysteresisCheck --> Interactive: x ≤ 255px (inside blade)
HysteresisCheck --> GraceTimer: x > 290px OR left y-bounds
GraceTimer --> Interactive: cursor returns within 250ms
}
Closed --> Open: dwell timer expires
Open --> Closed: grace timer expires
```
### 剪贴板捕获与去重 pipeline
```
sequenceDiagram
participant OS as System Clipboard
participant CW as ClipboardWatcher
participant IS as ItemStore (disk)
participant R as React UI
loop Every 600ms
CW->>OS: read available formats
alt Files (Win32 HDROP / FileNameW)
CW->>OS: PowerShell GetFileDropList (bypasses single-file limit)
else Image
CW->>OS: clipboard.readImage → raw BGRA
CW->>CW: FNV-1a hash over ~400 sampled bytes
else Text / URL / HTML
CW->>OS: clipboard.readText / readHTML
end
CW->>IS: lookup signature
alt Duplicate
IS->>IS: bump hitCount, move to front, update timestamp
else New
IS->>IS: prepend, enforce historyLimit, evict oldest unpinned
end
IS-->>R: broadcast state:items
end
```
### 原生拖出流程
```
sequenceDiagram
participant U as User
participant R as React Card
participant M as Electron Main
participant OS as OS / External App
Note over U,OS: Drag OUT — native OLE
U->>R: dragstart on item tile
R->>R: preventDefault (kill HTML5 ghost)
R->>M: send item:start-drag (fire & forget)
M->>M: stage content → temp file (.png / .txt / real paths)
M->>OS: webContents.startDrag({ file, icon })
OS->>U: native drag ghost → drop anywhere
Note over U,OS: Drag IN — file drop
U->>R: drag files onto panel edge
R->>M: webUtils.getPathForFile
M->>M: validate paths, create bundle, broadcast
```
## 功能
**零点击边缘悬停**
- 无边框、透明、置顶的 `BrowserWindow`,锚定在 `x=0`
- 折叠时 100% 点击穿透——桌面保持完全可用
- 16ms 的 Main 进程光标轮询(绕过有缺陷的 Windows 透明窗口 `pointermove` 转发)
- 可配置的热区高度(屏幕的 25% / 40% / 60%)和面板高度(40% – 100%)
**多格式剪贴板引擎**
- 捕获纯文本、URL、富 HTML、原始图片和多选文件
- 通过 PowerShell 解析 Win32 `FileNameW` / HDROP,绕过 Electron 的单文件限制
- 尊重密码管理器和听写工具的隐私标志(不区分大小写匹配)
- 智能去重——重新复制会累积 `hitCount` 并将条目移至顶部
- 隐身模式——一键暂停轮询以处理敏感数据
**原生 OS 拖放**
- `webContents.startDrag()` 将真实的文件句柄交给外部应用
- 自定义拖拽图标:带数量徽章的层叠卡片 PNG、毛玻璃效果文本卡片、真实图片缩略图
- 拖入:将文件拖放到面板上即可添加;拖出:可放置在任何位置——Photoshop、Word、Explorer、Slack
- 预热的图标缓存确保第一次拖拽瞬间完成
**流畅的集合与堆栈**
- 自动将多文件拖入和多图片复制分组为 3D 卡片堆栈(最多 10 个)
- 将一个卡片拖拽到另一个卡片上即可将它们合并为一个包
- 双击展开,将子项目拖拽到左边缘即可重新拆分出来
- 类型安全的合并规则:图片仅与图片合并,文件与文件合并(文本从不分组)
**UI / UX**
- 毛玻璃 macOS 美学——深黑色、`backdrop-filter: blur(20px)`、超细边框
- Framer Motion 弹簧物理效果,打开时具有同步的弹性过冲
- 随面板缩放的自定义 SVG 连接光晕
- 顶部和底部的滚动渐变遮罩,使项目淡入黑色背景
- 单色置顶 / 倍数徽章,最大程度提高可读性
- 为无障碍功能提供的减少动态效果设置
## 安全性
Edge-Drop 涉及 OS 剪贴板、文件系统和 Win32 OLE 拖拽管道——因此其安全设计是有意为之的,绝非可选项。
| 控制 | 实现 |
|---|---|
| 进程隔离 | 在两个窗口上均启用 `contextIsolation: true` · `nodeIntegration: false` · `sandbox: true` |
| 类型化 IPC | `shared/ipc.ts` 定义了 `InvokeMap`、`EventMap`、`SendMap`——通道名称和 payload 类型在两端均经过静态检查 |
| 注重隐私的剪贴板 | 遵循 `ExcludeClipboardContentFromMonitorProcessing`、`ClipboardViewerIgnore`、`CanIncludeInClipboardHistory`、`CanUploadToCloudClipboard`,以及 1Password / Bitwarden / KeePass 的隐藏格式 |
| 原子持久化 | JSON 索引通过临时文件 + 重命名写入;图片字节存储为按 id 命名的 PNG 文件 |
| 开发安全启动 | `app.setLoginItemSettings` 受 `app.isPackaged` 控制——开发构建永远不会触碰 Windows 注册表 |
| 外部链接 | `setWindowOpenHandler` 强制所有窗口打开请求通过 `shell.openExternal`——没有应用内导航 |
## 技术栈
| 层级 | 选择 | 原因 |
|---|---|---|
| 桌面 runtime | **Electron 30+** | 从 JS 访问 Win32 OLE 拖拽管道和原生剪贴板格式的唯一方法| 构建工具 | **electron-vite** | 使用 Vite HMR 的独立 Main / Preload / Renderer 构建 |
| UI | **React 18 + TypeScript** | 强类型的组件层次结构 |
| 动画 | **Framer Motion** | 弹簧物理效果、布局过渡、手势动画 |
| 状态 | **Zustand** | Selector 优化,在拖拽过程中零级联重新渲染 |
| 拖拽图标 | **@resvg/resvg-js** | 用于自定义拖拽幻影的服务器端 SVG → PNG 渲染 |
## 项目结构
```
Edge-Drop/
├─ shared/ Typed IPC contracts & domain models
│ ├─ types.ts ClipboardItem, Bundle, Settings, DragRequest DTOs
│ └─ ipc.ts InvokeMap / EventMap / SendMap channel definitions
├─ electron/ Node.js backend & OS integrations
│ ├─ main/
│ │ ├─ index.ts Single-instance lock, IPC registration, startup
│ │ ├─ window.ts Frameless window, setIgnoreMouseEvents, cursor poll
│ │ ├─ tray.ts System tray icon & context menus
│ │ └─ drag.ts OLE startDrag, temp-file staging, icon generation
│ ├─ preload/ Sandbox bridge exposing window.edge
│ ├─ clipboard/
│ │ ├─ ClipboardWatcher.ts 600ms poll loop, transient-copy rejection
│ │ └─ formats.ts FNV-1a signatures, Win32 HDROP, privacy-flag detection
│ └─ store/
│ ├─ ItemStore.ts Atomic JSON persistence, dedup, merge/split logic
│ ├─ settings.ts User config & startup registration
│ └─ paths.ts AppData + temp directory resolution
├─ src/ React renderer
│ ├─ components/ Panel, ItemList, ClipboardItem, SearchBar, Settings, Icons
│ ├─ hooks/ useEdgeHover (hysteresis), useDragOut, useFilteredItems
│ ├─ store/ Zustand appStore
│ ├─ lib/ Theme tokens, format helpers, file-type detection
│ └─ styles/ tokens.css, panel.css, settings.css, item.css, global.css
```
## 路线图
Edge-Drop 目前处于**公测阶段**。以下是按大致优先级排列的规划功能:
- [ ] **AI 语义自组织**——对文本/URL/HTML 条目进行 embedding 处理,自动聚类为命名组,取代手动置顶
- [ ] **AI 总结**——将多文件包和冗长的 HTML 副本浓缩为单行摘要和标签
- [ ] **多显示器支持**——锚定到任何显示器边缘,而不仅仅是主显示器
- [ ] **Linux 移植**——用跨平台的等效实现替换 Win32 特定路径
- [ ] **Plugin SDK**——允许用户编写自定义格式读取器和拖出目标
- [ ] **云同步(可选加入,E2E 加密)**——跨机器同步置顶项目
- [ ] **全历史记录搜索**——目前上限为 `historyLimit`(默认 500)
AI 功能是路线图的首要任务,也是本项目申请加入 OpenAI 的 **Codex for Open Source** 计划的原因。
### 开发工作流
```
npm install
npm run dev # Electron + Vite HMR
npm run typecheck # tsc --noEmit (node + web configs)
npm run package # build Windows NSIS installer to /dist
```
## 许可证
Apache License 2.0——详见 [LICENSE](LICENSE)。允许商业和非商业用途、修改和分发,只需注明出处。