Deepender25/Edge-Drop

GitHub: Deepender25/Edge-Drop

Edge-Drop 是一款隐藏在屏幕边缘、悬停即激活的桌面剪贴板管理面板,支持将剪贴内容以原生拖放方式直接传输到任意应用程序中。

Stars: 25 | Forks: 1

Edge-Drop Logo

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)。允许商业和非商业用途、修改和分发,只需注明出处。
标签:Electron, MITM代理, React, Syscalls, 剪贴板管理, 拖拽交互, 文件传输, 桌面效率工具, 自动化攻击, 跨应用共享