From 66d75919bb862dbbfa5030f4c5a5870ac475389f Mon Sep 17 00:00:00 2001 From: guanjihuan Date: Sun, 20 Sep 2026 23:49:24 +0800 Subject: [PATCH] update --- .gitignore | 2 + README.md | 2 +- main.js | 32 ++++++--- package.json | 9 +-- preload.js | 5 +- renderer/index.html | 20 +++--- renderer/renderer.js | 107 ++++++++++++++++++++++++---- renderer/styles.css | 165 +++++++++++++++++++++++++++++++++++++------ 8 files changed, 283 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index b947077..f48a701 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules/ dist/ +# 本地 UI 走查截图(开发产物,不入库) +ui-check/ diff --git a/README.md b/README.md index 6e118d9..ebfd903 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ npm run dist # 打包 NSIS 安装包到 dist/ └── README.md ``` -`npm run dist` 会用 `electron-builder` 走 NSIS 打包,产物落在 `dist/` 下(`快速粘贴 Setup x.y.z.exe` 安装包 + `win-unpacked/` 免安装目录),仓库已通过 `.gitignore` 忽略。 +`npm run dist` 会用 `electron-builder` 走 NSIS 打包,产物落在 `dist/` 下(`Quick Paste Setup x.y.z.exe` 安装包 + `win-unpacked/` 免安装目录),仓库已通过 `.gitignore` 忽略。 ## 许可证 diff --git a/main.js b/main.js index e36aa90..f780b51 100644 --- a/main.js +++ b/main.js @@ -98,13 +98,12 @@ const VK_CONTROL = 0x11; const VK_V = 0x56; const KEYEVENTF_KEYUP = 0x02; -const isDev = !app.isPackaged; // 注意:userData 路径依赖 app 初始化状态,在 whenReady 里再解析更稳妥 let userDataPath = null; // 构建标记:用户跑应用时看到这个 banner 就知道是含本次「快捷键自定义」的最新代码。 // 调整 settings 面板里的快捷键录制 / 恢复默认相关问题时同步修改此字符串。 -const BUILD_TAG = 'shortcut-customize-2026-09-16'; +const BUILD_TAG = 'shortcut-customize-2026-09-20'; console.log(`[quick-paste] build: ${BUILD_TAG}`); let mainWindow = null; @@ -126,7 +125,6 @@ let startedAtLogin = false; // 上次前台窗口句柄(用于粘贴前主动切回) let lastFgHwnd = null; let ourThreadId = 0; -const ourPid = process.pid; // 前台窗口轮询定时器 let fgTrackTimer = null; @@ -370,7 +368,7 @@ function createWindow() { minimizable: true, skipTaskbar: false, backgroundColor: '#08090A', - title: '快速粘贴', + title: 'Quick Paste', show: false, webPreferences: { preload: path.join(__dirname, 'preload.js'), @@ -434,7 +432,7 @@ function createTray() { } tray = new Tray(trayIcon); - tray.setToolTip('快速粘贴'); + tray.setToolTip('Quick Paste'); const menu = Menu.buildFromTemplate([ { label: '显示 / 隐藏', click: () => toggleWindow() }, @@ -564,9 +562,13 @@ function sendCtrlV() { console.error('sendCtrlV error:', e); } } else if (process.platform === 'darwin') { - spawn('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']); + // spawn 失败(命令不存在等)会以异步 'error' 事件抛出, + // 没有监听器会变成未捕获异常把主进程拉崩,这里必须兜底。 + spawn('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']) + .on('error', (e) => console.error('sendCtrlV spawn error:', e)); } else { - spawn('xdotool', ['key', 'ctrl+v']); + spawn('xdotool', ['key', 'ctrl+v']) + .on('error', (e) => console.error('sendCtrlV spawn error:', e)); } } @@ -757,7 +759,11 @@ ipcMain.handle('paste', (_, text, opts) => { return { ok: true, hasTarget: true }; }); -ipcMain.handle('window:hide', () => { if (mainWindow) mainWindow.hide(); }); +ipcMain.handle('window:hide', () => { + // 与其他 handler 保持一致的防护:退出路径上窗口可能已销毁, + // hide() 会抛 "Object has been destroyed" + if (mainWindow && !mainWindow.isDestroyed()) mainWindow.hide(); +}); // 切换「始终置顶」。渲染进程负责把偏好写进 data.settings(持久化), // 主进程只负责:1) 立即应用;2) 后续 showFloating / pasteByHiding 路径拿这个状态。 ipcMain.handle('window:setAlwaysOnTop', (_, on) => { @@ -904,6 +910,16 @@ ipcMain.handle('data:export', async () => { // 渲染层获取当前已注册的全局快捷键(设置面板首次打开时显示) ipcMain.handle('shortcuts:getGlobalToggle', () => currentGlobalShortcut); +// 录制新快捷键期间临时挂起 / 恢复当前全局快捷键: +// 用户想录入的组合如果恰好是「当前已注册的那个」(比如想重录 Ctrl+Shift+V), +// 按键会被系统级钩子截获去切换窗口,渲染层永远收不到 keydown,录制 UI 卡死。 +// 进入录制态时 unregisterAll 挂起,退出录制态(含取消 / 关闭设置面板)时恢复注册。 +// 极端情况(录制中进程崩溃)也只是本次会话没有全局快捷键,重启后 whenReady 会重新注册。 +ipcMain.handle('shortcuts:suspendGlobalToggle', () => { + try { globalShortcut.unregisterAll(); return true; } catch { return false; } +}); +ipcMain.handle('shortcuts:resumeGlobalToggle', () => applyGlobalShortcut(currentGlobalShortcut)); + // 修改全局快捷键:渲染层在设置面板里点「录制」拿到新组合键后调用。 // 1. 校验格式:必须是非空字符串且至少有一个 `+`(modifier + key 的最小形态) // 2. 落盘:复用 withOwnedSettings → saveData 链路,确保 windowBounds 跟着更新; diff --git a/package.json b/package.json index a0a0557..0739ca9 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "quick-paste", "version": "1.0.0", - "description": "悬浮快速粘贴工具 — 分组管理常用句子,点击即粘贴到当前焦点窗口", + "description": "Quick Paste — 悬浮粘贴工具,分组管理常用句子,点击即粘贴到当前焦点窗口", + "private": true, "main": "main.js", - "author": "", + "author": "关济寰", "license": "MIT", "scripts": { "start": "node ./scripts/start.js", @@ -19,7 +20,7 @@ }, "build": { "appId": "com.example.quickpaste", - "productName": "快速粘贴", + "productName": "Quick Paste", "files": [ "main.js", "preload.js", @@ -44,7 +45,7 @@ "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, - "shortcutName": "快速粘贴" + "shortcutName": "Quick Paste" } } } diff --git a/preload.js b/preload.js index 1fbc463..fc3f85e 100644 --- a/preload.js +++ b/preload.js @@ -47,4 +47,7 @@ contextBridge.exposeInMainWorld('api', { // 全局快捷键:读当前 / 改写并重注册(设置面板录制按钮调用) getGlobalToggle: () => ipcRenderer.invoke('shortcuts:getGlobalToggle'), setGlobalToggle: (acc) => ipcRenderer.invoke('shortcuts:setGlobalToggle', acc), -}); \ No newline at end of file + // 录制期间临时挂起 / 恢复全局快捷键,避免想录入的按键被系统钩子截获去切窗口 + suspendGlobalToggle: () => ipcRenderer.invoke('shortcuts:suspendGlobalToggle'), + resumeGlobalToggle: () => ipcRenderer.invoke('shortcuts:resumeGlobalToggle'), +}); diff --git a/renderer/index.html b/renderer/index.html index c80f934..c9c897b 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -4,7 +4,7 @@ - 快速粘贴 + Quick Paste @@ -122,9 +122,9 @@ - 快速粘贴 + Quick Paste
-
@@ -186,7 +186,9 @@ @@ -238,13 +240,13 @@ @@ -402,7 +404,7 @@
粘贴时隐藏悬浮窗
-
关闭后悬浮窗会立即重新显示
+
粘贴瞬间短暂隐藏,约 0.2 秒后自动重新显示