From b178c86ac7758808c507d94abbdaca1dbd8e1b52 Mon Sep 17 00:00:00 2001 From: guanjihuan Date: Sat, 12 Sep 2026 14:01:15 +0800 Subject: [PATCH] update --- .gitignore | 2 + README.md | 117 ++ icon.ico | Bin 0 -> 40571 bytes main.js | 907 ++++++++++ package-lock.json | 4126 ++++++++++++++++++++++++++++++++++++++++++ package.json | 50 + preload.js | 41 + renderer/index.html | 481 +++++ renderer/renderer.js | 1947 ++++++++++++++++++++ renderer/styles.css | 1939 ++++++++++++++++++++ scripts/start.js | 48 + 11 files changed, 9658 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 icon.ico create mode 100644 main.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 preload.js create mode 100644 renderer/index.html create mode 100644 renderer/renderer.js create mode 100644 renderer/styles.css create mode 100644 scripts/start.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0beb1b6 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Quick Paste + +桌面悬浮的剪贴板工具:按分组管理常用句子,点一下就粘贴到当前正在输入的窗口。 + +写代码、做客服、写文案时总有几句话要重复敲——问候、结束语、固定回复、常用代码片段。把它做成常驻桌面的悬浮窗,分组整理好,一键贴到任意正在输入的窗口。 + +- 不抢焦点,点完继续干活 +- 不挑场景,浏览器、记事本、Office、微信、QQ、IDE 都能用 + +## 快速开始 + +需要 Node.js 18+。 + +```bash +npm install +npm start # 启动 +npm run dist # 打包 NSIS 安装包到 dist/ +``` + +`npm start` 走的是 `scripts/start.js` 而不是直接 `electron`:Git Bash 等终端会把 `ELECTRON_RUN_AS_NODE=1` 注入子进程,让 Electron 退化成普通 Node.js 卡住。启动脚本会先清掉这个变量再 spawn。 + +## 主要功能 + +- 悬浮窗无边框、可拖拽;标题栏一键切换「始终置顶」 +- 分组管理:右键重命名 / 删除 / 清空,分组与短语都可拖动重排 +- 一键粘贴:点击 → 写剪贴板 → 自动 Ctrl+V +- 收藏 / 最近 / 使用计数;「最近」列表与使用计数都能在设置面板一键清空 +- 实时模糊搜索,命中片段高亮 +- 占位符:`{date}` `{time}` `{datetime}` `{weekday}` `{clipboard}`,编辑时实时预览;`\{xxx}` 反斜杠保留字面量 +- 明暗主题、窗口透明度(60–100%)、粘贴时是否隐藏悬浮窗 +- 侧栏可折叠(`Ctrl+B`) +- JSON 导入导出:整库替换 或按 ID 合并 +- 单实例锁 + +## 快捷键 + +弹窗打开时只响应 `Esc` / `Ctrl+Enter`;焦点在输入框时只响应 `Ctrl` / `Alt` 组合键和 `Esc`,普通字符不会触发。 + +| 类别 | 快捷键 | 作用 | +|---|---|---| +| 全局 | `Ctrl+Shift+V` | 唤出 / 收起悬浮窗 | +| 列表 | `↑` / `↓` | 上下移动选中项 | +| 列表 | `1` ~ `9` | 粘贴当前可见顺序的前 9 条 | +| 列表 | `Enter` | 粘贴选中的短语 | +| 列表 | `Alt+↑` / `Alt+↓` | 调整顺序 | +| 过滤 | `g` / `f` / `r` | 切换 全部 / 收藏 / 最近 | +| 过滤 | `/` 或 `Ctrl+F` | 聚焦搜索框 | +| 过滤 | `Esc` | 清空搜索 / 关闭弹窗 / 隐藏窗口 | +| 操作 | `Ctrl+N` / `Ctrl+G` | 新建短语 / 新建分组 | +| 操作 | `Ctrl+B` | 折叠 / 展开分组栏 | +| 操作 | `Ctrl+,` | 打开设置 | +| 操作 | `Ctrl+Enter` | 保存短语编辑 | + +## 占位符 + +| 标签 | 展开为 | +|---|---| +| `{date}` | 当前日期,如 `2026-09-11` | +| `{time}` | 当前时间,如 `14:32` | +| `{datetime}` | 日期 + 时间,如 `2026-09-11 14:32` | +| `{weekday}` | 中文星期,如 `周四` | +| `{clipboard}` | 当前剪贴板内容 | + +含 `{clipboard}` 的短语粘贴时会先快照原剪贴板再覆盖,避免读到自身写出去的内容。 + +想教别人用占位符时,写成 `\{date}` 会保留成字面量 `{date}`,不会被展开。 + +## 数据存储 + +| 系统 | 路径 | +|---|---| +| Windows | `%APPDATA%\quick-paste\data.json` | +| macOS | `~/Library/Application Support/quick-paste/data.json` | +| Linux | `~/.config/quick-paste/data.json` | + +设置面板 → 数据 → 「打开」一键定位。写盘采用「先写临时文件再 rename」的原子化策略,畸形 JSON 会自动尝试修复。 + +## 常见问题 + +**粘贴后窗口消失了?** +默认不会。走「隐藏 → 粘贴 → 重新显示」兜底有三种:识别不到目标窗口、开了「粘贴时隐藏悬浮窗」、目标窗口拒绝被拉到前台。200ms 后会自动回来,嫌慢改 `main.js` 的 `PASTE_RESTORE_DELAY_MS`。 + +**某些应用粘贴不上?** +部分 Electron / Chrome 内核应用为安全考虑忽略 SendKeys。普通应用都没问题。 + +**`Ctrl+Shift+V` 不起作用?** +可能被其它程序占用了。注册失败时主进程只会在终端打一行 warn,应用照常运行——改用托盘图标唤出即可。要换快捷键改 `main.js` 的 `GLOBAL_TOGGLE_SHORTCUT`。 + +**拖到副屏的窗口下次启动不见了?** +不会丢。启动时会按窗口中心点找最近的显示器,把窗口整体夹回该显示器的工作区;副屏拔掉 / 分辨率改了也能正常恢复。 + +**怎么完全退出?** +关闭按钮只隐藏到托盘。真正退出请右键托盘图标 → 退出。 + +## 项目结构 + +``` +. +├── package.json +├── main.js # 主进程:窗口 / 托盘 / 模拟粘贴 / 单实例锁 / IPC +├── preload.js # contextBridge 暴露 window.api +├── renderer/ +│ ├── index.html +│ ├── styles.css +│ └── renderer.js +├── scripts/ +│ └── start.js # 清掉 ELECTRON_RUN_AS_NODE 后启动 Electron +├── icon.ico # 托盘 / 标题栏 / 安装包共用 +├── .gitignore # 忽略 node_modules/ 与 dist/ +└── README.md +``` + +`npm run dist` 会用 `electron-builder` 走 NSIS 打包,产物落在 `dist/` 下(`快速粘贴 Setup x.y.z.exe` 安装包 + `win-unpacked/` 免安装目录),仓库已通过 `.gitignore` 忽略。 + +## 许可证 + +MIT diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4bae4d4259505bbb822ced34fa7f10123f0cbef5 GIT binary patch literal 40571 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 T`I;EBUV`CjvLyQdT}b{1arL%m literal 0 HcmV?d00001 diff --git a/main.js b/main.js new file mode 100644 index 0000000..a6f1eb5 --- /dev/null +++ b/main.js @@ -0,0 +1,907 @@ +/** + * 主进程:创建悬浮窗口、托盘、IPC 处理、用 Windows API 立即触发粘贴 + */ +// 必须在 require('electron') 之前执行! +// 某些环境(如 npm bash)会把 ELECTRON_RUN_AS_NODE=1 设进环境, +// 这会让 Electron 退化成普通 Node.js,无法访问 electron API。 +delete process.env.ELECTRON_RUN_AS_NODE; + +const { + app, + BrowserWindow, + Tray, + Menu, + ipcMain, + clipboard, + dialog, + screen, + nativeImage, + shell, + globalShortcut, +} = require('electron'); +const path = require('path'); +const fs = require('fs'); +const crypto = require('crypto'); +const { spawn } = require('child_process'); + +// 必须发生在 app.whenReady 之前:通过开关抑制一些无害但刷屏的 Chromium 警告 +// (GPU 进程退出、Network service 重启等)。 +app.commandLine.appendSwitch('disable-gpu-shader-disk-cache'); +app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion'); + +// ==================== 单实例锁 ==================== +// 本应用「关闭窗口 = 隐藏到托盘」,用户很容易忘记它还在后台跑, +// 于是又去双击一次桌面快捷方式。没有这把锁的话会起第二个实例,后果是: +// 1. 托盘里出现两个图标,点哪个都只控制其中一半; +// 2. 两个实例各自持有整份数据的内存副本,都会往同一个 data.json 写 —— +// 后保存的那个会把先保存的改动整段覆盖掉,用户新加的短语凭空消失; +// 3. Ctrl+Shift+V 全局快捷键只有先启动的那个能注册成功。 +// 拿不到锁就直接退出,并让已经在跑的那个实例把窗口亮出来。 +// +// 注意:CommonJS 模块被包在函数里,这里的顶层 return 在 CJS 下也是合法的, +// 但 ESM 化(package.json 加 "type": "module" 或改 .mjs)后会立刻崩。 +// 改用 process.exit 更稳,跨模块系统都安全。 +const gotSingleInstanceLock = app.requestSingleInstanceLock(); +if (!gotSingleInstanceLock) { + app.quit(); + process.exit(0); +} +app.on('second-instance', () => { + // 第二个实例启动了 → 把我们的悬浮窗唤出来,等价于用户点了托盘图标 + if (mainWindow && !mainWindow.isDestroyed()) { + showFloating(); + } +}); + +// 必须发生在 app.whenReady 之前:把 Electron 的缓存目录显式指向用户数据里的子目录, +// 避免每次启动时它去尝试「迁移默认缓存到 userData」——Windows 上迁移失败会刷一堆 +// cache_util_win.cc ERROR,虽然不影响功能但日志很难看。 +try { + app.setPath('cache', path.join(app.getPath('appData'), 'quick-paste-cache')); +} catch (e) { + console.warn('set cache path failed:', e); +} + +// 仅在 Windows 上引入 koffi 调用系统 DLL +let koffi = null; +let user32 = null; +let kernel32 = null; +let SetForegroundWindow = null; +let GetForegroundWindow = null; +let GetWindowThreadProcessId = null; +let AttachThreadInput = null; +let GetCurrentThreadId = null; +let IsWindow = null; +let keybd_event = null; + +if (process.platform === 'win32') { + try { + koffi = require('koffi'); + user32 = koffi.load('user32.dll'); + kernel32 = koffi.load('kernel32.dll'); + + SetForegroundWindow = user32.func('SetForegroundWindow', 'bool', ['void*']); + GetForegroundWindow = user32.func('GetForegroundWindow', 'void*', []); + GetWindowThreadProcessId = user32.func('GetWindowThreadProcessId', 'uint32', ['void*', 'void*']); + AttachThreadInput = user32.func('AttachThreadInput', 'bool', ['uint32', 'uint32', 'bool']); + IsWindow = user32.func('IsWindow', 'bool', ['void*']); + keybd_event = user32.func('keybd_event', 'void', ['uint8', 'uint8', 'uint32', 'intptr']); + + // GetCurrentThreadId 在 kernel32.dll + GetCurrentThreadId = kernel32.func('GetCurrentThreadId', 'uint32', []); + } catch (e) { + console.error('koffi load failed:', e); + } +} + +const VK_CONTROL = 0x11; +const VK_V = 0x56; +const KEYEVENTF_KEYUP = 0x02; + +const isDev = !app.isPackaged; +// 注意:userData 路径依赖 app 初始化状态,在 whenReady 里再解析更稳妥 +let userDataPath = null; + +// 构建标记:用户跑应用时看到这个 banner 就知道是含本次搜索框修复的最新代码。 +// 调整搜索框「无法输入」相关问题时同步修改此字符串。 +const BUILD_TAG = 'always-on-top-toggle-2026-09-11'; +console.log(`[quick-paste] build: ${BUILD_TAG}`); + +let mainWindow = null; +let tray = null; +let isQuitting = false; + +// 始终置顶开关:默认开启(这是悬浮粘贴工具的核心体验)。 +// 主进程持有「真相源」:窗口本身可能已被用户从 Windows 任务栏右键菜单里改过, +// 但渲染进程保存的偏好才是用户期望的,启动时用它覆盖回去。 +let alwaysOnTop = true; + +// 上次前台窗口句柄(用于粘贴前主动切回) +let lastFgHwnd = null; +let ourThreadId = 0; +const ourPid = process.pid; +// 前台窗口轮询定时器 +let fgTrackTimer = null; + +/* ==================== 数据持久化 ==================== */ +const DATA_SCHEMA_VERSION = 1; +// 短语内容上限:100KB,挡住手抖粘贴整个文件 / 滥用脚本导致剪贴板异常 +const MAX_PHRASE_CONTENT_LENGTH = 100 * 1024; + +// 只读窗口位置(不参与完整数据校验,启动早期就要拿到) +function readSavedBounds() { + try { + if (!userDataPath || !fs.existsSync(userDataPath)) return null; + const raw = fs.readFileSync(userDataPath, 'utf-8'); + const data = JSON.parse(raw); + const b = data && data.settings && data.settings.windowBounds; + if ( + b && Number.isFinite(b.x) && Number.isFinite(b.y) && + Number.isFinite(b.width) && Number.isFinite(b.height) + ) { + // 多显示器:找到窗口中心点最近的显示器,把窗口整体夹回它的工作区。 + // 副屏拔了、分辨率改了导致窗口跑到屏幕外的情况都不会丢。 + const displays = screen.getAllDisplays(); + const cx = b.x + b.width / 2; + const cy = b.y + b.height / 2; + const nearest = screen.getDisplayNearestPoint({ x: cx, y: cy }) || displays[0]; + const wa = nearest.workArea; + const width = Math.max(280, Math.min(b.width, wa.width, 800)); + const height = Math.max(360, Math.min(b.height, wa.height, 900)); + // 关键:x/y 必须保证窗口完整落在 wa 内,否则用户找不到窗口 + const x = Math.max(wa.x, Math.min(b.x, wa.x + wa.width - width)); + const y = Math.max(wa.y, Math.min(b.y, wa.y + wa.height - height)); + return { x, y, width, height }; + } + } catch {} + return null; +} + +function uid(prefix = 'id') { + // 8 字节随机熵 ≈ 1.8e19 组合,比原来 4 字符 (~65K) 几乎不可能碰撞 + return `${prefix}_${Date.now().toString(36)}_${crypto.randomBytes(8).toString('hex')}`; +} + +function getDefaultData() { + return { + version: DATA_SCHEMA_VERSION, + groups: [ + { + id: 'grp_default', + name: '常用', + phrases: [ + { id: 'phr_1', title: '你好', content: '你好,很高兴认识你!', fav: false, useCount: 0 }, + { id: 'phr_2', title: '收到', content: '收到,我会尽快处理。', fav: false, useCount: 0 }, + ], + }, + { + id: 'grp_work', + name: '工作', + phrases: [ + { id: 'phr_3', title: '开会通知', content: '各位同事,今天下午 3 点在会议室 A 开会,请准时参加。', fav: false, useCount: 0 }, + ], + }, + ], + activeGroupId: 'grp_default', + recent: [], + settings: {}, + }; +} + +function loadData() { + try { + if (fs.existsSync(userDataPath)) { + const raw = fs.readFileSync(userDataPath, 'utf-8'); + const data = JSON.parse(raw); + if (data && Array.isArray(data.groups)) { + // 容错:每条 group 必须有 id/name/phrases 数组,过滤掉畸形项 + const cleaned = data.groups + .filter((g) => g && typeof g === 'object') + .map((g) => ({ + id: typeof g.id === 'string' ? g.id : uid('grp'), + name: typeof g.name === 'string' ? g.name : '未命名分组', + phrases: Array.isArray(g.phrases) + ? g.phrases + .filter((p) => p && typeof p === 'object' && typeof p.content === 'string') + .map((p) => ({ + id: typeof p.id === 'string' ? p.id : uid('phr'), + title: typeof p.title === 'string' ? p.title : '', + // 截断异常大的内容(防手抖 / 防被外部脚本注入巨型数据) + content: p.content.slice(0, MAX_PHRASE_CONTENT_LENGTH), + // 收藏标志。旧数据只有 pinned,这里一次性迁移成 fav。 + // 迁移后必须丢弃 pinned:否则 fav 已经被用户关掉、pinned 还留着 true 时, + // 下次启动这个 `p.fav || p.pinned` 会把收藏又打开 —— 取消收藏永远存不住。 + fav: typeof p.fav === 'boolean' ? p.fav : !!p.pinned, + // 使用次数(被粘贴/复制过的次数,统计用) + useCount: typeof p.useCount === 'number' && p.useCount >= 0 ? p.useCount : 0, + })) + : [], + })); + // 空分组必须保留!之前这里会把 phrases 为空的分组过滤掉, + // 于是「新建分组」「清空短语」之后一重启,那个分组就凭空消失了。 + // 只有整份数据连一个分组都没有时才回落到默认数据。 + const groups = cleaned.length > 0 ? cleaned : getDefaultData().groups; + // 最近使用列表:渲染进程维护的 [{id, ts}] + const recent = Array.isArray(data.recent) + ? data.recent + .filter((r) => r && typeof r === 'object' && typeof r.id === 'string') + .slice(0, 50) + : []; + return { + version: DATA_SCHEMA_VERSION, + groups, + activeGroupId: + typeof data.activeGroupId === 'string' && + groups.some((g) => g.id === data.activeGroupId) + ? data.activeGroupId + : groups[0].id, + recent, + // 透传 settings(如 windowBounds),让用户在主进程直接读写它 + settings: data.settings && typeof data.settings === 'object' ? data.settings : {}, + }; + } + } + } catch (e) { + console.error('load data error:', e); + } + return getDefaultData(); +} + +// 原子化写盘:先写临时文件再 rename,避免崩溃时留下损坏的 data.json +// 返回 { ok, error }:渲染端能区分成功 / 失败,把失败 toast 出来 +function saveData(data) { + try { + fs.mkdirSync(path.dirname(userDataPath), { recursive: true }); + // 注入版本号;后续做迁移时按这个字段分支 + const payload = JSON.stringify({ ...data, version: DATA_SCHEMA_VERSION }, null, 2); + const tmp = userDataPath + '.tmp'; + fs.writeFileSync(tmp, payload, 'utf-8'); + fs.renameSync(tmp, userDataPath); + return { ok: true }; + } catch (e) { + console.error('save data error:', e); + return { ok: false, error: e.message || String(e) }; + } +} + +/* ==================== 窗口创建 ==================== */ +// 把 alwaysOnTop 的「真相源」应用到窗口上:不存在时直接 return。 +// 注意:必须保留旧的 setAlwaysOnTop(true, 'floating') 默认行为路径, +// showFloating / pasteByHiding 仍然依赖它把窗口拉回顶层。 +function applyAlwaysOnTop() { + if (!mainWindow || mainWindow.isDestroyed()) return; + try { + // 'floating' 是 Windows 上的「在最前面」层级,比 normal top 更高, + // 大多数全屏应用都能压住它。 + mainWindow.setAlwaysOnTop(alwaysOnTop, 'floating'); + } catch (e) { + console.error('applyAlwaysOnTop error:', e); + } +} + +// 窗口位置记忆:用户拖到哪里下次就在哪里启动 +// 写盘是 debounce 的(300ms)避免拖动过程刷文件 +// +// Bug 修复:原实现 `loadData() → 修改 → saveData()` 会从盘上读全量数据再合并窗口位置。 +// 如果渲染层在这 300ms 窗口内也调了 data:save 并完成落盘,主进程后续写盘会把磁盘 +// 上那份更新过的数据(用户最新的短语改动)覆盖掉,造成数据丢失。 +// 改为:缓存最近一次由 data:save 写入的完整数据快照,更新窗口位置时直接复用这份 +// 快照,绝不再从盘读。 +let cachedDataSnapshot = null; +let saveBoundsTimer = null; +function scheduleSaveBounds() { + if (saveBoundsTimer) clearTimeout(saveBoundsTimer); + saveBoundsTimer = setTimeout(() => { + saveBoundsTimer = null; + if (!mainWindow || mainWindow.isDestroyed()) return; + try { + const b = mainWindow.getBounds(); + // 优先用最近一次 data:save 传入的快照;快照不存在(应用刚启动、还没 + // 任何渲染层写入)时再退回到读盘路径。 + if (cachedDataSnapshot) { + cachedDataSnapshot.settings = { + ...(cachedDataSnapshot.settings || {}), + windowBounds: b, + }; + saveData(cachedDataSnapshot); + } else { + const data = loadData(); + data.settings = { ...(data.settings || {}), windowBounds: b }; + saveData(data); + cachedDataSnapshot = data; + } + } catch (e) { + console.error('save bounds error:', e); + } + }, 300); +} + +function createWindow() { + const display = screen.getPrimaryDisplay(); + const { width: sw, height: sh } = display.workAreaSize; + // 优先用上次保存的位置/大小;首启动或位置跑到屏幕外时回落默认 + const saved = readSavedBounds(); + const bounds = saved || { + width: 380, + height: 560, + x: Math.max(0, sw - 400), + y: 80, + }; + + // 启动时把磁盘上的 alwaysOnTop 偏好读进来; + // 旧数据没这个字段时 loadData 已经容错处理,settings 至少是 {},所以取默认值 true 即可。 + try { + const persisted = loadData(); + const savedAlways = persisted && persisted.settings && persisted.settings.alwaysOnTop; + alwaysOnTop = savedAlways === undefined ? true : !!savedAlways; + } catch { + alwaysOnTop = true; + } + + mainWindow = new BrowserWindow({ + ...bounds, + minWidth: 280, + minHeight: 360, + // 上限:避免被拖成全屏把内容扯稀烂;长内容让用户自己加大字号或滚动 + maxWidth: 800, + maxHeight: 900, + // 任务栏 / Alt-Tab 缩略图显示的图标。ico 内置 16/32/48 等多尺寸, + // Windows 会按场景自动挑合适的;Linux/macOS 下 Electron 也会自动转换。 + icon: path.join(__dirname, 'icon.ico'), + frame: false, + // 初始值用 alwaysOnTop 偏好的临时快照(之前的临时快照就是模块级变量本身, + // 这里直接读取即可);setAlwaysOnTop 由 applyAlwaysOnTop() 统一收口。 + alwaysOnTop, + resizable: true, + maximizable: false, + minimizable: true, + skipTaskbar: false, + backgroundColor: '#08090A', + title: '快速粘贴', + show: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + }); + + applyAlwaysOnTop(); + mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html')); + + // 窗口保持可聚焦(这样搜索框能直接接收键盘事件),但显示用 showInactive 不抢外部焦点。 + // 旧的 setNoActivate(true) 会让整窗口 setFocusable(false),搜索框也跟着收不到按键, + // 那是无法接受的硬 bug。点短语走 hide → paste → show 路径,不依赖 noActivate 模式。 + mainWindow.setFocusable(true); + + // 拖动 / 拉伸后记住位置 + mainWindow.on('move', scheduleSaveBounds); + mainWindow.on('resize', scheduleSaveBounds); + + mainWindow.once('ready-to-show', () => { + showFloating(); + }); + + // 关闭按钮 → 最小化到托盘,而不是退出 + mainWindow.on('close', (e) => { + if (!isQuitting) { + e.preventDefault(); + mainWindow.hide(); + } + }); +} + +// 显示悬浮窗:用 showInactive 避免把焦点从目标输入框抢走。 +// 窗口本身保持 setFocusable(true)(见 createWindow),让搜索框能直接接收键盘事件。 +// 弹窗(编辑短语/新建分组等)需要主动接管键盘,仍由 setTyping 调用 mainWindow.focus()。 +function showFloating() { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.showInactive(); + // showInactive 之后层级有时会被 Windows 重置;重新应用一次置顶偏好 + // 否则用户关掉置顶 → 隐藏 → 再唤起时窗口可能"复活"为置顶态。 + applyAlwaysOnTop(); +} + +/* ==================== 托盘 ==================== */ +// 托盘图标使用项目根目录的 icon.ico:它内置了 16/32 等多种尺寸, +// Windows 任务栏托盘会自动挑选最合适的那一档显示。 +// 原 fallback 路径(assets/tray.png)不再维护 —— 始终从 icon.ico 加载。 +const TRAY_ICON_PATH = path.join(__dirname, 'icon.ico'); + +function createTray() { + let trayIcon; + if (fs.existsSync(TRAY_ICON_PATH)) { + trayIcon = nativeImage.createFromPath(TRAY_ICON_PATH); + } else { + // 极端兜底:icon.ico 不在时给个透明占位,避免 Tray 构造抛错把托盘拉崩 + trayIcon = nativeImage.createEmpty(); + } + + tray = new Tray(trayIcon); + tray.setToolTip('快速粘贴'); + + const menu = Menu.buildFromTemplate([ + { label: '显示 / 隐藏', click: () => toggleWindow() }, + { type: 'separator' }, + { + label: '退出', + click: () => { + isQuitting = true; + app.quit(); + }, + }, + ]); + tray.setContextMenu(menu); + tray.on('click', () => toggleWindow()); +} + +function toggleWindow() { + // isVisible()/hide()/show() 都会抛 "Object has been destroyed" + // 在 quit 路径下(isQuitting=true、will-quit 还没反注册全局快捷键之前)可能撞上, + // 这里显式拦一下,避免未捕获异常把渲染 / 托盘拉崩。 + if (!mainWindow || mainWindow.isDestroyed()) return; + if (mainWindow.isVisible()) { + mainWindow.hide(); + } else { + showFloating(); + } +} + +/* ==================== 窗口句柄工具 ==================== */ +// koffi 返回的 HWND 是 External 指针对象,Electron 的 getNativeWindowHandle() 返回 Buffer, +// 两者直接用 !== 比较永远不相等 → 必须统一换算成数值地址再比。 +function hwndAddr(hwnd) { + if (!hwnd || !koffi) return 0n; + try { + return BigInt(koffi.address(hwnd)); + } catch { + return 0n; + } +} + +// 本进程所有窗口的句柄地址(兜底用) +// 注意:用 process.arch 判断位数比看 Buffer.length 更稳—— +// Electron 在不同版本上 getNativeWindowHandle 返回 Buffer 的长度有时不完全等于本机指针宽度 +const IS_64BIT = process.arch === 'x64' || process.arch === 'arm64'; +function ownWindowAddrs() { + const set = new Set(); + for (const win of BrowserWindow.getAllWindows()) { + if (win.isDestroyed()) continue; + try { + const buf = win.getNativeWindowHandle(); + set.add(IS_64BIT ? buf.readBigUInt64LE(0) : BigInt(buf.readUInt32LE(0))); + } catch {} + } + return set; +} + +// 这个窗口是不是我们自己的(悬浮窗、弹窗等) +// 直接走 ownWindowAddrs —— BrowserWindow.getNativeWindowHandle() 是 Electron 自己的稳定 API, +// 比用 koffi `out()` 拿 PID 简单(koffi 在不同版本下 out 参数的回写行为不一致)。 +// 120ms 一次的轮询 + 遍历 1~3 个 BrowserWindow 完全可以忽略不计。 +function isOwnWindow(hwnd) { + return ownWindowAddrs().has(hwndAddr(hwnd)); +} + +/* ==================== 前台窗口跟踪 ==================== */ +function startForegroundTracking() { + if (process.platform !== 'win32' || !GetForegroundWindow) return; + ourThreadId = GetCurrentThreadId(); + + // 每 120ms 记录一次前台窗口,只记「不属于本应用」的窗口。 + // 用户点悬浮窗时保持上一次的目标不变,这样连点多条短语都会粘到同一个输入框。 + fgTrackTimer = setInterval(() => { + try { + const hwnd = GetForegroundWindow(); + if (!hwnd || hwndAddr(hwnd) === 0n) return; + if (isOwnWindow(hwnd)) return; + lastFgHwnd = hwnd; + } catch {} + }, 120); +} + +// 取当前可用的粘贴目标(窗口还在、且不是我们自己) +function getPasteTarget() { + if (!lastFgHwnd || !IsWindow) return null; + try { + if (!IsWindow(lastFgHwnd)) return null; + if (isOwnWindow(lastFgHwnd)) return null; + } catch { + return null; + } + return lastFgHwnd; +} + +// 把目标窗口拉回前台,返回是否成功 +function focusTarget(hwnd) { + try { + // 已经在前台(悬浮窗不抢焦点时的常态)→ 什么都不用做 + if (hwndAddr(GetForegroundWindow()) === hwndAddr(hwnd)) return true; + + const targetTid = GetWindowThreadProcessId(hwnd, null); + if (!targetTid) return false; + if (targetTid === ourThreadId) return true; + + // 临时附加输入线程,绕过前台窗口锁定 + AttachThreadInput(ourThreadId, targetTid, true); + try { + SetForegroundWindow(hwnd); + } finally { + AttachThreadInput(ourThreadId, targetTid, false); + } + return hwndAddr(GetForegroundWindow()) === hwndAddr(hwnd); + } catch (e) { + console.error('focusTarget error:', e); + return false; + } +} + +/* ==================== 立即触发 Ctrl+V ==================== */ +function sendCtrlV() { + if (process.platform === 'win32' && keybd_event) { + try { + keybd_event(VK_CONTROL, 0, 0, 0); + keybd_event(VK_V, 0, 0, 0); + keybd_event(VK_V, 0, KEYEVENTF_KEYUP, 0); + keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0); + } catch (e) { + console.error('sendCtrlV error:', e); + } + } else if (process.platform === 'darwin') { + spawn('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']); + } else { + spawn('xdotool', ['key', 'ctrl+v']); + } +} + +// 兜底方案:不知道该粘到哪 → 隐藏自己把焦点让给下面的窗口,粘完再无焦点显示回来 +// 拆成两个有明确语义的常量: +// PASTE_HIDE_DELAY_MS:hide → keybd_event 之间的离场时间,避免 Ctrl+V 误发给「正在 +// 隐藏中」的本窗口(hide 是异步的,立即 sendCtrlV 可能撞上) +// PASTE_RESTORE_DELAY_MS:sendCtrlV → showInactive 之间让目标应用接收并真正把 +// 剪贴板内容落到输入框。覆盖多数应用从切前台 → 输入框就绪的时间 +const PASTE_HIDE_DELAY_MS = 50; +const PASTE_RESTORE_DELAY_MS = 200; +// 用单 timer 取代「每次都 setTimeout」:连点多条短语时多个回显计时器会互相抢着触发, +// 最后一个回显完成之前其它都会执行 → 窗口一闪一闪。把后续请求合并到同一个 timer, +// 只回显一次即可。 +let pasteHideTimer = null; +let pasteRestoreTimer = null; +function pasteByHiding() { + if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()) { + mainWindow.hide(); + } + // 清掉前一轮还没触发的回显任务(窗口已经隐藏,再回调一次也是同一个状态) + if (pasteRestoreTimer) { clearTimeout(pasteRestoreTimer); pasteRestoreTimer = null; } + if (pasteHideTimer) clearTimeout(pasteHideTimer); + pasteHideTimer = setTimeout(() => { + pasteHideTimer = null; + sendCtrlV(); + pasteRestoreTimer = setTimeout(() => { + pasteRestoreTimer = null; + if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isVisible()) { + mainWindow.showInactive(); + // 同 showFloating:showInactive 会重置层级,必须按当前偏好重新应用 + applyAlwaysOnTop(); + } + }, PASTE_RESTORE_DELAY_MS - PASTE_HIDE_DELAY_MS); + }, PASTE_HIDE_DELAY_MS); +} + +/* ==================== 占位符展开 ==================== */ +// 在粘贴/复制前把 {date} / {time} / {datetime} / {weekday} / {clipboard} 替换成实际值。 +// 在主进程做是为了 `{clipboard}` 能可靠地读到系统剪贴板(避免渲染层权限差异)。 +// 调用方需自己先快照原剪贴板内容(防止被自身的 writeText 覆盖)。 +const WEEKDAY_CN = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; +function expandPlaceholders(text, clipboardSnapshot = '') { + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + const replacements = { + date: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`, + time: `${pad(now.getHours())}:${pad(now.getMinutes())}`, + weekday: WEEKDAY_CN[now.getDay()], + clipboard: clipboardSnapshot, + }; + replacements.datetime = `${replacements.date} ${replacements.time}`; + // 单次正则遍历替代 5 次 replace。支持 \{xxx\} 反斜杠转义—— + // 想在短语里写「{date}」字面量教别人用占位符时,加一个反斜杠就行。 + return text.replace( + /\\?\{(date|time|datetime|weekday|clipboard)\}/g, + (m, key) => m.startsWith('\\') ? `{${key}}` : replacements[key] + ); +} + +/* ==================== IPC ==================== */ +// windowBounds 只由主进程维护(move/resize 事件)。渲染进程保存的是它启动时读到的 +// 那份 settings 快照,里面的 windowBounds 已经过期——直接落盘会把用户刚拖好的窗口 +// 位置回滚。所以每次写盘前都用「当前真实窗口位置」覆盖这个字段。 +function withOwnedSettings(incoming) { + const out = { ...(incoming || {}) }; + const settings = { ...(out.settings || {}) }; + let bounds = null; + if (mainWindow && !mainWindow.isDestroyed()) { + try { + bounds = mainWindow.getBounds(); + } catch {} + } + // 窗口不可用(退出过程中)时回落到磁盘上已有的值,避免把字段清掉 + if (!bounds) { + try { + const disk = loadData(); + bounds = disk && disk.settings ? disk.settings.windowBounds : null; + } catch {} + } + if (bounds) settings.windowBounds = bounds; + out.settings = settings; + return out; +} + +ipcMain.handle('data:load', () => loadData()); +ipcMain.handle('data:save', (_, data) => { + const enriched = withOwnedSettings(data); + const res = saveData(enriched); + // 关键:先 saveData 再决定是否更新快照。saveData 失败时保持上一次成功状态, + // 否则 scheduleSaveBounds 会把磁盘上本不该存在的那份失败数据当成「真实状态」写盘。 + if (res && res.ok) { + // 维护一份最近一次写入的全量数据快照。scheduleSaveBounds 用它来同步窗口位置, + // 不再从盘读 —— 避免和渲染层的并发保存产生覆盖。 + // 浅拷贝即可:data 来自渲染进程 IPC structured clone、withOwnedSettings 也已经 + // 浅拷贝过 settings,本进程内部无须再 JSON.parse(JSON.stringify(...)) 深拷贝一次。 + // 后续 scheduleSaveBounds 会改写 settings.windowBounds,那块新对象也独立。 + cachedDataSnapshot = { ...enriched, settings: { ...enriched.settings } }; + // data:save 刚刚成功落盘,待处理的窗口位置合并任务无意义了 —— 下一次 + // move/resize 会重新 schedule。提前清掉避免它再用一份过期快照覆盖磁盘。 + if (saveBoundsTimer) { clearTimeout(saveBoundsTimer); saveBoundsTimer = null; } + } + return res; +}); +// 同步版本:用于 beforeunload 场景,sendSync 会阻塞渲染进程直到写盘完成 +ipcMain.on('data:saveSync', (e, data) => { + const enriched = withOwnedSettings(data); + const res = saveData(enriched); + if (res && res.ok) { + cachedDataSnapshot = { ...enriched, settings: { ...enriched.settings } }; + if (saveBoundsTimer) { clearTimeout(saveBoundsTimer); saveBoundsTimer = null; } + } + e.returnValue = res; +}); + +// 仅复制到剪贴板,不触发粘贴;和 `paste` 共用同一套占位符展开,保持一致性 +ipcMain.handle('clipboard:copy', (_, text) => { + if (typeof text !== 'string' || text.length === 0) { + return { ok: false, error: '内容为空' }; + } + // 同样的 100KB 上限挡住滥用:和 paste 对齐 + if (text.length > MAX_PHRASE_CONTENT_LENGTH) { + return { ok: false, error: `内容过长(${text.length} > ${MAX_PHRASE_CONTENT_LENGTH})` }; + } + try { + // 含 {clipboard} 时,先快照原剪贴板再覆盖,避免读到自身写出去的内容 + const snapshot = text.includes('{clipboard}') ? clipboard.readText() : ''; + const expanded = expandPlaceholders(text, snapshot); + clipboard.writeText(expanded); + return { ok: true }; + } catch (e) { + console.error('clipboard:copy error:', e); + return { ok: false, error: e.message || String(e) }; + } +}); + +ipcMain.handle('paste', (_, text, opts) => { + if (typeof text !== 'string' || text.length === 0) { + return { ok: false, error: '内容为空' }; + } + // 剪贴板巨型写入会卡顿甚至触发系统限制:超过 100KB 就拒绝 + if (text.length > MAX_PHRASE_CONTENT_LENGTH) { + return { ok: false, error: `内容过长(${text.length} > ${MAX_PHRASE_CONTENT_LENGTH})` }; + } + + // 1. 含 {clipboard} 时必须先快照原剪贴板,否则下一步 writeText 会覆盖掉要读的内容 + const clipboardSnapshot = text.includes('{clipboard}') ? clipboard.readText() : ''; + const expanded = expandPlaceholders(text, clipboardSnapshot); + + // 2. 写入剪贴板 + try { + clipboard.writeText(expanded); + } catch (e) { + console.error('clipboard.writeText error:', e); + return { ok: false, error: '剪贴板写入失败' }; + } + + // 2.5 用户在设置里开了「粘贴时隐藏悬浮窗」→ 必须走 pasteByHiding, + // 因为只有它会在 PASTE_RESTORE_DELAY_MS 后把窗口重新显示出来。 + // (之前由渲染进程先调 window:hide 再走下面的「有目标」分支, + // 那条分支不负责重显,窗口就一直留在隐藏状态回不来了。) + if (opts && opts.hideFirst) { + pasteByHiding(); + return { ok: true, hasTarget: !!getPasteTarget() }; + } + + // 3. 取目标窗口(非 Windows 平台没有句柄跟踪,一律走兜底) + const target = process.platform === 'win32' && keybd_event ? getPasteTarget() : null; + + // 4. 没有已知目标 → 兜底:隐藏自己让焦点回落,粘完再无焦点显示回来 + if (!target) { + pasteByHiding(); + // 告诉渲染端:剪贴板已写入,但目标窗口未知,需要用户手动点一下输入框 + return { ok: true, hasTarget: false }; + } + + // 5. 有明确目标:拉回前台后直接注入 Ctrl+V。 + // 这里刻意不隐藏 / 重显悬浮窗 —— 连点多条短语时窗口一直在原地不闪, + // 目标输入框也始终是同一个,不用再回去点一次输入框。 + // 如果拉前台失败(被系统拒绝、目标在切换中等),退回兜底走隐藏-粘贴-回显流程, + // 避免把 Ctrl+V 误发到自己窗口里。 + if (!focusTarget(target)) { + pasteByHiding(); + return { ok: true, hasTarget: false }; + } + sendCtrlV(); + + return { ok: true, hasTarget: true }; +}); + +ipcMain.handle('window:hide', () => { if (mainWindow) mainWindow.hide(); }); +// 切换「始终置顶」。渲染进程负责把偏好写进 data.settings(持久化), +// 主进程只负责:1) 立即应用;2) 后续 showFloating / pasteByHiding 路径拿这个状态。 +ipcMain.handle('window:setAlwaysOnTop', (_, on) => { + alwaysOnTop = !!on; + applyAlwaysOnTop(); + return alwaysOnTop; +}); +// 调整窗口透明度(用户设置里 60~100) +ipcMain.handle('window:setOpacity', (_, value) => { + if (!mainWindow || mainWindow.isDestroyed()) return false; + // 注意:不能用 `Number(value) || 1`——0 || 1 === 1 把 0 当成 falsy 落到默认值了。 + // 用 Number.isFinite 显式判 NaN/Infinity/非数字字符串。 + const v = Math.max(0.6, Math.min(1, Number(value))); + if (!Number.isFinite(v)) return false; + try { + mainWindow.setOpacity(v); + return true; + } catch (e) { + console.error('setOpacity error:', e); + return false; + } +}); +ipcMain.handle('app:quit', () => { isQuitting = true; app.quit(); }); + +// 渲染进程要打字了(打开编辑弹窗)→ 临时让窗口获得焦点; +// 关掉弹窗后只把焦点还给原来的目标窗口,窗口本身仍保持可聚焦 +// (搜索框需要持续可用;如果这里 setNoActivate(true) 会让搜索框跟着失能)。 +ipcMain.handle('window:setTyping', (_, typing) => { + if (!mainWindow || mainWindow.isDestroyed()) return false; + if (process.platform !== 'win32') return true; + + try { + if (typing) { + mainWindow.focus(); + } else { + const target = getPasteTarget(); + // 没有目标(比如刚启动、还没有任何前台窗口轮询过)→ 啥也不做,让窗口自然失焦 + if (target) focusTarget(target); + } + return true; + } catch (e) { + console.error('setTyping error:', e); + return false; + } +}); + +// 返回数据文件路径(绝对路径) +ipcMain.handle('app:getDataPath', () => userDataPath); + +// 在文件管理器中定位数据文件 +ipcMain.handle('app:revealDataFile', () => { + try { + if (fs.existsSync(userDataPath)) { + shell.showItemInFolder(userDataPath); + } else { + // 文件还不存在(首次启动未保存)→ 打开所在目录 + shell.openPath(path.dirname(userDataPath)); + } + return true; + } catch (e) { + console.error('reveal data file error:', e); + return false; + } +}); + +// 在系统默认浏览器中打开外部链接 +// 渲染进程走的是 file:// + contextIsolation,直接 在 Electron 里 +// 不会自动跳出当前窗口。让渲染进程通过 IPC 把 URL 交给主进程,由 shell.openExternal +// 调用系统浏览器打开 —— 这样也顺便强制校验协议头,避免被劫持去打开 file:// 之类。 +ipcMain.handle('app:openExternal', (_evt, url) => { + try { + const u = String(url || '').trim(); + if (!/^https?:\/\//i.test(u)) { + console.warn('openExternal rejected non-http(s) url:', u); + return false; + } + shell.openExternal(u); + return true; + } catch (e) { + console.error('openExternal error:', e); + return false; + } +}); + +// 导出数据:弹保存对话框,把当前数据写到用户选的 .json 文件 +ipcMain.handle('data:export', async () => { + try { + const current = loadData(); + const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const defaultName = `quick-paste-${stamp}.json`; + const res = await dialog.showSaveDialog(mainWindow || undefined, { + title: '导出短语', + defaultPath: defaultName, + filters: [{ name: 'JSON', extensions: ['json'] }], + }); + if (res.canceled || !res.filePath) return { ok: false, canceled: true }; + fs.writeFileSync(res.filePath, JSON.stringify(current, null, 2), 'utf-8'); + return { ok: true, filePath: res.filePath }; + } catch (e) { + console.error('export error:', e); + return { ok: false, error: e.message }; + } +}); + +/* ==================== 生命周期 ==================== */ +// 全局快捷键:Ctrl+Shift+V 在系统任何地方都能唤出 / 收起悬浮窗 +// README / 帮助面板里都列着这条快捷键,缺了等于核心交互路径断了 +const GLOBAL_TOGGLE_SHORTCUT = 'CommandOrControl+Shift+V'; + +app.whenReady().then(() => { + userDataPath = path.join(app.getPath('userData'), 'data.json'); + createWindow(); + createTray(); + + // 启动前台窗口跟踪 + startForegroundTracking(); + + // 注册全局唤出快捷键。register 失败通常是别的程序占了这个组合键 + // (比如某些截图工具也用 Ctrl+Shift+V)——失败就静默退化到只通过托盘点击唤出, + // 不阻塞应用启动。 + try { + const ok = globalShortcut.register(GLOBAL_TOGGLE_SHORTCUT, () => toggleWindow()); + if (!ok) console.warn('全局快捷键注册失败,可能被其它应用占用:', GLOBAL_TOGGLE_SHORTCUT); + } catch (e) { + console.warn('globalShortcut.register error:', e); + } +}); + +app.on('will-quit', () => { + // 退出时反注册所有全局快捷键,避免下次的注册失败 + try { globalShortcut.unregisterAll(); } catch {} +}); + +app.on('before-quit', () => { + isQuitting = true; + // 清理窗口位置合并任务:避免退出后定时器回调访问已销毁的 mainWindow + // (回调里有 isDestroyed 检查不会崩,但清理掉更对称、也省一次空写盘) + if (saveBoundsTimer) { + clearTimeout(saveBoundsTimer); + saveBoundsTimer = null; + } + // 清理前台窗口轮询定时器,避免退出后仍在访问主窗口 + if (fgTrackTimer) { + clearInterval(fgTrackTimer); + fgTrackTimer = null; + } + // 清理回显 timer,避免退出后还会触发 mainWindow.showInactive 撞到销毁态 + if (pasteRestoreTimer) { + clearTimeout(pasteRestoreTimer); + pasteRestoreTimer = null; + } + // 清理 hide→paste 延迟 timer,同上理由(回调里 sendCtrlV 会在已销毁进程上下文里跑) + if (pasteHideTimer) { + clearTimeout(pasteHideTimer); + pasteHideTimer = null; + } +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + else if (mainWindow) showFloating(); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7507829 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4126 @@ +{ + "name": "quick-paste", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quick-paste", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "koffi": "^2.16.3" + }, + "devDependencies": { + "electron": "^31.7.7", + "electron-builder": "^24.13.3" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz", + "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "31.7.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.7.7.tgz", + "integrity": "sha512-HZtZg8EHsDGnswFt0QeV8If8B+et63uD6RJ7I4/xhcXqmTIbI08GoubX/wm+HdY0DwcuPe1/xsgqpmYvjdjRoA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/koffi": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.3.tgz", + "integrity": "sha512-E9y1AsgYGlaxMhcZzHr8y96QF2U5XzA12GGVAfbWqIubTwPNMXQarfBzePNXHe0xtIEtNd6ifAv3GAKYGUeBAQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a0a0557 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "quick-paste", + "version": "1.0.0", + "description": "悬浮快速粘贴工具 — 分组管理常用句子,点击即粘贴到当前焦点窗口", + "main": "main.js", + "author": "", + "license": "MIT", + "scripts": { + "start": "node ./scripts/start.js", + "pack": "electron-builder --dir", + "dist": "electron-builder" + }, + "devDependencies": { + "electron": "^31.7.7", + "electron-builder": "^24.13.3" + }, + "dependencies": { + "koffi": "^2.16.3" + }, + "build": { + "appId": "com.example.quickpaste", + "productName": "快速粘贴", + "files": [ + "main.js", + "preload.js", + "renderer/**/*", + "icon.ico", + "package.json" + ], + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ], + "icon": "icon.ico" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "快速粘贴" + } + } +} diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..168c9a6 --- /dev/null +++ b/preload.js @@ -0,0 +1,41 @@ +/** + * 预加载脚本:通过 contextBridge 把主进程能力安全地暴露给渲染进程 + */ +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('api', { + // 数据 + loadData: () => ipcRenderer.invoke('data:load'), + saveData: (data) => ipcRenderer.invoke('data:save', data), + // 同步写盘:仅用于 beforeunload 等不能 await 的场景,调用会阻塞渲染进程直到主进程写完 + saveDataSync: (data) => ipcRenderer.sendSync('data:saveSync', data), + + // 核心:把文本粘贴到当前焦点窗口 + // opts.hideFirst = true 时主进程走「隐藏 → Ctrl+V → 重新显示」流程 + paste: (text, opts) => ipcRenderer.invoke('paste', text, opts || {}), + + // 仅复制到剪贴板,不触发粘贴(用于「复制」按钮) + copy: (text) => ipcRenderer.invoke('clipboard:copy', text), + + // 窗口控制 + hideWindow: () => ipcRenderer.invoke('window:hide'), + // 切换「始终置顶」,返回应用后的实际状态 + setAlwaysOnTop: (on) => ipcRenderer.invoke('window:setAlwaysOnTop', !!on), + quitApp: () => ipcRenderer.invoke('app:quit'), + + // 要打字了(打开/关闭编辑弹窗)→ 临时允许窗口获得键盘焦点 + setTyping: (typing) => ipcRenderer.invoke('window:setTyping', typing), + + // 调整窗口透明度(设置面板里的滑块) + setOpacity: (value) => ipcRenderer.invoke('window:setOpacity', value), + + // 数据文件路径 + getDataPath: () => ipcRenderer.invoke('app:getDataPath'), + revealDataFile: () => ipcRenderer.invoke('app:revealDataFile'), + + // 在系统默认浏览器中打开外部链接(设置面板「关于」区使用) + openExternal: (url) => ipcRenderer.invoke('app:openExternal', url), + + // 导出(保存对话框需要主进程;导入走隐藏 file input,无需 IPC) + exportData: () => ipcRenderer.invoke('data:export'), +}); \ No newline at end of file diff --git a/renderer/index.html b/renderer/index.html new file mode 100644 index 0000000..7d0483b --- /dev/null +++ b/renderer/index.html @@ -0,0 +1,481 @@ + + + + + + 快速粘贴 + + + + + + + + + +
+
+ + 快速粘贴 +
+
+ + + +
+
+ + +
+ + + + +
+
+
+

常用

+ +
+
+ + + + +
    + + + +
    + + + +
    +
    +
    + + + + + + + + + + + +
    + + + + + +
    + + + + diff --git a/renderer/renderer.js b/renderer/renderer.js new file mode 100644 index 0000000..eb942e8 --- /dev/null +++ b/renderer/renderer.js @@ -0,0 +1,1947 @@ +/** + * 渲染进程:UI 交互、数据读写、点击触发粘贴 + */ +const $ = (id) => document.getElementById(id); +const $$ = (sel, root = document) => root.querySelectorAll(sel); + +let data = null; +let editingPhraseId = null; +let searchQuery = ''; +let selectedPhraseIndex = -1; +let currentFilter = 'all'; +let dragSrcId = null; +let dragSrcKind = null; +// 拖拽过程中的 rect 缓存:dragstart 时一次性算出所有 li 的 bounding rect, +// 避免 dragover 每像素 mousemove 都调 getBoundingClientRect 强制 reflow。 +let dragRectCache = null; +// dragover 的 drop-above/below class 切换用 rAF 节流: +// 同一帧内多次 mousemove 只切换 class 一次。 +let dropRafPending = false; +let lastDropTarget = null; +let lastDropAbove = null; +// 上一帧高亮的 li:dragover 从 A 切到 B 时,A 上的 class 要清掉否则留下残影。 +let prevDropTarget = null; + +function cacheDragRects(selector) { + dragRectCache = new Map(); + const list = document.querySelectorAll(selector); + for (const el of list) { + dragRectCache.set(el, el.getBoundingClientRect()); + } +} + +function scheduleDropIndicator(target, above) { + // 目标 / 朝向都没变 → 不再排队(避免同帧内重复 toggle class) + if (lastDropTarget === target && lastDropAbove === above && !dropRafPending) return; + prevDropTarget = lastDropTarget; + lastDropTarget = target; + lastDropAbove = above; + if (dropRafPending) return; + dropRafPending = true; + requestAnimationFrame(() => { + dropRafPending = false; + // 旧 li 上的 drop 标记要清掉(拖到隔壁 li 时,旧 li 仍可能挂着 drop-above/below) + if (prevDropTarget && prevDropTarget !== lastDropTarget) { + prevDropTarget.classList.remove('drop-above', 'drop-below'); + } + if (lastDropTarget) { + lastDropTarget.classList.toggle('drop-above', lastDropAbove); + lastDropTarget.classList.toggle('drop-below', !lastDropAbove); + } + }); +} + +function clearDropIndicators() { + // 清掉所有 li 上的 drop 状态:拖拽结束时统一收尾,避免拖出缝隙留下残影 + const sel = dragSrcKind === 'group' ? '.group-item' : '.phrase-item'; + document.querySelectorAll(sel).forEach((el) => { + el.classList.remove('drop-above', 'drop-below'); + }); + lastDropTarget = null; + lastDropAbove = null; + prevDropTarget = null; + dropRafPending = false; +} + +function uid(prefix = 'id') { + const bytes = new Uint8Array(8); + // 优先用 Web Crypto;某些极端环境(极旧 Electron / sandbox 受限)下不可用 + // 就退回到 Math.random——碰撞概率仍极低,且只在异常路径触发。 + if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { + crypto.getRandomValues(bytes); + } else { + for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256); + } + let hex = ''; + for (const b of bytes) hex += b.toString(16).padStart(2, '0'); + return `${prefix}_${Date.now().toString(36)}_${hex}`; +} + +function escapeHtml(str) { + return String(str ?? '').replace(/[&<>"']/g, (c) => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[c])); +} + +function debounce(fn, wait) { + let timer = null; + let lastArgs = null; + return function debounced(...args) { + lastArgs = args; + if (timer) clearTimeout(timer); + timer = setTimeout(() => { + timer = null; + fn.apply(this, lastArgs); + }, wait); + }; +} + +function highlightText(str, q) { + if (!q || !str) return escapeHtml(str); + // 先 escape 再 regex-escape:避免把未转义的 HTML 字符塞进 $1。 + // 旧版本只 regex-escape,特殊情况下会把用户输入的原始字符当 HTML 注入。 + const safe = escapeHtml(str); + const safeQ = escapeHtml(q).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + try { + return safe.replace(new RegExp(`(${safeQ})`, 'gi'), '$1'); + } catch { + return safe; + } +} + +const TOAST_ICONS = { success: 'i-check', error: 'i-alert', info: 'i-info', '': 'i-info' }; + +function showToast(msg, type = '') { + const stack = $('toast-stack'); + if (!stack) return console.log(`[toast:${type}] ${msg}`); + while (stack.children.length >= 4) stack.firstElementChild.remove(); + const el = document.createElement('div'); + el.className = `toast ${type}`; + const iconId = TOAST_ICONS[type] || 'i-info'; + el.innerHTML = ` + + ${escapeHtml(msg)} + + `; + el.querySelector('.toast-close').addEventListener('click', () => removeToast(el)); + stack.appendChild(el); + el._timer = setTimeout(() => removeToast(el), 2600); +} + +function removeToast(el) { + if (!el || !el.parentNode) return; + if (el._removing) return; + el._removing = true; + if (el._timer) clearTimeout(el._timer); + el.classList.add('fade-out'); + // 用 _removing 标记保证只走一条路径:动画结束优先;超时兜底只清理未被动画触发的元素 + let done = false; + const finalize = () => { + if (done) return; + done = true; + el.remove(); + }; + el.addEventListener('animationend', finalize, { once: true }); + setTimeout(finalize, 400); +} + +function openContextMenu(x, y, items) { + const menu = $('context-menu'); + if (!menu) return; + menu.innerHTML = ''; + for (const it of items) { + if (it.type === 'divider') { + const div = document.createElement('div'); + div.className = 'ctx-divider'; + menu.appendChild(div); + continue; + } + const el = document.createElement('div'); + el.className = 'ctx-item' + (it.danger ? ' danger' : ''); + el.innerHTML = `${escapeHtml(it.label)}`; + el.addEventListener('click', () => { + closeContextMenu(); + if (typeof it.onClick === 'function') it.onClick(); + }); + menu.appendChild(el); + } + menu.classList.remove('hidden'); + requestAnimationFrame(() => { + const rect = menu.getBoundingClientRect(); + const winW = window.innerWidth; + const winH = window.innerHeight; + menu.style.left = Math.max(4, Math.min(x, winW - rect.width - 8)) + 'px'; + menu.style.top = Math.max(4, Math.min(y, winH - rect.height - 8)) + 'px'; + }); + setTimeout(() => { + document.addEventListener('mousedown', onClickOutsideMenu, { once: true }); + }, 0); +} + +function onClickOutsideMenu(e) { + const menu = $('context-menu'); + if (menu && !menu.contains(e.target)) closeContextMenu(); +} + +function closeContextMenu() { + const menu = $('context-menu'); + if (menu) menu.classList.add('hidden'); +} + +function openPromptModal({ + title = '提示', message = '', defaultValue = '', + okLabel = '确定', cancelLabel = '取消', placeholder = '', + danger = false, icon = 'info', showInput = true, +}) { + return new Promise((resolve) => { + const modal = $('prompt-modal'); + $('prompt-title').textContent = title; + $('prompt-message').textContent = message; + const input = $('prompt-input'); + input.value = defaultValue; + input.placeholder = placeholder; + $('prompt-message').classList.toggle('hidden', !message); + input.classList.toggle('hidden', !showInput); + $('prompt-ok').textContent = okLabel; + $('prompt-cancel').textContent = cancelLabel; + const iconEl = $('prompt-icon'); + const iconKind = icon === 'check' ? 'check' : icon === 'alert' ? 'alert' : icon === 'edit' ? 'edit' : 'info'; + iconEl.innerHTML = ``; + iconEl.className = 'prompt-icon ' + (danger ? 'danger' : icon === 'check' ? 'success' : ''); + modal.classList.remove('hidden'); + if (window.api.setTyping) window.api.setTyping(true); + setTimeout(() => { + if (!input.classList.contains('hidden')) { + input.focus(); + input.select(); + } else { + $('prompt-ok').focus(); + } + }, 30); + + const cleanup = (val) => { + modal.classList.add('hidden'); + $('prompt-cancel').removeEventListener('click', onCancel); + $('prompt-ok').removeEventListener('click', onOk); + modal.removeEventListener('click', onBackdrop); + // 关掉 prompt 后若还有其它面板(设置 / 短语编辑弹窗)打开, + // 就不要再 setTyping(false),否则会把它们的打字态一起关掉, + // 导致设置面板里的输入框瞬间收不到键盘事件。 + if (!isAnyPanelOpen() && window.api.setTyping) window.api.setTyping(false); + resolve(val); + }; + const onOk = () => cleanup(showInput ? input.value : true); + const onCancel = () => cleanup(null); + const onBackdrop = (e) => { if (e.target.id === 'prompt-modal') onCancel(); }; + + $('prompt-cancel').addEventListener('click', onCancel); + $('prompt-ok').addEventListener('click', onOk); + modal.addEventListener('click', onBackdrop); + }); +} + +async function promptUser(message, defaultValue = '', opts = {}) { + const v = await openPromptModal({ + title: opts.title || '请输入', message, defaultValue, + placeholder: opts.placeholder || '', okLabel: opts.okLabel || '确定', + danger: !!opts.danger, icon: 'edit', showInput: true, + }); + return v == null ? null : v; +} + +async function confirmUser(message, opts = {}) { + const v = await openPromptModal({ + title: opts.title || '确认操作', message, + okLabel: opts.okLabel || '确定', cancelLabel: opts.cancelLabel || '取消', + danger: opts.danger !== false, icon: 'alert', showInput: false, + }); + return v === true; +} + +function updateTotals() { + const favCount = data.groups.reduce((n, g) => n + g.phrases.filter((p) => p.fav).length, 0); + const recCount = (data.recent || []).length; + // 侧栏顶部快捷区(收藏 / 最近)的角标 + const qFav = $('quick-count-fav'); if (qFav) qFav.textContent = String(favCount); + const qRec = $('quick-count-recent'); if (qRec) qRec.textContent = String(recCount); +} + +function isSidebarCollapsed() { + return !!(data && data.settings && data.settings.sidebarCollapsed); +} + +// 同步 DOM 到当前折叠状态。 +// 图标始终表示「点击后会做什么」:展开态显示「折叠」图标,折叠态显示「展开」图标。 +// 按钮有两份,分别挂在 .sidebar-footer(展开时可见)和 .add-bar(折叠时可见), +// 互斥显示由 #main.sidebar-collapsed 这个 class 控制;两份按钮的 icon/title/aria 同步刷新。 +function applySidebarState() { + const collapsed = isSidebarCollapsed(); + const main = $('main'); + if (main) main.classList.toggle('sidebar-collapsed', collapsed); + // 两份按钮:sidebar-footer 内的折叠按钮 + add-bar 内的展开按钮 + const buttons = [ + { el: $('btn-toggle-sidebar'), next: 'i-sidebar-collapse' }, + { el: $('btn-expand-sidebar'), next: 'i-sidebar-expand' }, + ]; + for (const { el, next } of buttons) { + if (!el) continue; + // 折叠按钮(侧栏展开时显示)图标 = 当前是展开态 → 点击会折叠 → 显示折叠图标 + // 展开按钮(侧栏折叠时显示)图标 = 当前是折叠态 → 点击会展开 → 显示展开图标 + // 所以两份按钮的图标固定不变(各自代表「下一步动作」),不需要 innerHTML 改写。 + el.title = collapsed ? '展开分组栏' : '折叠分组栏'; + el.setAttribute('aria-pressed', collapsed ? 'false' : 'true'); + el.setAttribute('aria-label', collapsed ? '展开分组栏' : '折叠分组栏'); + // next 留作未来扩展(万一以后想让按钮图标随状态微调);当前两份按钮的图标在 HTML 里已固定 + void next; + } +} + +function toggleSidebar() { + if (!data) return; + if (!data.settings) data.settings = {}; + data.settings.sidebarCollapsed = !isSidebarCollapsed(); + applySidebarState(); + persist(); +} + +// 把置顶按钮的视觉态(激活态、tooltip、aria)同步到 on。 +// 图标本身不切换,靠 .toggle-on 的高亮色区分开/关。 +function applyPinButtonUI(on) { + const btn = $('btn-pin-top'); + if (!btn) return; + btn.classList.toggle('toggle-on', !!on); + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + btn.title = on ? '始终置顶(已开启,点击关闭)' : '始终置顶(已关闭,点击开启)'; +} + +function renderGroups() { + const list = $('group-list'); + list.innerHTML = ''; + for (const g of data.groups) { + const li = document.createElement('li'); + li.className = 'group-item' + (g.id === data.activeGroupId ? ' active' : ''); + li.dataset.id = g.id; + li.draggable = true; + li.innerHTML = `${escapeHtml(g.name)}${g.phrases.length}`; + li.addEventListener('click', () => { + if (g.id === data.activeGroupId) return; + data.activeGroupId = g.id; + currentFilter = 'all'; + selectedPhraseIndex = -1; + // 切分组时清空搜索:先清 input.value 再走 setSearchQuery, + // 让 syncSearchClearVisibility 读到空值,清空按钮正确隐藏。 + const sInput = $('search-input'); + if (sInput) sInput.value = ''; + setSearchQuery(''); + persist(); + renderAll(); + }); + li.addEventListener('contextmenu', (e) => { + e.preventDefault(); + openContextMenu(e.clientX, e.clientY, [ + { label: '重命名分组', icon: 'i-edit', onClick: () => renameGroup(g) }, + { label: '清空短语', icon: 'i-trash', onClick: () => clearGroupPhrases(g) }, + { type: 'divider' }, + { label: '删除分组', icon: 'i-trash', danger: true, onClick: () => deleteGroup(g) }, + ]); + }); + + li.addEventListener('dragstart', (e) => { + dragSrcId = g.id; + dragSrcKind = 'group'; + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', g.id); + // 拖拽过程中 list 不会重排,rect 在整个拖拽期间稳定。 + // 在 dragstart 一次性算好所有 li 的 rect,dragover 直接读缓存, + // 避免每个 mousemove 都触发 getBoundingClientRect 强制 layout。 + cacheDragRects('.group-item'); + requestAnimationFrame(() => li.classList.add('dragging')); + }); + li.addEventListener('dragend', () => { + li.classList.remove('dragging'); + clearDropIndicators(); + dragSrcId = null; dragSrcKind = null; + dragRectCache = null; + }); + li.addEventListener('dragover', (e) => { + if (dragSrcKind !== 'group' || dragSrcId === g.id) return; + e.preventDefault(); + const rect = dragRectCache && dragRectCache.get(li); + if (!rect) return; + const above = (e.clientY - rect.top) < rect.height / 2; + scheduleDropIndicator(li, above); + }); + // drop-indicator 的清理统一在 dragend / dragleave 一起做(避免拖到 li 之间的缝隙时 + // 残留半边 drop-above/drop-below 状态)。这里只清自己——dragend 会兜底。 + li.addEventListener('drop', (e) => { + e.preventDefault(); + const rect = li.getBoundingClientRect(); + const above = (e.clientY - rect.top) < rect.height / 2; + reorderGroups(dragSrcId, g.id, above); + }); + + list.appendChild(li); + } + updateTotals(); +} + +function reorderGroups(srcId, targetId, above) { + if (srcId === targetId) return; + const srcIdx = data.groups.findIndex((g) => g.id === srcId); + const tgtIdx = data.groups.findIndex((g) => g.id === targetId); + if (srcIdx < 0 || tgtIdx < 0) return; + const [moved] = data.groups.splice(srcIdx, 1); + let insertAt = data.groups.findIndex((g) => g.id === targetId); + if (!above) insertAt += 1; + data.groups.splice(insertAt, 0, moved); + persist(); + renderGroups(); + renderPhrases(); +} + +// 搜索过滤:三个视图(全部 / 收藏 / 最近)共用同一套匹配规则。 +// 抽出来是因为「最近」视图之前根本没接搜索——搜索框在该视图下可见却完全不生效。 +function applySearchFilter(list) { + if (!searchQuery) return list; + const q = searchQuery.toLowerCase(); + return list.filter( + (p) => (p.title || '').toLowerCase().includes(q) || + (p.content || '').toLowerCase().includes(q) + ); +} + +function visiblePhrases() { + if (currentFilter === 'recent') { + const out = []; + for (const r of data.recent || []) { + for (const g of data.groups) { + const p = g.phrases.find((x) => x.id === r.id); + if (p) { + out.push(Object.assign({}, p, { _groupId: g.id, _groupName: g.name, _ts: r.ts })); + break; + } + } + } + return applySearchFilter(out); + } + if (currentFilter === 'fav') { + // 跨分组聚合:与侧栏顶部「收藏」角标的统计口径一致, + // 也匹配 README 第 10 行「跨分组查看」的产品承诺。 + const out = []; + for (const g of data.groups) { + for (const p of g.phrases) { + if (!p.fav) continue; + out.push(Object.assign({}, p, { _groupId: g.id, _groupName: g.name })); + } + } + return applySearchFilter(out); + } + const group = data.groups.find((g) => g.id === data.activeGroupId); + if (!group) return []; + return applySearchFilter(group.phrases.slice()); +} + +function renderPhrases() { + const group = data.groups.find((g) => g.id === data.activeGroupId); + const list = $('phrase-list'); + const empty = $('phrase-empty'); + list.innerHTML = ''; + + const phrases = visiblePhrases(); + + let headerName = group ? group.name : '(未选择)'; + if (currentFilter === 'fav') headerName = '收藏'; + else if (currentFilter === 'recent') headerName = '最近使用'; + $('current-group-name').textContent = headerName; + + const countEl = $('phrase-count'); + const totalInGroup = group ? group.phrases.length : 0; + // 三个视图统一口径:有搜索时显示「命中 / 总数」,没搜索时只显示总数。 + if (currentFilter === 'recent') { + const totalRecent = (data.recent || []).length; + countEl.textContent = `${phrases.length}` + (searchQuery ? ` / ${totalRecent}` : ''); + countEl.classList.remove('hidden'); + } else if (currentFilter === 'fav') { + // 跨分组统计(与 visiblePhrases 的口径一致),不要再限定 group + const favCount = data.groups.reduce((n, g) => n + g.phrases.filter((p) => p.fav).length, 0); + countEl.textContent = `${phrases.length}` + (searchQuery ? ` / ${favCount}` : ''); + countEl.classList.remove('hidden'); + } else if (searchQuery && group) { + countEl.textContent = `${phrases.length} / ${totalInGroup}`; + countEl.classList.remove('hidden'); + } else { + countEl.textContent = ''; + countEl.classList.add('hidden'); + } + + if (phrases.length === 0) { + const titleEl = empty.querySelector('.empty-title'); + const descEl = empty.querySelector('.empty-desc'); + const actionEl = empty.querySelector('.empty-action'); + const illu = empty.querySelector('.empty-illustration'); + let showAction = false; + // 搜索必须排在最前面判断:搜索无结果时不论处在哪个视图, + // 用户要看到的都是「没有匹配」,而不是「还没有收藏 / 还没有最近使用」 + // (后者会让人以为收藏被清空了)。 + if (searchQuery) { + titleEl.textContent = '没有匹配的短语'; + descEl.textContent = '换个关键词试试'; + } else if (currentFilter === 'recent') { + titleEl.textContent = '还没有最近使用'; + descEl.textContent = '粘贴短语后会显示在这里'; + } else if (currentFilter === 'fav') { + titleEl.textContent = '还没有收藏'; + descEl.textContent = '点击短语前的星标来收藏'; + } else { + titleEl.textContent = '这个分组还是空的'; + descEl.textContent = '点下方按钮添加你的第一条常用语'; + showAction = true; + } + if (actionEl) actionEl.classList.toggle('hidden', !showAction); + // 重新触发入场效果:先拿掉再 reflow 再加回去,保证动画每次重置 + if (illu) { + illu.classList.remove('run-in'); + // 读取 offsetWidth 强制浏览器做一次 reflow,让下面的 add 触发动画而不是被合并掉 + void illu.offsetWidth; + illu.classList.add('run-in'); + } + empty.classList.remove('hidden'); + selectedPhraseIndex = -1; + return; + } + empty.classList.add('hidden'); + + if (selectedPhraseIndex >= phrases.length) selectedPhraseIndex = phrases.length - 1; + + const frag = document.createDocumentFragment(); + for (let idx = 0; idx < phrases.length; idx++) { + const p = phrases[idx]; + const li = document.createElement('li'); + li.className = 'phrase-item' + (idx === selectedPhraseIndex ? ' selected' : ''); + li.dataset.id = p.id; + li.dataset.idx = String(idx); + li.draggable = currentFilter === 'all' && !searchQuery; + const isFav = !!p.fav; + const useCount = p.useCount || 0; + // 「最近」/「收藏」是跨分组视图,需要标注来源分组; + // 「全部」视图下当前分组已经在标题栏显示,就不再重复。 + const showGroup = (currentFilter === 'recent' || currentFilter === 'fav') && p._groupName; + const title = (p.title || p.content || '').slice(0, 40); + const preview = (p.content || '').replace(/\s+/g, ' ').slice(0, 200); + li.innerHTML = ` +
    + + ${highlightText(title, searchQuery)} +
    + + + +
    +
    +
    ${highlightText(preview, searchQuery)}
    + ${(useCount > 0 || showGroup) ? ` +
    + ${showGroup ? `${escapeHtml(p._groupName)}` : ''} + ${useCount > 0 ? `已用 ${useCount} 次` : ''} +
    ` : ''} + `; + + if (li.draggable) { + li.addEventListener('dragstart', (e) => { + dragSrcId = p.id; + dragSrcKind = 'phrase'; + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', p.id); + // 与分组拖拽共用 rect 缓存:避免每个 mousemove 都触发 layout + cacheDragRects('.phrase-item'); + requestAnimationFrame(() => li.classList.add('dragging')); + }); + li.addEventListener('dragend', () => { + li.classList.remove('dragging'); + clearDropIndicators(); + dragSrcId = null; dragSrcKind = null; + dragRectCache = null; + }); + li.addEventListener('dragover', (e) => { + if (dragSrcKind !== 'phrase' || dragSrcId === p.id) return; + e.preventDefault(); + const rect = dragRectCache && dragRectCache.get(li); + if (!rect) return; + const above = (e.clientY - rect.top) < rect.height / 2; + scheduleDropIndicator(li, above); + }); + li.addEventListener('drop', (e) => { + e.preventDefault(); + const rect = li.getBoundingClientRect(); + const above = (e.clientY - rect.top) < rect.height / 2; + reorderPhrases(dragSrcId, p.id, above); + }); + } + + // 右键菜单:复用 openContextMenu(x, y, items),与分组右键一致 + // 「收藏/取消收藏」按当前状态动态切换文案 + li.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const items = [ + { label: '粘贴', icon: 'i-paste-icon', onClick: () => pastePhrase(p) }, + { label: '仅复制', icon: 'i-copy', onClick: () => copyPhrase(p) }, + { type: 'divider' }, + { label: p.fav ? '取消收藏' : '收藏', icon: 'i-pin', onClick: () => toggleFav(p) }, + { label: '编辑', icon: 'i-edit', onClick: () => openPhraseModal(p) }, + { type: 'divider' }, + { label: '删除', icon: 'i-trash', danger: true, onClick: () => deletePhrase(p) }, + ]; + openContextMenu(e.clientX, e.clientY, items); + }); + + frag.appendChild(li); + } + list.appendChild(frag); +} + +function reorderPhrases(srcId, targetId, above) { + if (!data.activeGroupId) return; + const group = data.groups.find((g) => g.id === data.activeGroupId); + if (!group) return; + if (srcId === targetId) return; + const srcIdx = group.phrases.findIndex((p) => p.id === srcId); + const tgtIdx = group.phrases.findIndex((p) => p.id === targetId); + if (srcIdx < 0 || tgtIdx < 0) return; + // 先记下当前选中的那条短语的 id:拖拽后数组下标整体位移, + // 不修正 selectedPhraseIndex 就会让 .selected 高亮跑到别的短语上去。 + const selectedId = (selectedPhraseIndex >= 0 && group.phrases[selectedPhraseIndex]) + ? group.phrases[selectedPhraseIndex].id + : null; + const [moved] = group.phrases.splice(srcIdx, 1); + let insertAt = group.phrases.findIndex((p) => p.id === targetId); + if (!above) insertAt += 1; + group.phrases.splice(insertAt, 0, moved); + if (selectedId) { + const newIdx = group.phrases.findIndex((p) => p.id === selectedId); + selectedPhraseIndex = newIdx >= 0 ? newIdx : -1; + } + persist(); + renderPhrases(); +} + +function renderAll() { + renderGroups(); + renderPhrases(); + renderQuickAccess(); +} + +// 侧栏顶部「收藏 / 最近」快捷入口的高亮:被 g/f/r 快捷键和点击共同使用。 +// 视觉锚点已经迁移到左侧栏,这里把 active 状态同步过去即可。 +function renderQuickAccess() { + $$('.quick-item').forEach((el) => { + el.classList.toggle('active', el.dataset.filter === currentFilter); + }); +} + +// 切换过滤视图:被 g/f/r 快捷键和侧栏快捷入口共同使用,统一处理选中态重置 +function setCurrentFilter(filter) { + if (!['all', 'fav', 'recent'].includes(filter)) return; + if (filter === currentFilter) return; + currentFilter = filter; + selectedPhraseIndex = -1; + renderQuickAccess(); + renderPhrases(); +} + +// 侧栏快捷入口的点击行为:再点一次已激活的「收藏 / 最近」回到「全部」。 +// 侧栏里没有「全部」按钮,如果不做这个 toggle,用户点进收藏后就只能靠 +// 按 g 键或改点某个分组才能退出去 —— 纯鼠标操作会卡死在该视图里。 +function toggleQuickFilter(filter) { + setCurrentFilter(filter === currentFilter ? 'all' : filter); +} + +const renderPhrasesDebounced = debounce(() => renderPhrases(), 80); + +// 搜索 query 的唯一写入入口:IME 上屏、英文输入、点清空按钮都走这里, +// 避免散落在多处的赋值 / 隐藏 toggle 出现状态不一致。 +function setSearchQuery(q) { + syncSearchClearVisibility(); + const next = (q || '').trim(); + if (next === searchQuery) return; + searchQuery = next; + selectedPhraseIndex = -1; + renderPhrasesDebounced(); +} + +// 清空按钮可见性跟输入框实际显示值走(而不是 trim 后的 searchQuery)—— +// 这样用户在 IME 拼写过程中输入 " abc" 时按钮也能正确显示, +// 而搜索匹配仍然按 trim 后的内容来。 +function syncSearchClearVisibility() { + const input = $('search-input'); + $('search-clear').classList.toggle('hidden', input.value.length === 0); +} + +function isAnyPanelOpen() { + return !$('modal').classList.contains('hidden') || + !$('prompt-modal').classList.contains('hidden') || + !$('settings-panel').classList.contains('hidden'); +} + +function moveSelection(delta) { + const phrases = visiblePhrases(); + if (phrases.length === 0) { selectedPhraseIndex = -1; return; } + if (selectedPhraseIndex < 0) selectedPhraseIndex = 0; + else selectedPhraseIndex = Math.max(0, Math.min(phrases.length - 1, selectedPhraseIndex + delta)); + const items = $('phrase-list').querySelectorAll('.phrase-item'); + items.forEach((el, i) => el.classList.toggle('selected', i === selectedPhraseIndex)); + const sel = items[selectedPhraseIndex]; + if (sel) sel.scrollIntoView({ block: 'nearest' }); +} + +function actOnSelected() { + const phrases = visiblePhrases(); + if (selectedPhraseIndex < 0 || selectedPhraseIndex >= phrases.length) return; + const p = phrases[selectedPhraseIndex]; + if (!p) return; + const items = $('phrase-list').querySelectorAll('.phrase-item'); + const item = items[selectedPhraseIndex]; + if (item) { + item.classList.add('fired'); + setTimeout(() => item.classList.remove('fired'), 700); + } + // 严格白名单:data.json 被外部脚本写脏("Paste" / "paste " / "xxx")时落到 paste + // 是安全的,但显式白名单比"未知值降级到 paste"读起来更明确。 + const ALLOWED_CLICK_ACTIONS = ['paste', 'copy']; + const act = ALLOWED_CLICK_ACTIONS.includes(data.settings && data.settings.clickAction) + ? data.settings.clickAction : 'paste'; + if (act === 'copy') copyPhrase(p); + else pastePhrase(p); +} + +const persist = (() => { + let timer = null; + let pending = null; + const flush = async () => { + if (timer) { clearTimeout(timer); timer = null; } + if (!pending) return; + const snapshot = pending; + pending = null; + try { + const res = await window.api.saveData(snapshot); + if (res && res.ok === false) showToast(`保存失败:${res.error || '未知错误'}`, 'error'); + } catch (e) { + console.error('saveData failed:', e); + showToast(`保存失败:${e.message || e}`, 'error'); + } + }; + const flushSync = () => { + if (timer) { clearTimeout(timer); timer = null; } + if (!pending || !window.api.saveDataSync) return; + const snapshot = pending; + pending = null; + try { window.api.saveDataSync(snapshot); } + catch (e) { console.error('saveDataSync failed:', e); } + }; + const fn = (next) => { + // IPC invoke 已经走 structured clone,主进程拿到的就是独立副本。 + // 这里再 JSON.parse(JSON.stringify(data)) 深拷贝一次是冗余的,每 200ms 防抖窗口 + // 内的任何修改(拖动窗口、粘贴、改设置)都会触发一次全量序列化,删掉。 + pending = next != null ? next : data; + if (timer) clearTimeout(timer); + timer = setTimeout(flush, 200); + }; + fn.flush = flush; + fn.flushSync = flushSync; + return fn; +})(); + +window.addEventListener('pagehide', () => { persist.flushSync(); }); +window.addEventListener('beforeunload', () => { persist.flushSync(); }); + +function recordRecent(phraseId) { + if (!data.recent) data.recent = []; + // 先确认短语存在 —— 否则会在 recent 里堆出指向已删除短语的孤立记录, + // 拖到 50 条后还要再重置数据。 + // 用 phraseIndex 索引,O(1) 拿到 group / phrase 对象,避免每个粘贴都遍历全库。 + const hit = phraseIndex.get(phraseId); + const target = hit ? hit.phrase : null; + if (!target) return false; + + // useCount 与「最近」列表是独立的统计概念:即使关闭了最近记录, + // 「已用 N 次」徽章也应当照常增长。早期实现把这两件事绑在一起, + // 关掉最近记录后徽章也跟着停了,对用户来说是隐藏行为变化。 + const before = target.useCount || 0; + target.useCount = before + 1; + + // 「最近」列表的更新受用户偏好控制。 + if (data.settings && data.settings.recordRecent === false) return before === 0; + + const idx = data.recent.findIndex((r) => r.id === phraseId); + if (idx >= 0) data.recent.splice(idx, 1); + data.recent.unshift({ id: phraseId, ts: Date.now() }); + if (data.recent.length > 50) data.recent.length = 50; + // 0 -> 1 是触发「已用 N 次」徽章首次出现的临界态,调用方据此判断要不要重渲染。 + return before === 0; +} + +async function pastePhrase(phrase) { + // 「粘贴时隐藏悬浮窗」必须交给主进程:它在 Ctrl+V 之后会把窗口重新显示出来。 + // 如果渲染进程先调 hideWindow() 再走 paste,主进程的「有明确目标」快路径 + // 不负责重显,窗口会一直留在隐藏状态。hideFirst 让主进程走 pasteByHiding。 + const hideFirst = !!(data.settings && data.settings.hideOnPaste); + let res; + try { + res = await window.api.paste(phrase.content || '', { hideFirst }); + } catch (e) { + // IPC 通道异常(preload 抛错、contextBridge 断开等)——主进程根本没拿到请求 + console.error('paste IPC failed:', e); + showToast(`粘贴失败:${e.message || e}`, 'error'); + return; + } + const ok = res === true || !!(res && res.ok); + if (!ok) { + showToast(`粘贴失败:${(res && res.error) || '未知错误'}`, 'error'); + return; + } + // 只在真正成功后才计入「最近使用」,失败的不该污染历史 + recordRecent(phrase.id); + // recordRecent 改的是 data.recent 和 p.useCount,必须落盘—— + // 否则「最近使用」和「已用 N 次」在重启后全部丢失(copyPhrase 一直有 persist, + // 粘贴路径漏了,导致最常用的那条路径反而不记数)。 + persist(); + // 「最近」/「收藏」角标变化(useCount 从 0→>0、recent 列表变化时需要刷新) + updateTotals(); + // 触发重渲染:recordRecent 调用后 useCount 已经 +1,任意视图下的「已用 N 次」 + // 数字(无论是首次出现 0→1,还是已有的 N→N+1)都必须立即刷新。 + // 旧条件 useCountJustAppeared || currentFilter === 'recent' 漏了 N→N+1 的常规递增, + // 导致最常用的那条短语点击后徽章数字不更新——这是用户最常看到的视图卡顿。 + // 「最近」视图的顺序变化也包含在此次重渲染里(list 本来就要重建)。 + renderPhrases(); + // 「hideFirst」走 pasteByHiding 路径,无论 hasTarget 真假 Ctrl+V 都已发送, + // 这里不能再因 hasTarget=false 提示「请点输入框」——会误导用户以为没粘上。 + // 非 hideFirst 且主进程确实不知道目标窗口(启动后第一次粘贴、lastFgHwnd 失效等)时, + // 才提示用户手动点一下目标输入框。 + if (!hideFirst && res && res.hasTarget === false) { + showToast('已复制到剪贴板,请先点一下要输入的位置', 'info'); + return; + } + showToast(`已粘贴:${phrase.title || '短语'}`, 'success'); +} + +async function copyPhrase(phrase) { + // 走主进程:和「粘贴」共用同一套占位符展开逻辑({date} / {time} / {clipboard} 等)。 + // 之前用 navigator.clipboard.writeText 时这些占位符是原文落盘, + // 与粘贴行为不一致,用户粘贴看到正确值,复制却看到 {date}。 + try { + const res = await window.api.copy(phrase.content || ''); + if (res && res.ok) { + recordRecent(phrase.id); + updateTotals(); + showToast(`已复制:${phrase.title || '短语'}`, 'success'); + persist(); + // useCount +1 后「已用 N 次」数字必须立刻刷新:包括 0→1 首次出现、 + // 以及 N→N+1 的常规递增。原条件漏了后者,导致点击后数字看起来不变。 + // 「最近」视图的顺序变化也在这次重渲染里一并覆盖。 + renderPhrases(); + } else { + showToast(`复制失败:${(res && res.error) || '未知错误'}`, 'error'); + } + } catch (e) { + showToast('复制失败:' + (e.message || e), 'error'); + } +} + +function renderPlaceholderPreview() { + const c = $('phrase-content'); + const previewBox = $('placeholder-preview'); + const previewText = $('placeholder-preview-text'); + if (!c || !previewBox || !previewText) return; + const src = c.value || ''; + // 同时识别 \{xxx\}(字面量)和 {xxx}(活跃占位符)——与主进程 expandPlaceholders 保持一致 + const PLACEHOLDER_RE = /\\?\{(date|time|datetime|weekday|clipboard)\}/g; + if (!PLACEHOLDER_RE.test(src)) { + previewBox.classList.add('hidden'); + previewText.innerHTML = ''; + return; + } + // 重置 RegExp 的 lastIndex(test() 会改它,再调用 replace 会跳过某些匹配) + PLACEHOLDER_RE.lastIndex = 0; + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + const replacements = { + date: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`, + time: `${pad(now.getHours())}:${pad(now.getMinutes())}`, + weekday: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][now.getDay()], + clipboard: '{剪贴板}', + }; + replacements.datetime = `${replacements.date} ${replacements.time}`; + // 先 escapeHtml 整个 src:避免把用户输入的原始字符当 HTML 注入 + // 再识别 \{xxx\}(字面量)和 {xxx}(活跃占位符): + // - 字面量:去掉反斜杠,显示为纯文本(不加 mark,因为不是活跃占位符) + // - 活跃占位符:用 包裹展开值 + const escaped = escapeHtml(src).replace( + PLACEHOLDER_RE, + (m, key) => m.startsWith('\\') + ? escapeHtml('{' + key + '}') + : `${escapeHtml(replacements[key])}` + ); + previewText.innerHTML = escaped; + previewBox.classList.remove('hidden'); +} + +function openPhraseModal(phrase = null) { + editingPhraseId = phrase ? phrase.id : null; + $('modal-title').textContent = phrase ? '编辑短语' : '添加短语'; + $('phrase-title').value = phrase ? (phrase.title || '') : ''; + $('phrase-content').value = phrase ? (phrase.content || '') : ''; + $('phrase-fav').checked = phrase ? !!phrase.fav : false; + refreshSaveButtonState(); + refreshCounter(); + renderPlaceholderPreview(); + $('modal').classList.remove('hidden'); + if (window.api.setTyping) window.api.setTyping(true); + setTimeout(() => $('phrase-title').focus(), 50); +} + +function closePhraseModal() { + if ($('modal').classList.contains('hidden')) return; + editingPhraseId = null; + $('modal').classList.add('hidden'); + // 与 prompt cleanup 保持一致:还有其它面板(设置)打开时不要关掉打字态, + // 否则设置面板里的输入框瞬间收不到键盘事件。 + if (!isAnyPanelOpen() && window.api.setTyping) window.api.setTyping(false); +} + +// 「收藏 / 最近」视图会跨分组展示短语,此时被操作的短语可能根本不在当前 activeGroup 里。 +// 所有按 ID 增删改之前都必须先定位它真正所属的分组,否则会静默失败 +// (编辑丢改动、删除删不掉、移动不生效)。 +// +// 性能:旧实现每次按 ID 查找都要 O(N×M) 遍历所有 group+phrases。 +// 维护一份 phraseIndex = Map,data 变化时重建, +// 查找降到 O(1)。短语库到几百条以上时 recordRecent / findPhraseOwner 的差距肉眼可见。 +let phraseIndex = new Map(); +function rebuildPhraseIndex() { + phraseIndex.clear(); + if (!data || !Array.isArray(data.groups)) return; + for (const g of data.groups) { + if (!Array.isArray(g.phrases)) continue; + for (let i = 0; i < g.phrases.length; i++) { + const p = g.phrases[i]; + if (p && typeof p.id === 'string') phraseIndex.set(p.id, { group: g, phrase: p, index: i }); + } + } +} + +function findPhraseOwner(phraseId) { + const hit = phraseIndex.get(phraseId); + if (!hit) return null; + // index 可能在用户拖拽 / 排序后过期;这里再校准一次以防万一 + // (拖拽路径会走 reorderPhrases,里面的 splice 会让后续 index 整体位移) + const realIdx = hit.group.phrases.indexOf(hit.phrase); + return { group: hit.group, phrase: hit.phrase, index: realIdx >= 0 ? realIdx : hit.index }; +} + +// 「最近」相关的角标 / 计数刷新(使用 recordRecent / 删除短语 后调用) +// 唯一需要触发的副作用是更新侧栏顶部「收藏 / 最近」的角标。 +// 旧版 applyToggles 已被合并到 renderPhrases 里,无需再单独调用。 +// 原 refreshRecentUi wrapper 已删除,调用点直接用 updateTotals()。 + +function savePhraseFromModal() { + const title = $('phrase-title').value.trim(); + const content = $('phrase-content').value; + const fav = $('phrase-fav').checked; + if (!content.trim()) { + showToast('内容不能为空', 'error'); + return; + } + + if (editingPhraseId) { + // 编辑:必须用 ID 跨组查找,否则在 收藏 / 最近 视图下打开的编辑弹窗会"保存成功"但实际丢改动 + const owner = findPhraseOwner(editingPhraseId); + if (owner) { + owner.group.phrases[owner.index].title = title; + owner.group.phrases[owner.index].content = content; + owner.group.phrases[owner.index].fav = fav; + } else { + showToast('找不到要编辑的短语,可能已被删除', 'error'); + closePhraseModal(); + return; + } + } else { + // 新增:固定插入到当前活动分组(行为不变) + const group = data.groups.find((g) => g.id === data.activeGroupId); + if (!group) { + showToast('请先选择分组', 'error'); + return; + } + group.phrases.unshift({ id: uid('phr'), title, content, fav, useCount: 0 }); + // 增量更新索引:新增条目只在这一条;编辑条目不改变 id,无需动索引。 + rebuildPhraseIndex(); + } + closePhraseModal(); + persist(); + renderAll(); + showToast(editingPhraseId ? '已更新' : '已添加', 'success'); +} + +async function deletePhrase(phrase) { + const label = phrase.title || (phrase.content || '').slice(0, 20) || '该短语'; + const ok = await confirmUser(`确认删除短语「${label}」?`, { title: '删除短语' }); + if (!ok) return; + // 找出当前显示列表里这条短语的真实位置:删除后需要把选中索引往前提, + // 否则键盘选中状态会指向空或下一条 + const visible = visiblePhrases(); + const visibleIdx = visible.findIndex((x) => x.id === phrase.id); + + for (const g of data.groups) { + g.phrases = g.phrases.filter((x) => x.id !== phrase.id); + } + if (data.recent) data.recent = data.recent.filter((r) => r.id !== phrase.id); + // 修复 selectedPhraseIndex: + // - 选中项就是要删除的项:重置为 -1(renderPhrases 会自动 -1 当超出范围,但 + // 直接重置更准确——删除后用户期望选中态消失,而不是悄悄换到下一条) + // - 选中项在删除项之后:下移 1 + // - 选中项在删除项之前:保持不变 + if (visibleIdx < 0) { + // 不可见视图(理论上不应发生,但保留兜底) + } else if (selectedPhraseIndex === visibleIdx) { + selectedPhraseIndex = -1; + } else if (selectedPhraseIndex > visibleIdx) { + selectedPhraseIndex -= 1; + } + // 短语索引同步:被删的 id 还留在 phraseIndex 里会污染下次查找 + rebuildPhraseIndex(); + persist(); + renderAll(); + showToast('已删除', 'success'); +} + +// 切换收藏:在「最近」/「收藏」跨分组视图下,p 只是源对象的浅拷贝, +// 必须用 _groupId 回写到原始数据,否则修改会在下一次渲染时被覆盖掉。 +// 「全部」视图下 p 直接指向源对象,原地修改即可。 +function toggleFav(p) { + if ((currentFilter === 'recent' || currentFilter === 'fav') && p._groupId) { + const g = data.groups.find((x) => x.id === p._groupId); + if (g) { + const real = g.phrases.find((x) => x.id === p.id); + if (real) { + real.fav = !real.fav; + p.fav = real.fav; + } else { + p.fav = !p.fav; + } + } else { + p.fav = !p.fav; + } + } else { + p.fav = !p.fav; + } + persist(); + // 收藏数变了 → 侧栏「收藏 N」徽章必须同步刷新(否则点完星标看不到数字变化) + updateTotals(); + renderPhrases(); + showToast(p.fav ? '已加入收藏' : '已取消收藏', 'success'); +} + +function movePhrase(phrase, delta) { + if (currentFilter !== 'all' || searchQuery) { + showToast('清空搜索 / 切换到全部后再调整顺序', 'info'); + return; + } + const group = data.groups.find((g) => g.id === data.activeGroupId); + if (!group) { showToast('当前没有可用分组', 'info'); return; } + const idx = group.phrases.findIndex((x) => x.id === phrase.id); + if (idx < 0) { showToast('短语不在当前分组', 'info'); return; } + // 「全部」视图的显示顺序 == group.phrases 的数组顺序(visiblePhrases 不做任何排序), + // 所以直接和相邻项交换即可。 + // + // 旧逻辑假设列表是「收藏区在前、未收藏区在后」,于是只肯在同 fav 的项之间跳着交换。 + // 但这个分区渲染从来没有实现过,结果就是:只要上/下一条的收藏状态和自己不同, + // Alt+↑/↓ 就静默什么都不做,用户以为快捷键坏了。 + const target = idx + delta; + if (target < 0 || target >= group.phrases.length) { + showToast(delta < 0 ? '已经在最顶部' : '已经在最底部', 'info'); + return; + } + [group.phrases[idx], group.phrases[target]] = [group.phrases[target], group.phrases[idx]]; + // 选中项的下标按显示顺序换算:本视图下显示顺序 = 数组顺序,所以 idx / target 直接换 + if (selectedPhraseIndex === idx) selectedPhraseIndex = target; + else if (selectedPhraseIndex === target) selectedPhraseIndex = idx; + persist(); + renderPhrases(); +} + +function refreshSaveButtonState() { + $('modal-save').disabled = !$('phrase-content').value.trim(); +} + +function refreshCounter() { + const t = $('phrase-title'); + const c = $('phrase-content'); + const tc = $('title-counter'); if (tc) tc.textContent = `${t.value.length} / 40`; + const cc = $('content-counter'); if (cc) cc.textContent = `${c.value.length} / 2000`; +} + +function onPhraseListClick(e) { + const favBtn = e.target.closest('[data-act="fav"]'); + if (favBtn) { + e.stopPropagation(); + const item = favBtn.closest('.phrase-item'); + const idx = parseIdx(item); + if (idx < 0) return; + const p = visiblePhrases()[idx]; + if (p) toggleFav(p); + return; + } + const actBtn = e.target.closest('[data-act]'); + if (actBtn) { + e.stopPropagation(); + const item = actBtn.closest('.phrase-item'); + const idx = parseIdx(item); + if (idx < 0) return; + const p = visiblePhrases()[idx]; + if (!p) return; + const act = actBtn.dataset.act; + if (act === 'copy') { + item.classList.add('fired'); + setTimeout(() => item.classList.remove('fired'), 700); + copyPhrase(p); + } else if (act === 'edit') openPhraseModal(p); + else if (act === 'del') deletePhrase(p); + return; + } + const item = e.target.closest('.phrase-item'); + if (!item) return; + const idx = parseIdx(item); + if (idx < 0) return; + const p = visiblePhrases()[idx]; + if (!p) return; + selectedPhraseIndex = idx; + $$('.phrase-item').forEach((el, i) => el.classList.toggle('selected', i === idx)); + item.classList.add('fired'); + setTimeout(() => item.classList.remove('fired'), 700); + actOnSelected(); +} + +// 把 "dataset.idx" 字符串解析成有效下标:Number(undefined) = NaN、NaN 作为数组下标 +// 返回 undefined,导致「点击了但什么都不发生」的静默失败。这里集中防御一次。 +function parseIdx(item) { + if (!item || !item.dataset) return -1; + const n = Number(item.dataset.idx); + return Number.isInteger(n) && n >= 0 ? n : -1; +} + +async function addGroup() { + const name = await promptUser('给新分组起个名字:', '', { title: '新建分组', placeholder: '例如:客服' }); + if (!name || !name.trim()) return; + const g = { id: uid('grp'), name: name.trim(), phrases: [] }; + data.groups.push(g); + data.activeGroupId = g.id; + currentFilter = 'all'; + selectedPhraseIndex = -1; + persist(); + renderAll(); + showToast('已新建分组', 'success'); +} + +async function renameGroup(target = null) { + // 支持右键传入任意分组,否则默认操作当前激活分组 + const group = target || data.groups.find((g) => g.id === data.activeGroupId); + if (!group) return; + const name = await promptUser('把分组重命名为:', group.name, { title: '重命名分组' }); + if (!name || !name.trim()) return; + group.name = name.trim(); + persist(); + renderAll(); +} + +async function deleteGroup(target = null) { + // 支持右键传入任意分组,否则默认操作当前激活分组 + const group = target || data.groups.find((g) => g.id === data.activeGroupId); + if (!group) return; + if (data.groups.length === 1) { + showToast('至少保留一个分组', 'error'); + return; + } + const yes = await confirmUser( + `确认删除分组「${group.name}」?该分组下的 ${group.phrases.length} 条短语也会被删除。`, + { title: '删除分组' } + ); + if (!yes) return; + const removedPhraseIds = new Set(group.phrases.map((p) => p.id)); + data.groups = data.groups.filter((g) => g.id !== group.id); + if (data.recent) data.recent = data.recent.filter((r) => !removedPhraseIds.has(r.id)); + // 只在被删的是当前激活分组时才切换激活分组,避免误改上下文 + if (data.activeGroupId === group.id) { + data.activeGroupId = data.groups[0].id; + selectedPhraseIndex = -1; + } + rebuildPhraseIndex(); + persist(); + renderAll(); + showToast('已删除分组', 'success'); +} + +async function clearGroupPhrases(group) { + if (!group || group.phrases.length === 0) { + showToast('该分组已经是空的', 'info'); + return; + } + const yes = await confirmUser( + `确认清空分组「${group.name}」中的 ${group.phrases.length} 条短语?`, + { title: '清空短语' } + ); + if (!yes) return; + const removedIds = new Set(group.phrases.map((p) => p.id)); + group.phrases = []; + if (data.recent) data.recent = data.recent.filter((r) => !removedIds.has(r.id)); + // 如果选中的短语在被清空的分组里 → 重置;否则选中索引可能指向空位置 + if (selectedPhraseIndex >= 0) { + const visible = visiblePhrases(); + if (selectedPhraseIndex >= visible.length) selectedPhraseIndex = -1; + } + rebuildPhraseIndex(); + persist(); + renderAll(); + showToast('已清空', 'success'); +} + +async function exportDataNow() { + const res = await window.api.exportData(); + if (res && res.ok) showToast(`已导出到:${res.filePath}`, 'success'); + else if (res && res.canceled) return; + else showToast(`导出失败:${(res && res.error) || '未知错误'}`, 'error'); +} + +function mergeImported(imported) { + let addedPhrases = 0; + let updatedPhrases = 0; + let addedGroups = 0; + const byGroupId = new Map(data.groups.map((g) => [g.id, g])); + for (const ig of imported.groups || []) { + if (!ig || typeof ig.name !== 'string') continue; + // 分组 id 必须统一为字符串,否则 byGroupId.get 用 ig.id 做 key 时 + // 会和现有字符串 id 走不同的 hash 路径,后续 if (!g) 分支也不会触达合并逻辑。 + const groupId = (typeof ig.id === 'string' && ig.id) ? ig.id : null; + let g = groupId ? byGroupId.get(groupId) : null; + if (!g) { + // 修复:保留导入分组的 ID(如果是合法字符串),后续再次合并时能识别同一组 + const newGroupId = groupId || uid('grp'); + g = { id: newGroupId, name: ig.name, phrases: [] }; + data.groups.push(g); + byGroupId.set(g.id, g); + addedGroups++; + } + const byPhraseId = new Map(g.phrases.map((p) => [p.id, p])); + for (const ip of ig.phrases || []) { + if (!ip || typeof ip.content !== 'string') continue; + // 短语 id 同样统一为字符串,避免数字/对象 id 被当作字符串 id 误识别。 + const phraseId = (typeof ip.id === 'string' && ip.id) ? ip.id : null; + const existing = phraseId ? byPhraseId.get(phraseId) : null; + if (existing) { + // 修复:导入数据没带 title/fav 时保留现值,避免覆盖为默认值 + existing.title = typeof ip.title === 'string' ? ip.title : existing.title; + existing.content = ip.content; + existing.fav = typeof ip.fav === 'boolean' ? ip.fav : existing.fav; + updatedPhrases++; + } else { + // 修复:保留导入短语的 ID,便于后续合并识别同一条 + const newPhraseId = phraseId || uid('phr'); + const newPhrase = { + id: newPhraseId, + title: typeof ip.title === 'string' ? ip.title : '', + content: ip.content, + fav: !!ip.fav, + useCount: typeof ip.useCount === 'number' ? ip.useCount : 0, + }; + g.phrases.push(newPhrase); + // 关键:把刚 push 的新短语登记回 byPhraseId, + // 否则导入文件里若出现重复 id 的两条短语,后一条会绕过合并、 + // 直接再加一遍成为重复条目。 + byPhraseId.set(newPhraseId, newPhrase); + addedPhrases++; + } + } + } + return { addedGroups, addedPhrases, updatedPhrases }; +} + +async function importDataNow() { + const input = $('import-file-input'); + if (!input) { + showToast('导入入口不可用', 'error'); + return; + } + input.value = ''; + input.click(); +} + +async function applyImportedData(incoming) { + const totalGroups = (incoming.groups || []).length; + const totalPhrases = (incoming.groups || []).reduce( + (n, g) => n + ((g && g.phrases) ? g.phrases.length : 0), 0 + ); + const choice = await openPromptModal({ + title: '导入短语', + message: `检测到 ${totalGroups} 个分组、${totalPhrases} 条短语。\n输入「替换」将覆盖现有数据,输入其他内容或留空则按 ID 合并。`, + defaultValue: '', + placeholder: '替换 / 合并', + okLabel: '确定', + icon: 'info', + showInput: true, + }); + if (choice == null) return; + const mode = String(choice).trim(); + if (mode === '替换') { + // 修复:导入数据里 groups 为空时不应该把整个分组列表清空, + // 否则 activeGroupId 变成 null、UI 进入「请先选择分组」的死锁状态。 + if (!incoming.groups || incoming.groups.length === 0) { + showToast('导入数据中没有分组,已取消替换', 'error'); + return; + } + // 替换前先索引一下当前 data 里所有短语的 useCount: + // 用户经常「导出备份 → 删了几条 → 导入同一份备份」来撤销删短语, + // 备份里那条的 useCount 是 0,但当前 data 里它可能是 87。 + // 盲替换会让所有累计使用次数归零,这是毫无意义的统计回退。 + const existingUseCount = new Map(); + for (const g of data.groups || []) { + for (const p of g.phrases || []) { + existingUseCount.set(p.id, p.useCount || 0); + } + } + const groups = (incoming.groups || []).map((g) => { + // 替换模式也要守住「content 必须是字符串」这道关。 + // 合并模式在 mergeImported 里逐条 continue 跳过了,这里直接过滤掉。 + const safePhrases = (g.phrases || []) + .filter((p) => p && typeof p.content === 'string') + .map((p) => { + const id = (typeof p.id === 'string' && p.id) ? p.id : uid('phr'); + return { + id, + title: typeof p.title === 'string' ? p.title : '', + content: p.content, + fav: !!p.fav, + // 导入文件带 useCount 用导入的,否则从本地旧数据里找回(同 id 的累积计数) + useCount: typeof p.useCount === 'number' + ? p.useCount + : (existingUseCount.get(id) || 0), + }; + }); + return { + // 保留导入的 ID:替换通常是恢复备份,ID 一致便于后续合并/对比/调试 + id: (typeof g.id === 'string' && g.id) ? g.id : uid('grp'), + name: typeof g.name === 'string' && g.name ? g.name : '未命名分组', + phrases: safePhrases, + }; + }); + // activeGroupId 必须在新 groups 数组构建完之后再算 —— 旧版本放在对象字面量里 + // 写成 null、再下一行再赋值,逻辑没错但读起来很怪,重组后更清晰。 + data = { + groups, + recent: [], + settings: data.settings, + activeGroupId: groups[0]?.id || null, + }; + showToast(`已替换:${totalPhrases} 条短语`, 'success'); + } else { + const r = mergeImported(incoming); + showToast( + `已合并:新增 ${r.addedGroups} 分组 / ${r.addedPhrases} 短语,更新 ${r.updatedPhrases} 短语`, + 'success' + ); + } + selectedPhraseIndex = -1; + currentFilter = 'all'; + // 导入后整张短语库都变了,索引必须重建 + rebuildPhraseIndex(); + // 导入必须同步刷盘,原因有两个: + // 1. 主进程持有 cachedDataSnapshot,「最近」清空等场景需要这次 save 才能拿到新快照; + // 走异步 persist() → 200ms 防抖窗口里 scheduleSaveBounds 仍可能用旧快照覆盖磁盘 + // 2. beforeunload 时已经走过 flushSync,再走异步 persist 不会更快落盘 + if (window.api.saveDataSync) { + try { window.api.saveDataSync(data); } + catch (e) { console.error('saveDataSync failed:', e); } + } else { + persist(); + } + renderAll(); +} + +function togglePanel(id, show) { + const panel = $(id); + if (!panel) return; + if (show == null) show = panel.classList.contains('hidden'); + panel.classList.toggle('hidden', !show); + if (show) { + if (window.api.setTyping) window.api.setTyping(true); + if (id === 'settings-panel') refreshSettingsUI(); + } else { + if (isAnyPanelOpen()) return; + if (window.api.setTyping) window.api.setTyping(false); + } +} + +// 把 data.settings.opacity 同步到主进程 + 设置面板 UI。启动和 refreshSettingsUI 都复用, +// 避免重复 IPC(旧的实现 init() 调一次、refreshSettingsUI 又调一次共两遍)。 +function applyOpacityFromSettings() { + const s = (data && data.settings) || {}; + const op = s.opacity != null ? s.opacity : 100; + const slider = $('opacity-slider'); + if (slider) slider.value = String(op); + const opVal = $('opacity-value'); + if (opVal) opVal.textContent = String(op); + if (window.api.setOpacity) { + try { window.api.setOpacity(op / 100); } catch {} + } +} + +function refreshSettingsUI() { + const s = data.settings || {}; + document.body.dataset.theme = s.theme || 'dark'; + $$('#theme-segmented .seg-btn').forEach((b) => { + b.classList.toggle('active', b.dataset.theme === (s.theme || 'dark')); + }); + applyOpacityFromSettings(); + const act = s.clickAction || 'paste'; + $$('#click-action-segmented .seg-btn').forEach((b) => { + b.classList.toggle('active', b.dataset.action === act); + }); + const hide = $('hide-on-paste-toggle'); + if (hide) hide.checked = !!s.hideOnPaste; + const rec = $('recent-toggle'); + if (rec) rec.checked = s.recordRecent !== false; + refreshRecentDesc(); + refreshUseCountDesc(); + const pathEl = $('data-path-value'); + // 总是向主进程要一次最新路径:旧的「只在为占位符时拉取」实现里, + // 一旦该字段因为其他原因先被改写过(比如未来给用户展示相对路径), + // 真实的绝对路径就再也不会被写回了。改成无条件更新,行为更稳。 + if (pathEl && window.api.getDataPath) { + window.api.getDataPath().then((p) => { if (p) pathEl.textContent = p; }); + } +} + +function refreshRecentDesc() { + const desc = $('recent-count-desc'); + if (!desc) return; + const n = (data.recent || []).length; + desc.textContent = n === 0 ? '还没有记录' : `当前共 ${n} 条最近使用记录`; +} + +// 「已用 N 次」徽章的累计统计:跨所有分组的 useCount 求和, +// 让用户在清空前能看到总次数,而不是只能凭印象猜。 +function refreshUseCountDesc() { + const desc = $('usecount-count-desc'); + if (!desc) return; + let total = 0; + let countedPhrases = 0; + for (const g of data.groups || []) { + for (const p of g.phrases || []) { + const c = p.useCount || 0; + if (c > 0) { + total += c; + countedPhrases++; + } + } + } + if (total === 0) desc.textContent = '还没有使用记录'; + else desc.textContent = `已累计 ${total} 次使用(${countedPhrases} 条短语)`; +} + +function initSettings() { + $$('#theme-segmented .seg-btn').forEach((btn) => { + btn.addEventListener('click', () => { + const theme = btn.dataset.theme; + data.settings.theme = theme; + document.body.dataset.theme = theme; + $$('#theme-segmented .seg-btn').forEach((b) => b.classList.toggle('active', b === btn)); + persist(); + }); + }); + const slider = $('opacity-slider'); + if (slider) { + // 拖滑块时 input 事件 60+ fps 触发,每次都走 IPC + 主进程 setOpacity 不可忽略。 + // 用 rAF 把多次 input 合并成每帧最多一次 IPC(节流到 ~60fps 上限), + // 落盘仍每次触发(200ms 防抖窗口内会合并,不会打爆磁盘)。 + let opacityRaf = null, pendingOpacity = null; + slider.addEventListener('input', (e) => { + const v = Number(e.target.value); + const opVal = $('opacity-value'); + if (opVal) opVal.textContent = String(v); + data.settings.opacity = v; + pendingOpacity = v / 100; + if (opacityRaf) return; + opacityRaf = requestAnimationFrame(() => { + opacityRaf = null; + const next = pendingOpacity; + pendingOpacity = null; + if (next != null && window.api.setOpacity) { + try { window.api.setOpacity(next); } catch {} + } + }); + persist(); + }); + } + $$('#click-action-segmented .seg-btn').forEach((btn) => { + btn.addEventListener('click', () => { + data.settings.clickAction = btn.dataset.action; + $$('#click-action-segmented .seg-btn').forEach((b) => b.classList.toggle('active', b === btn)); + persist(); + }); + }); + const hide = $('hide-on-paste-toggle'); + if (hide) { + hide.addEventListener('change', () => { + data.settings.hideOnPaste = hide.checked; + persist(); + }); + } + const rec = $('recent-toggle'); + if (rec) { + rec.addEventListener('change', () => { + data.settings.recordRecent = rec.checked; + if (!rec.checked) data.recent = []; + persist(); + renderAll(); + refreshRecentDesc(); + }); + } + const clr = $('btn-clear-recent'); + if (clr) { + clr.addEventListener('click', async () => { + const yes = await confirmUser('确认清空所有最近使用记录?', { title: '清空最近' }); + if (!yes) return; + data.recent = []; + persist(); + renderAll(); + refreshRecentDesc(); + showToast('已清空最近记录', 'success'); + }); + } + // 「已用 N 次」徽章的累计清零:useCount 与「最近」列表是独立统计, + // 所以走自己的按钮,不会顺带影响最近使用历史。 + const clrUse = $('btn-clear-usecount'); + if (clrUse) { + clrUse.addEventListener('click', async () => { + let total = 0; + for (const g of data.groups || []) { + for (const p of g.phrases || []) total += p.useCount || 0; + } + if (total === 0) { + showToast('还没有使用记录', 'info'); + return; + } + const yes = await confirmUser( + `确认将所有短语的「已用 N 次」清零?当前累计 ${total} 次使用。`, + { title: '清空使用统计' } + ); + if (!yes) return; + for (const g of data.groups || []) { + for (const p of g.phrases || []) p.useCount = 0; + } + persist(); + renderAll(); + refreshUseCountDesc(); + showToast('已清空使用统计', 'success'); + }); + } +} + +/* ==================== 全局键盘快捷键 ==================== +// README 里宣传的快捷键之前完全没有 keydown 监听器,全部都失效。 +// 这里统一处理:优先级 弹窗 > 输入框 > 列表导航。 +// +// 设计要点: +// 1. 弹窗/面板打开时只处理 Esc / Ctrl+Enter 等弹窗内的快捷键, +// 不响应 Ctrl+B / g/f/r 等全局快捷键,避免误触。 +// 2. 焦点在输入框 / 文本域时,普通字符键(g/f/r/数字/斜杠)不触发, +// 让用户能正常输入。Ctrl 系列、Esc、Alt+方向 仍然响应。 +// 3. 数字键直触 1~9 走的是「按当前可见顺序的前 9 条」, +// 通过 visiblePhrases() 拿到的列表,搜索 / 过滤器都会影响它。 */ +function setupKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + const modalOpen = !$('modal').classList.contains('hidden'); + const promptOpen = !$('prompt-modal').classList.contains('hidden'); + const settingsOpen = !$('settings-panel').classList.contains('hidden'); + const anyPanelOpen = modalOpen || promptOpen || settingsOpen; + + const active = document.activeElement; + const tag = active && active.tagName; + const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || (active && active.isContentEditable); + // 搜索框获得焦点时再单独判一次,便于决定 Esc / Enter 的行为 + const isSearch = active === $('search-input'); + + /* ====== 1. 弹窗内的快捷键(最高优先级) ====== */ + if (modalOpen) { + // Ctrl+Enter 保存短语 + if (e.ctrlKey && !e.shiftKey && !e.altKey && (e.key === 'Enter' || e.key === '\n')) { + e.preventDefault(); + if (!$('modal-save').disabled) savePhraseFromModal(); + return; + } + // Esc 关闭短语弹窗 + if (e.key === 'Escape') { + e.preventDefault(); + closePhraseModal(); + return; + } + // 弹窗打开时不响应其它快捷键 + return; + } + if (promptOpen) { + // Esc 关闭提示弹窗(按钮上的 click 已经监听) + if (e.key === 'Escape') { + e.preventDefault(); + $('prompt-cancel').click(); + return; + } + // 提示弹窗里 Enter 直接确认 + if (e.key === 'Enter') { + e.preventDefault(); + $('prompt-ok').click(); + return; + } + return; + } + if (settingsOpen) { + if (e.key === 'Escape') { e.preventDefault(); togglePanel('settings-panel', false); return; } + return; + } + + /* ====== 2. Esc 顶层兜底:清空搜索 / 隐藏窗口 ====== */ + if (e.key === 'Escape') { + if (isSearch || searchQuery) { + e.preventDefault(); + const si = $('search-input'); + if (si) si.value = ''; + setSearchQuery(''); + if (isSearch) si.blur(); + return; + } + // 没有任何面板打开、也不在搜索 → 隐藏到托盘 + if (window.api.hideWindow) { + e.preventDefault(); + window.api.hideWindow(); + } + return; + } + + /* ====== 3. Ctrl / Meta 系列:不管焦点在哪都响应 ====== */ + if (e.ctrlKey || e.metaKey) { + const k = (e.key || '').toLowerCase(); + if (e.shiftKey) { + // Ctrl+Shift+V 由主进程的 globalShortcut 接管,这里只挡一下默认行为 + if (k === 'v') { e.preventDefault(); return; } + } + switch (k) { + case 'n': + e.preventDefault(); + openPhraseModal(); + return; + case 'g': + e.preventDefault(); + addGroup(); + return; + case 'b': + e.preventDefault(); + toggleSidebar(); + return; + case 'f': + e.preventDefault(); + { + const si = $('search-input'); + if (si) { si.focus(); si.select(); } + } + return; + case ',': + e.preventDefault(); + // 不传 show 参数 → togglePanel 内部走「根据当前 hidden 状态反转」分支, + // 这样再按一次 Ctrl+, 能正确关闭设置面板(之前传 true 永远显示)。 + togglePanel('settings-panel'); + return; + } + return; + } + + /* ====== 4. Alt + 方向键:调整顺序(在输入框里也能用) ====== */ + if (e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { + e.preventDefault(); + const phrases = visiblePhrases(); + const p = phrases[selectedPhraseIndex]; + if (p) movePhrase(p, e.key === 'ArrowDown' ? 1 : -1); + return; + } + + /* ====== 5. 输入框内只看 Alt+方向 / Esc,其它字符都让浏览器处理 ====== */ + if (isInput) return; + + /* ====== 6. 字符类快捷键:/、g/f/r、1~9 ====== */ + // ? 原先是打开帮助面板的,现在帮助面板已经从界面去掉,删掉避免误触后无效果 + if (e.key === '/') { + e.preventDefault(); + const si = $('search-input'); + if (si) { si.focus(); si.select(); } + return; + } + if (e.key === 'g' || e.key === 'G') { e.preventDefault(); setCurrentFilter('all'); return; } + if (e.key === 'f' || e.key === 'F') { e.preventDefault(); setCurrentFilter('fav'); return; } + if (e.key === 'r' || e.key === 'R') { e.preventDefault(); setCurrentFilter('recent'); return; } + + // 数字键直触:当前可见顺序的前 9 条 + if (e.key >= '1' && e.key <= '9') { + const idx = Number(e.key) - 1; + const phrases = visiblePhrases(); + if (phrases[idx]) { + e.preventDefault(); + // 选中态同步:actOnSelected 不会改 .selected,但 actOnSelected 内部会把 + // items[selectedPhraseIndex] 加 .fired 触发动画,所以这里只需手动同步选中高亮。 + // 否则按完 5 视觉上还在原选中上,下次按 ↓ 仍跳到原 idx——反馈错位。 + selectedPhraseIndex = idx; + $$('.phrase-item').forEach((el, i) => { + el.classList.toggle('selected', i === idx); + }); + actOnSelected(); + } + return; + } + + /* ====== 7. 方向键导航 + Enter 触发 ====== */ + if (e.key === 'ArrowDown') { + e.preventDefault(); + moveSelection(1); + return; + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + moveSelection(-1); + return; + } + if (e.key === 'Enter') { + const phrases = visiblePhrases(); + if (phrases.length === 0) return; + if (selectedPhraseIndex < 0) { + // 搜索/重渲染后还没用方向键选过:Enter 默认取第一条, + // 与数字键 1 的行为保持一致(避免输入完搜索词立刻按 Enter 无响应) + selectedPhraseIndex = 0; + $$('.phrase-item').forEach((el, i) => el.classList.toggle('selected', i === 0)); + } + e.preventDefault(); + actOnSelected(); + return; + } + }); +} + +async function init() { + data = await window.api.loadData(); + // 防御性兜底:主进程在 whenReady 之前一切异常都可能导致返回 null/undefined, + // 此处不能让 null 走到下面的 data.groups / data.recent 访问上。 + if (!data || typeof data !== 'object') data = { groups: [], recent: [], settings: {} }; + if (!data.groups) data.groups = []; + if (!data.recent) data.recent = []; + if (!data.settings) { + data.settings = { + theme: 'dark', opacity: 100, clickAction: 'paste', + hideOnPaste: false, recordRecent: true, + alwaysOnTop: true, + }; + } + // 老数据没有 alwaysOnTop 时按"开启"处理(保持历史行为) + if (typeof data.settings.alwaysOnTop !== 'boolean') { + data.settings.alwaysOnTop = true; + } + if (!data.groups.find((g) => g.id === data.activeGroupId)) { + data.activeGroupId = data.groups[0]?.id || null; + } + if (!data.activeGroupId && data.groups.length > 0) { + data.activeGroupId = data.groups[0].id; + } + + // 短语 ID → {group, phrase, index} 索引:让 findPhraseOwner / recordRecent 走 O(1) 查找。 + // 启动期 / 导入完成后都重建一次,后续 mutate 入口(add/delete/move/import)后也会重建。 + rebuildPhraseIndex(); + + document.body.dataset.theme = data.settings.theme || 'dark'; + // 透明度初始化:刷新 slider / 标签和发 setOpacity IPC 都由下面的 + // refreshSettingsUI 统一做一次(refreshSettingsUI 内部会调 applyOpacityFromSettings), + // 不要再单独跑一次,否则启动期会多发一次 setOpacity 跨进程调用。 + + // 把磁盘上的置顶偏好同步到主进程;主进程在 createWindow 里已经读过一次, + // 这里再做一次幂等更新,避免数据被外部修改(导入/手动编辑)后状态不一致。 + if (window.api.setAlwaysOnTop) { + try { window.api.setAlwaysOnTop(data.settings.alwaysOnTop); } catch {} + } + applyPinButtonUI(data.settings.alwaysOnTop); + + // 先应用折叠态再渲染,避免首屏出现「先展开再收缩」的闪烁 + applySidebarState(); + + renderAll(); + refreshSettingsUI(); + + $('btn-hide').addEventListener('click', () => window.api.hideWindow && window.api.hideWindow()); + // 两份按钮都绑同一个 toggle:侧栏展开时 sidebar-footer 的「折叠」按钮可见, + // 侧栏折叠时 add-bar 左边的「展开」按钮可见,两者互斥显示。 + $('btn-toggle-sidebar').addEventListener('click', toggleSidebar); + const expandBtn = $('btn-expand-sidebar'); + if (expandBtn) expandBtn.addEventListener('click', toggleSidebar); + + // 标题栏设置按钮:直接打开设置面板 + $('btn-settings').addEventListener('click', () => togglePanel('settings-panel', true)); + + // 置顶开关:标题栏上的小按钮,点击即时切换窗口层级 + const pinBtn = $('btn-pin-top'); + if (pinBtn) { + pinBtn.addEventListener('click', async () => { + const next = !data.settings.alwaysOnTop; + // 先发 IPC(主进程会调 setAlwaysOnTop),再落盘、再切 UI。 + // 失败也不阻塞 UI:用户点了就当生效,最坏下次启动再修正。 + let ok = next; + if (window.api.setAlwaysOnTop) { + try { + const res = await window.api.setAlwaysOnTop(next); + // 主进程返回应用后的真实状态(理论上永远等于 next,但保留防御) + if (typeof res === 'boolean') ok = res; + } catch {} + } + data.settings.alwaysOnTop = ok; + applyPinButtonUI(ok); + persist(); + showToast(ok ? '已开启:始终置顶' : '已关闭:始终置顶', 'info'); + }); + } + + const reveal = async () => { + const ok = await window.api.revealDataFile(); + if (!ok) showToast('打开文件管理器失败', 'error'); + }; + const revealBtn = $('btn-reveal-file-2'); + if (revealBtn) revealBtn.addEventListener('click', reveal); + + // 「关于」区:链接 + 右侧「打开」按钮都通过 IPC 跳到主进程 + // 用 shell.openExternal 走系统默认浏览器,避免在当前 Electron 窗口里直接跳转。 + const openDeveloper = async () => { + const link = $('link-developer'); + const url = link ? link.getAttribute('href') : ''; + if (!url) return; + const ok = await window.api.openExternal(url); + if (!ok) showToast('打开链接失败', 'error'); + }; + const openDevBtn = $('btn-open-developer'); + if (openDevBtn) openDevBtn.addEventListener('click', openDeveloper); + const devLink = $('link-developer'); + if (devLink) devLink.addEventListener('click', (e) => { + // 阻止默认导航(避免把 Electron 窗口带飞),统一走 IPC + e.preventDefault(); + openDeveloper(); + }); + + // 面板关闭:data-close 按钮 + 点遮罩区域 + document.addEventListener('click', (e) => { + const closeBtn = e.target.closest('[data-close]'); + if (closeBtn) { + const panel = closeBtn.closest('.panel'); + if (panel) togglePanel(panel.id, false); + return; + } + if (e.target.classList && e.target.classList.contains('panel-backdrop')) { + const panel = e.target.closest('.panel'); + if (panel) togglePanel(panel.id, false); + } + }); + + // 数据路径展示在 refreshSettingsUI 内按需懒加载(首次打开设置面板时才请求主进程), + // 这里不必再额外调一次 getDataPath,避免与 refreshSettingsUI 抢着写 DOM 引发竞态。 + + $('btn-add-group').addEventListener('click', addGroup); + + $('btn-add-phrase').addEventListener('click', () => openPhraseModal()); + + // 空状态里的快速「添加第一条」按钮 + const emptyAdd = $('empty-add-btn'); + if (emptyAdd) emptyAdd.addEventListener('click', () => openPhraseModal()); + + $('phrase-list').addEventListener('click', onPhraseListClick); + + // 侧栏顶部「收藏 / 最近」点击:切换过滤视图, + // 再点一次已激活的项则退回「全部」(侧栏没有「全部」按钮,需要这个出口)。 + $$('.quick-item').forEach((el) => { + el.addEventListener('click', () => toggleQuickFilter(el.dataset.filter)); + }); + + // IME 拼写期间(input 在 composition 中)只同步清空按钮可见性, + // 搜索推迟到 compositionend 之后再触发,避免拼音过程里搜索结果不停闪烁。 + let searchComposing = false; + $('search-input').addEventListener('compositionstart', () => { + searchComposing = true; + }); + $('search-input').addEventListener('compositionend', (e) => { + searchComposing = false; + setSearchQuery(e.target.value); + }); + $('search-input').addEventListener('input', (e) => { + if (searchComposing) { + syncSearchClearVisibility(); + return; + } + setSearchQuery(e.target.value); + }); + $('search-clear').addEventListener('click', () => { + $('search-input').value = ''; + setSearchQuery(''); + $('search-input').focus(); + }); + + $('modal-close').addEventListener('click', closePhraseModal); + $('modal-cancel').addEventListener('click', closePhraseModal); + $('modal-save').addEventListener('click', savePhraseFromModal); + $('modal').addEventListener('click', (e) => { + if (e.target.id === 'modal') closePhraseModal(); + }); + $('phrase-title').addEventListener('input', refreshCounter); + $('phrase-content').addEventListener('input', () => { + refreshSaveButtonState(); + refreshCounter(); + renderPlaceholderPreview(); + }); + + initSettings(); + setupKeyboardShortcuts(); + + $('btn-export').addEventListener('click', exportDataNow); + $('btn-import').addEventListener('click', importDataNow); + const importInput = $('import-file-input'); + if (importInput) { + importInput.addEventListener('change', async (e) => { + const file = e.target.files && e.target.files[0]; + if (!file) return; + try { + const text = await file.text(); + const incoming = JSON.parse(text); + if (!incoming || !Array.isArray(incoming.groups)) { + showToast('导入失败:文件格式不合法(缺少 groups 数组)', 'error'); + return; + } + await applyImportedData(incoming); + } catch (err) { + showToast(`导入失败:${err.message || err}`, 'error'); + } finally { + importInput.value = ''; + } + }); + } + + refreshSaveButtonState(); + refreshCounter(); +} + +init().catch((e) => { + console.error('init failed:', e); + showToast('初始化失败,尝试重启应用', 'error'); +}); + +// 全局错误兜底:把渲染层的未捕获异常和未处理的 Promise 拒绝落地到日志, +// 避免 Chromium 默默吞掉、用户只看到界面卡死不知道发生了什么。 +window.addEventListener('error', (e) => { + console.error('[renderer error]', e.error || e.message); +}); +window.addEventListener('unhandledrejection', (e) => { + console.error('[renderer unhandledrejection]', e.reason); +}); diff --git a/renderer/styles.css b/renderer/styles.css new file mode 100644 index 0000000..df6bc4c --- /dev/null +++ b/renderer/styles.css @@ -0,0 +1,1939 @@ +/* ============================================================ + 快速粘贴 — 精致深色主题(基于 Linear/Notion 设计语言) + ============================================================ */ + +:root { + /* Backgrounds */ + --bg-base: #08090A; + --bg-elevated: #0D0E10; + --bg-overlay: #161719; + --bg-hover: #1B1C1F; + --bg-active: #262830; + --bg-input: #0F1012; + + /* Borders */ + --border-subtle: rgba(255, 255, 255, 0.06); + --border-default: rgba(255, 255, 255, 0.09); + --border-strong: rgba(255, 255, 255, 0.14); + + /* Text */ + --text-primary: #F7F8F8; + --text-secondary: rgba(247, 248, 248, 0.66); + --text-tertiary: rgba(247, 248, 248, 0.42); + --text-disabled: rgba(247, 248, 248, 0.25); + + /* Accent */ + --accent: #5E6AD2; + --accent-hover: #7170FF; + --accent-active: #4F5BC8; + --accent-subtle: rgba(94, 106, 210, 0.13); + --accent-glow: rgba(94, 106, 210, 0.28); + /* Accent 半透明色:用于 toggle 激活态 hover、激活边线、占位符 mark 等 */ + --accent-hover-bg: rgba(94, 106, 210, 0.18); + --accent-border: rgba(94, 106, 210, 0.22); + --accent-mark-bg: rgba(94, 106, 210, 0.22); + --accent-pulse: rgba(94, 106, 210, 0.45); + + /* Status */ + --danger: #F87171; + --danger-hover: #FCA5A5; + --danger-subtle: rgba(248, 113, 113, 0.13); + --success: #4CB782; + --success-subtle: rgba(76, 183, 130, 0.13); + --warning: #FBBF24; + --warning-subtle: rgba(251, 191, 36, 0.13); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 16px 32px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-glow: 0 0 0 1px var(--accent-glow), 0 0 16px var(--accent-glow); + + /* Radius */ + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 10px; + --radius-xl: 14px; + + /* Motion */ + --ease: cubic-bezier(0.16, 1, 0.3, 1); + --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1); + --duration-fast: 120ms; + --duration-base: 180ms; + --duration-slow: 260ms; +} + +/* ===== 亮色主题覆盖 ===== */ +body[data-theme="light"] { + --bg-base: #FAFAF9; + --bg-elevated: #FFFFFF; + --bg-overlay: #FFFFFF; + --bg-hover: #F4F4F2; + --bg-active: #E8E8E5; + --bg-input: #FAFAF9; + + --border-subtle: rgba(15, 17, 21, 0.06); + --border-default: rgba(15, 17, 21, 0.09); + --border-strong: rgba(15, 17, 21, 0.14); + + --text-primary: #1A1B1E; + --text-secondary: rgba(15, 17, 21, 0.66); + --text-tertiary: rgba(15, 17, 21, 0.42); + --text-disabled: rgba(15, 17, 21, 0.25); + + --accent: #5563D3; + --accent-hover: #4F5BC8; + --accent-active: #414FB8; + --accent-subtle: rgba(85, 99, 211, 0.10); + --accent-glow: rgba(85, 99, 211, 0.22); + --accent-hover-bg: rgba(85, 99, 211, 0.14); + --accent-border: rgba(85, 99, 211, 0.22); + --accent-mark-bg: rgba(85, 99, 211, 0.20); + --accent-pulse: rgba(85, 99, 211, 0.35); + + --danger: #DC2626; + --danger-hover: #B91C1C; + --danger-subtle: rgba(220, 38, 38, 0.08); + --success: #15803D; + --success-subtle: rgba(21, 128, 61, 0.08); + --warning: #B45309; + --warning-subtle: rgba(180, 83, 9, 0.10); + + --shadow-sm: 0 1px 2px rgba(15, 17, 21, 0.06); + --shadow-md: 0 4px 12px rgba(15, 17, 21, 0.08), 0 1px 3px rgba(15, 17, 21, 0.04); + --shadow-lg: 0 16px 32px rgba(15, 17, 21, 0.12), 0 2px 8px rgba(15, 17, 21, 0.06); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", + "Microsoft YaHei", "PingFang SC", sans-serif; + font-size: 13px; + font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: var(--text-primary); + background: var(--bg-base); + overflow: hidden; + user-select: none; + letter-spacing: -0.01em; + transition: background var(--duration-base) var(--ease), + color var(--duration-base) var(--ease); +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + outline: none; + background: transparent; + color: var(--text-primary); + letter-spacing: inherit; +} + +input, textarea { + font-family: inherit; + font-size: 13px; + color: var(--text-primary); + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + padding: 8px 10px; + outline: none; + user-select: text; + transition: border-color var(--duration-fast) var(--ease), + box-shadow var(--duration-fast) var(--ease), + background var(--duration-fast) var(--ease); +} + +input::placeholder, textarea::placeholder { + color: var(--text-tertiary); +} + +input:hover, textarea:hover { + border-color: var(--border-strong); +} + +input:focus, textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-subtle); +} + +/* ===== 通用图标系统 ===== */ +.icon { + width: 14px; + height: 14px; + flex-shrink: 0; + display: inline-block; + vertical-align: middle; + fill: currentColor; +} + +/* ===== 标题栏 ===== */ +#titlebar { + height: 44px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px 0 12px; + background: var(--bg-elevated); + border-bottom: 1px solid var(--border-subtle); + -webkit-app-region: drag; + position: relative; + z-index: 10; +} + +#titlebar::after { + content: ''; + position: absolute; + left: 0; right: 0; bottom: -1px; height: 1px; + background: linear-gradient(90deg, + transparent 0%, + var(--accent-subtle) 30%, + var(--accent-subtle) 70%, + transparent 100%); + opacity: 0.65; + pointer-events: none; +} + +#titlebar .title { + display: flex; + align-items: center; + gap: 10px; +} + +#titlebar .logo { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + /* 图标本身已经有自己的配色和背景,不再叠渐变; + 浅薄边/阴影保留下来,确保浅色主题下图标轮廓也能看见。 */ + background: transparent; + box-shadow: 0 0 0 1px rgba(255,255,255,0.08), 0 2px 8px var(--accent-glow); +} + +/* 不再铺满 logo 容器,缩小到 18px 留 3px 内边距:和标题栏文字字号(13px)拉开一档, + 视觉上更精致,也避免紧贴边框显得拥挤。 + ICO 内置 16/32/48 多尺寸,Chromium 会自动挑最接近的那一档, + 无需指定具体 width/height(反而会让高 DPI 屏显得模糊)。 */ +#titlebar .logo .logo-img { + width: 18px; + height: 18px; + display: block; + /* ICO 解码出来的位图默认走 nearest-neighbor 在缩小时会发糊,这里切到平滑。 */ + image-rendering: -webkit-optimize-contrast; +} + +#titlebar .title-name { + font-weight: 600; + font-size: 13px; + color: var(--text-primary); + letter-spacing: -0.01em; +} + +#titlebar .title-actions { + display: flex; + align-items: center; + gap: 2px; + -webkit-app-region: no-drag; +} + +/* ===== 下拉菜单(标题栏 ⋯ / 分组 ⋯)已移除:右键分组用的是 .context-menu, + 标题栏没有 ⋯ 按钮。下方的 .dropdown-* 选择器和 @keyframes dropdown-in 已清理。 ===== */ + +.icon-btn { + width: 28px; + height: 28px; + border-radius: var(--radius-md); + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + background: transparent; + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease), + transform var(--duration-fast) var(--ease); + position: relative; +} + +.icon-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.icon-btn:active { + background: var(--bg-active); + transform: scale(0.94); +} + +.icon-btn .icon { + width: 15px; + height: 15px; +} + +/* 标题栏里的开关按钮(如置顶):激活时高亮,使用 accent 色暗示「已开启」 */ +.icon-btn.toggle-on { + background: var(--accent-subtle); + color: var(--accent); +} + +.icon-btn.toggle-on:hover { + background: var(--accent-hover-bg); + color: var(--accent-hover); +} + +body[data-theme="light"] .icon-btn.toggle-on:hover { + background: var(--accent-hover-bg); +} + +/* ===== 通用按钮 ===== */ +.mini-btn { + width: 22px; + height: 22px; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-secondary); + display: inline-flex; + align-items: center; + justify-content: center; + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease), + transform var(--duration-fast) var(--ease); +} + +.mini-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.mini-btn:active { + background: var(--bg-active); + transform: scale(0.92); +} + +.mini-btn .icon { + width: 12px; + height: 12px; +} + +.text-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 9px; + border-radius: var(--radius-md); + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + background: transparent; + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease); +} + +.text-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.text-btn:active { + background: var(--bg-active); +} + +.text-btn.danger:hover { + background: var(--danger-subtle); + color: var(--danger); +} + +.text-btn .icon { + width: 12px; + height: 12px; +} + +.primary-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 7px 14px; + border-radius: var(--radius-md); + font-size: 12.5px; + font-weight: 500; + background: var(--accent); + color: #fff; + transition: background var(--duration-fast) var(--ease), + box-shadow var(--duration-fast) var(--ease), + transform var(--duration-fast) var(--ease); + box-shadow: 0 0 0 1px rgba(255,255,255,0.05) inset, + 0 1px 2px rgba(0,0,0,0.3); +} + +.primary-btn:hover { + background: var(--accent-hover); + box-shadow: 0 0 0 1px rgba(255,255,255,0.08) inset, + 0 0 0 4px var(--accent-subtle), + 0 1px 2px rgba(0,0,0,0.3); +} + +.primary-btn:active { + background: var(--accent-active); + transform: translateY(0.5px); +} + +.primary-btn:disabled { + background: var(--bg-active); + color: var(--text-disabled); + cursor: not-allowed; + box-shadow: none; + transform: none; +} + +.primary-btn:disabled:hover { + background: var(--bg-active); + box-shadow: none; +} + +.primary-btn .icon { + width: 14px; + height: 14px; +} + +/* ===== 主体 ===== */ +#main { + display: flex; + height: calc(100% - 44px); +} + +/* ===== 左侧分组 ===== */ +#sidebar { + width: 112px; + background: var(--bg-elevated); + border-right: 1px solid var(--border-subtle); + display: flex; + flex-direction: column; + flex-shrink: 0; + overflow: hidden; + transition: width var(--duration-base) var(--ease), + border-right-width var(--duration-base) var(--ease), + opacity var(--duration-fast) var(--ease); +} + +/* 折叠态:width 收到 0,content 自然铺满剩余宽度 */ +#main.sidebar-collapsed #sidebar { + width: 0; + border-right-width: 0; + opacity: 0; + pointer-events: none; +} + +/* 折叠 / 展开按钮(原 content-expand-btn)已统一到 .add-bar 的左侧,这里不再保留样式。 + 新位置请见 .add-bar 段。*/ + +/* ===== 侧栏顶部:收藏 / 最近(独立区域,跨分组的快捷入口) ===== */ +.sidebar-quick { + flex-shrink: 0; + padding: 8px 4px 6px; + border-bottom: 1px solid var(--border-subtle); +} + +.quick-item { + position: relative; + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 6px 8px 6px 10px; + margin-bottom: 1px; + border-radius: var(--radius-md); + cursor: pointer; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + background: transparent; + border: 1px solid transparent; + text-align: left; + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease); +} + +.quick-item:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.quick-item.active { + background: var(--accent-subtle); + color: var(--text-primary); + border-color: var(--accent-border); +} + +.quick-item .icon { + width: 14px; + height: 14px; + flex-shrink: 0; + color: var(--text-tertiary); +} + +.quick-item[data-filter="fav"] .icon { + color: var(--warning); +} + +.quick-item[data-filter="recent"] .icon { + color: var(--accent-hover); +} + +.quick-item .name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.quick-item .count { + margin-left: auto; + font-size: 10.5px; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: var(--text-tertiary); + padding: 1px 6px; + border-radius: 999px; + background: var(--bg-hover); + min-width: 18px; + text-align: center; +} + +.quick-item.active .count { + color: var(--text-secondary); + background: rgba(255, 255, 255, 0.07); +} + +.sidebar-section { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 10px 8px; + font-size: 10.5px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-tertiary); +} + +#group-list { + flex: 1; + list-style: none; + overflow-y: auto; + padding: 0 4px 10px; +} + +.group-item { + position: relative; + padding: 6px 8px 6px 10px; + margin-bottom: 1px; + border-radius: var(--radius-md); + cursor: pointer; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 6px; + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease); + border: 1px solid transparent; +} + +.group-item:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.group-item.active { + background: var(--accent-subtle); + color: var(--text-primary); + border-color: var(--accent-border); +} + +.group-item.active::before { + content: ''; + position: absolute; + left: -6px; + top: 50%; + transform: translateY(-50%) scaleY(0.6); + width: 3px; + height: 14px; + background: var(--accent); + border-radius: 2px; + box-shadow: 0 0 8px var(--accent-glow); + opacity: 0; + animation: active-indicator-in 0.32s var(--ease) forwards; +} + +@keyframes active-indicator-in { + from { opacity: 0; transform: translateY(-50%) scaleY(0.4); } + to { opacity: 1; transform: translateY(-50%) scaleY(1); } +} + +.group-item.dragging { + opacity: 0.4; +} + +.group-item.drop-above { + box-shadow: 0 -2px 0 var(--accent); +} + +.group-item.drop-below { + box-shadow: 0 2px 0 var(--accent); +} + +.group-item .name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.group-item .count { + margin-left: auto; + font-size: 10.5px; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: var(--text-tertiary); + padding: 1px 6px; + border-radius: 999px; + background: var(--bg-hover); + min-width: 18px; + text-align: center; +} + +.group-item.active .count { + color: var(--text-secondary); + background: rgba(255,255,255,0.07); +} + +/* 侧栏底部:只放一个折叠按钮,靠右下对齐——尺寸与 add-bar 里的展开按钮一致 */ +.sidebar-footer { + flex-shrink: 0; + padding: 10px 10px 10px 12px; + border-top: 1px solid var(--border-subtle); + display: flex; + justify-content: flex-end; +} + +/* 折叠 / 展开按钮有两个位置: + ① 侧栏展开时:放在 .sidebar-footer 的右下角; + ② 侧栏折叠时:放在 .add-bar 左侧(与「+ 添加短语」并排)。 + 两份按钮的尺寸统一为 32×32,让用户在任何状态下看到的「折叠/展开」控件大小一致。 */ +.sidebar-toggle-btn { + flex-shrink: 0; + width: 32px; + height: 32px; + border-radius: var(--radius-md); + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease), + transform var(--duration-fast) var(--ease); +} + +.sidebar-toggle-btn .icon { + width: 14px; + height: 14px; +} + +/* add-bar 里的展开按钮:与「+ 添加短语」同高,左对齐;尺寸与 .sidebar-toggle-btn 一致 */ +.sidebar-expand-btn { + width: 32px; + height: 32px; + flex-shrink: 0; + border-radius: var(--radius-md); +} + +.sidebar-expand-btn .icon { + width: 14px; + height: 14px; +} + +/* 互斥:侧栏展开时,add-bar 里的展开按钮隐藏;折叠时由 .sidebar-footer 自然被 width:0 裁掉 */ +#main:not(.sidebar-collapsed) .sidebar-expand-btn { + display: none; +} + +.dim { + color: var(--text-tertiary); +} + +/* ===== 右侧内容 ===== */ +#content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--bg-base); + min-width: 0; +} + +.content-header { + padding: 12px 14px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + justify-content: space-between; + background: var(--bg-base); + flex-shrink: 0; +} + +.content-header-left { + display: flex; + align-items: baseline; + gap: 8px; + min-width: 0; +} + +.content-header h2 { + font-size: 14px; + font-weight: 600; + letter-spacing: -0.015em; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.phrase-count { + font-size: 11px; + font-weight: 500; + color: var(--text-tertiary); + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +/* ===== 搜索 ===== */ +.search-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--bg-base); + position: relative; + flex-shrink: 0; +} + +.search-bar .search-icon { + position: absolute; + left: 22px; + top: 50%; + transform: translateY(-50%); + color: var(--text-tertiary); + pointer-events: none; + width: 13px; + height: 13px; +} + +.search-bar input { + width: 100%; + padding: 6px 60px 6px 28px; + font-size: 12.5px; +} + +.search-clear { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + border-radius: 50%; + color: var(--text-tertiary); + background: var(--bg-hover); + display: inline-flex; + align-items: center; + justify-content: center; + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease); +} + +.search-clear:hover { + background: var(--bg-active); + color: var(--text-primary); +} + +.search-clear .icon { + width: 9px; + height: 9px; +} + +/* ===== 短语列表 ===== */ +#phrase-list { + flex: 1; + list-style: none; + overflow-y: auto; + padding: 4px 8px 12px; +} + +.phrase-item { + padding: 10px 12px; + margin-bottom: 4px; + border-radius: var(--radius-md); + cursor: pointer; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + transition: background var(--duration-fast) var(--ease), + border-color var(--duration-fast) var(--ease), + transform var(--duration-fast) var(--ease), + opacity var(--duration-fast) var(--ease), + box-shadow var(--duration-fast) var(--ease); + position: relative; +} + +.phrase-item:hover { + background: var(--bg-hover); + border-color: var(--border-default); +} + +.phrase-item.selected { + background: var(--accent-subtle); + border-color: var(--accent); + box-shadow: 0 0 0 1px var(--accent-subtle); +} + +.phrase-item.fired { + background: var(--accent-subtle); + border-color: var(--accent); + animation: phrase-pulse 0.55s var(--ease); +} + +@keyframes phrase-pulse { + 0% { box-shadow: 0 0 0 0 var(--accent-pulse); transform: scale(1); } + 35% { box-shadow: 0 0 0 6px transparent; transform: scale(1.012); } + 100% { box-shadow: 0 0 0 0 transparent; transform: scale(1); } +} + +.phrase-item.dragging { + opacity: 0.4; +} + +/* 可拖拽短语:鼠标变抓手,光标暗示「可以拖动」 + * 注意不要作用在按钮上:移动端/键盘用户点击操作会更舒服 */ +.phrase-item[draggable="true"] { + cursor: grab; +} +.phrase-item[draggable="true"]:active { + cursor: grabbing; +} +.phrase-item[draggable="true"] button { + cursor: pointer; +} + +.phrase-item.drop-above { + box-shadow: 0 -2px 0 var(--accent); +} + +.phrase-item.drop-below { + box-shadow: 0 2px 0 var(--accent); +} + +.phrase-top { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 3px; +} + +.phrase-fav { + flex-shrink: 0; + color: var(--text-tertiary); + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 4px; + transition: color var(--duration-fast) var(--ease), + background var(--duration-fast) var(--ease); +} + +.phrase-fav:hover { + background: var(--bg-active); +} + +.phrase-fav.on { + color: var(--warning); +} + +.phrase-fav.on .icon { + animation: star-pop 0.36s var(--ease); +} + +.phrase-fav .icon { + width: 12px; + height: 12px; + transition: transform var(--duration-fast) var(--ease); +} + +.phrase-fav:active .icon { + transform: scale(0.82); +} + +@keyframes star-pop { + 0% { transform: scale(1) rotate(0deg); } + 40% { transform: scale(1.45) rotate(-6deg); } + 70% { transform: scale(0.95) rotate(2deg); } + 100% { transform: scale(1) rotate(0deg); } +} + +.phrase-title { + font-weight: 500; + font-size: 13px; + color: var(--text-primary); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.phrase-actions { + flex-shrink: 0; + display: flex; + gap: 1px; + opacity: 0; + transition: opacity var(--duration-fast) var(--ease); +} + +.phrase-item:hover .phrase-actions, +.phrase-item.selected .phrase-actions { + opacity: 1; +} + +.phrase-actions .action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 5px; + color: var(--text-secondary); + background: transparent; + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease), + transform var(--duration-fast) var(--ease); +} + +.phrase-actions .action-btn:hover { + background: var(--bg-active); + color: var(--text-primary); +} + +.phrase-actions .action-btn:active { + transform: scale(0.92); +} + +.phrase-actions .action-btn.danger:hover { + background: var(--danger-subtle); + color: var(--danger); +} + +.phrase-actions .action-btn .icon { + width: 12px; + height: 12px; +} + +.phrase-preview { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-word; + letter-spacing: -0.005em; +} + +.phrase-preview mark { + background: rgba(251, 191, 36, 0.18); + color: var(--warning); + padding: 0 2px; + border-radius: 2px; +} + +.phrase-meta { + display: flex; + align-items: center; + gap: 8px; + margin-top: 6px; + font-size: 10.5px; + color: var(--text-tertiary); +} + +.phrase-meta .use-count { + display: inline-flex; + align-items: center; + gap: 3px; + font-variant-numeric: tabular-nums; +} + +/* ===== 空状态 ===== */ +.empty-hint { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 40px 24px; + gap: 6px; +} + +.empty-hint.hidden { display: none; } + +.empty-illustration { + width: 56px; + height: 56px; + color: var(--text-tertiary); + margin-bottom: 6px; + opacity: 0.6; + animation: empty-breathe 3.6s var(--ease) infinite; +} + +.empty-illustration svg { + width: 100%; + height: 100%; +} + +@keyframes empty-breathe { + 0%, 100% { opacity: 0.55; transform: scale(1) translateY(0); } + 50% { opacity: 0.78; transform: scale(1.04) translateY(-2px); } +} + +/* 进入空状态时让插画先轻快地“落一下”,再进入循环 —— 否则每次显示都会从中间态跳一下 */ +.empty-illustration.run-in { + animation: empty-breathe-in 0.5s var(--ease) both, + empty-breathe 3.6s var(--ease) 0.5s infinite; +} + +@keyframes empty-breathe-in { + from { opacity: 0; transform: scale(0.85) translateY(4px); } + to { opacity: 0.55; transform: scale(1) translateY(0); } +} + +.empty-title { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + letter-spacing: -0.01em; +} + +.empty-desc { + font-size: 11.5px; + color: var(--text-tertiary); + line-height: 1.55; + max-width: 220px; + margin-bottom: 4px; +} + +.empty-action { + margin-top: 6px; + padding: 5px 12px; + font-size: 12px; + font-weight: 500; + color: var(--accent); + background: var(--accent-subtle); + border: 1px solid transparent; + border-radius: var(--radius-md); + transition: background var(--duration-fast) var(--ease), + border-color var(--duration-fast) var(--ease); +} + +.empty-action:hover { + background: var(--accent-hover-bg); + border-color: var(--accent-pulse); +} + +.empty-action.hidden { display: none; } + +/* ===== 添加栏 ===== */ +.add-bar { + padding: 10px 12px; + border-top: 1px solid var(--border-subtle); + background: var(--bg-base); + flex-shrink: 0; + display: flex; + align-items: center; + gap: 8px; +} + +.add-bar .primary-btn { + flex: 1; + height: 32px; + min-width: 0; +} + +/* ===== 通用 .hidden ===== */ +.hidden { + display: none !important; +} + +/* ===== 滚动条 ===== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.08); + border-radius: 999px; + border: 2px solid transparent; + background-clip: padding-box; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.16); + background-clip: padding-box; + border: 2px solid transparent; +} +body[data-theme="light"] ::-webkit-scrollbar-thumb { + background: rgba(15, 17, 21, 0.10); + background-clip: padding-box; + border: 2px solid transparent; +} +body[data-theme="light"] ::-webkit-scrollbar-thumb:hover { + background: rgba(15, 17, 21, 0.20); + background-clip: padding-box; + border: 2px solid transparent; +} + +/* ============================================================ + 弹窗 + ============================================================ */ +.modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + animation: modal-fade var(--duration-base) var(--ease); +} + +.modal.hidden { display: none; } + +.modal-card { + width: 360px; + background: var(--bg-overlay); + border: 1px solid var(--border-default); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + overflow: hidden; + animation: modal-pop var(--duration-base) var(--ease); +} + +body[data-theme="light"] .modal { + background: rgba(15, 17, 21, 0.30); +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--border-subtle); + font-size: 13.5px; + font-weight: 600; + color: var(--text-primary); + letter-spacing: -0.01em; +} + +.modal-title-wrap { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.modal-title-icon { + width: 16px; + height: 16px; + color: var(--accent); +} + +.modal-body { + padding: 16px; +} + +.field-label { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-tertiary); + margin-bottom: 6px; + margin-top: 12px; +} + +.field-label:first-child { margin-top: 0; } + +.field-counter { + font-size: 10.5px; + font-weight: 500; + color: var(--text-tertiary); + text-transform: none; + letter-spacing: 0; + font-variant-numeric: tabular-nums; +} + +.field-counter.warn { + color: var(--warning); +} + +.field-counter.over { + color: var(--danger); +} + +.field-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; + padding: 8px 10px; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); +} + +.field-label-inline { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; + user-select: none; +} + +.field-label-inline input[type="checkbox"] { + appearance: none; + width: 14px; + height: 14px; + border: 1px solid var(--border-strong); + border-radius: 3px; + background: var(--bg-base); + cursor: pointer; + display: inline-grid; + place-content: center; + transition: background var(--duration-fast) var(--ease), + border-color var(--duration-fast) var(--ease); + margin: 0; + padding: 0; +} + +.field-label-inline input[type="checkbox"]:checked { + background: var(--accent); + border-color: var(--accent); +} + +.field-label-inline input[type="checkbox"]:checked::after { + content: ''; + width: 8px; + height: 5px; + border: 1.5px solid #fff; + border-top: none; + border-right: none; + transform: rotate(-45deg) translate(1px, -1px); +} + +.field-label-inline .icon { + width: 12px; + height: 12px; + color: var(--warning); +} + +.field-tip { + font-size: 10.5px; + color: var(--text-tertiary); +} + +.modal-body input, +.modal-body textarea { + width: 100%; + resize: vertical; +} + +.modal-body textarea { + font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', "Microsoft YaHei", monospace; + font-size: 12.5px; + line-height: 1.55; + min-height: 96px; + color: var(--text-primary); +} + +/* 占位符展开预览:仅当内容含 {date|time|...} 时显示 */ +.placeholder-preview { + margin-top: 10px; + padding: 8px 10px; + background: var(--accent-subtle); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); +} + +.placeholder-preview-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-tertiary); + margin-bottom: 4px; +} + +.placeholder-preview-text { + font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', "Microsoft YaHei", monospace; + font-size: 11.5px; + line-height: 1.55; + color: var(--text-secondary); + white-space: pre-wrap; + word-break: break-word; + max-height: 96px; + overflow-y: auto; +} + +.placeholder-preview-text mark { + background: var(--accent-mark-bg); + color: var(--accent-hover); + padding: 0 3px; + border-radius: 3px; +} + +/* 占位符变量说明:折叠面板,节省编辑区空间 */ +.placeholder-hint { + margin-top: 8px; + font-size: 11px; + color: var(--text-tertiary); +} + +.placeholder-hint summary { + cursor: pointer; + user-select: none; + padding: 4px 0; + font-weight: 500; + color: var(--text-secondary); + list-style: none; + display: flex; + align-items: center; + gap: 4px; +} + +.placeholder-hint summary::before { + content: '▸'; + font-size: 10px; + color: var(--text-tertiary); + transition: transform var(--duration-fast) var(--ease); +} + +.placeholder-hint[open] summary::before { + transform: rotate(90deg); +} + +.placeholder-hint summary:hover { + color: var(--text-primary); +} + +.placeholder-hint .placeholder-list { + display: flex; + flex-direction: column; + gap: 4px; + padding: 6px 0 4px 14px; + font-size: 11px; + color: var(--text-tertiary); +} + +.placeholder-hint code { + font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace; + font-size: 10.5px; + padding: 1px 5px; + margin-right: 4px; + background: var(--bg-hover); + color: var(--accent-hover); + border-radius: 3px; + font-weight: 500; +} + +/* 通用提示弹窗 */ +.prompt-card { + width: 340px; +} + +.prompt-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 6px; + background: var(--accent-subtle); + color: var(--accent); +} + +.prompt-icon.danger { + background: var(--danger-subtle); + color: var(--danger); +} + +.prompt-icon.success { + background: var(--success-subtle); + color: var(--success); +} + +.prompt-icon .icon { + width: 13px; + height: 13px; +} + +.prompt-message { + font-size: 13px; + line-height: 1.55; + color: var(--text-secondary); + margin-bottom: 14px; + word-break: break-word; + user-select: text; + /* 消息是用 textContent 写进去的,导入确认框等场景带 \n 换行。 + 默认 white-space 会把换行折叠成空格,两行提示挤成一行。 */ + white-space: pre-line; +} + +.prompt-input { + width: 100%; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 18px; +} + +@keyframes modal-fade { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes modal-pop { + from { + opacity: 0; + transform: scale(0.96) translateY(6px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* ============================================================ + Toast 栈(从右下角滑入) + ============================================================ */ +.toast-stack { + position: fixed; + bottom: 16px; + right: 16px; + display: flex; + flex-direction: column-reverse; + gap: 8px; + z-index: 200; + pointer-events: none; + max-width: 320px; +} + +.toast { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: var(--bg-overlay); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + font-size: 12.5px; + font-weight: 500; + color: var(--text-primary); + box-shadow: var(--shadow-md); + pointer-events: auto; + animation: toast-in var(--duration-base) var(--ease) both; + min-width: 200px; + position: relative; + overflow: hidden; +} + +.toast.fade-out { + animation: toast-out var(--duration-base) var(--ease) both; +} + +.toast::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--text-tertiary); +} + +.toast.success::before { background: var(--success); } +.toast.error::before { background: var(--danger); } +.toast.info::before { background: var(--accent); } + +.toast .toast-icon { + flex-shrink: 0; + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); +} + +.toast.success .toast-icon { color: var(--success); } +.toast.error .toast-icon { color: var(--danger); } +.toast.info .toast-icon { color: var(--accent); } + +.toast .toast-icon .icon { + width: 14px; + height: 14px; +} + +.toast .toast-text { + flex: 1; + line-height: 1.4; + word-break: break-word; +} + +.toast .toast-close { + flex-shrink: 0; + width: 16px; + height: 16px; + border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); + transition: background var(--duration-fast) var(--ease), + color var(--duration-fast) var(--ease); +} + +.toast .toast-close:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.toast .toast-close .icon { + width: 9px; + height: 9px; +} + +@keyframes toast-in { + from { + opacity: 0; + transform: translateX(24px) scale(0.96); + } + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +@keyframes toast-out { + from { + opacity: 1; + transform: translateX(0) scale(1); + } + to { + opacity: 0; + transform: translateX(20px) scale(0.96); + } +} + +/* ============================================================ + 上下文菜单 + ============================================================ */ +.context-menu { + position: fixed; + background: var(--bg-overlay); + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + padding: 4px; + min-width: 180px; + z-index: 150; + animation: ctx-menu-in var(--duration-fast) var(--ease); + user-select: none; +} + +.context-menu.hidden { display: none; } + +.context-menu .ctx-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + border-radius: var(--radius-md); + font-size: 12.5px; + color: var(--text-primary); + cursor: pointer; + transition: background var(--duration-fast) var(--ease); +} + +.context-menu .ctx-item:hover { + background: var(--bg-hover); +} + +.context-menu .ctx-item.danger { + color: var(--danger); +} + +.context-menu .ctx-item.danger:hover { + background: var(--danger-subtle); +} + +.context-menu .ctx-item .icon { + width: 13px; + height: 13px; + color: var(--text-tertiary); +} + +.context-menu .ctx-item.danger .icon { + color: var(--danger); +} + +.context-menu .ctx-divider { + height: 1px; + background: var(--border-subtle); + margin: 4px 0; +} + +@keyframes ctx-menu-in { + from { + opacity: 0; + transform: scale(0.96) translateY(-4px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* ============================================================ + 面板(设置/帮助) + ============================================================ */ +.panel { + position: fixed; + inset: 0; + z-index: 90; + display: flex; + align-items: center; + justify-content: center; + pointer-events: auto; +} + +.panel.hidden { display: none; } + +.panel-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + animation: modal-fade var(--duration-base) var(--ease); +} + +body[data-theme="light"] .panel-backdrop { + background: rgba(15, 17, 21, 0.25); +} + +.panel-card { + position: relative; + background: var(--bg-overlay); + border: 1px solid var(--border-default); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + max-height: calc(100% - 32px); + display: flex; + flex-direction: column; + animation: modal-pop var(--duration-base) var(--ease); + overflow: hidden; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; +} + +.panel-title { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.panel-title .icon { + width: 15px; + height: 15px; + color: var(--accent); +} + +.panel-body { + flex: 1; + overflow-y: auto; + padding: 4px 4px 16px; +} + +/* ===== 设置面板 ===== */ +.settings-card { + width: 440px; +} + +.setting-group { + padding: 14px 16px; +} + +.setting-group + .setting-group { + border-top: 1px solid var(--border-subtle); +} + +.setting-group-title { + font-size: 10.5px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-tertiary); + margin-bottom: 8px; +} + +.setting-row { + display: flex; + align-items: center; + gap: 16px; + padding: 10px 8px; + margin: 0 -8px; + border-radius: var(--radius-md); + transition: background var(--duration-fast) var(--ease); +} + +.setting-row:hover { + background: var(--bg-hover); +} + +.setting-row + .setting-row { + border-top: 1px solid var(--border-subtle); +} + +.setting-row-text { + flex: 1; + min-width: 0; +} + +.setting-label { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 2px; +} + +.setting-desc { + font-size: 11.5px; + color: var(--text-tertiary); + line-height: 1.4; +} + +.setting-desc.mono { + font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace; + font-size: 11px; + word-break: break-all; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.setting-actions { + display: flex; + gap: 4px; +} + +/* 设置面板里的可点击链接(关于区开发者链接等): + 取消默认下划线,用 accent 色 + hover 强化,避免和段落正文混在一起 */ +.setting-link { + color: var(--accent); + text-decoration: none; + word-break: break-all; + transition: color var(--duration-fast) var(--ease); +} + +.setting-link:hover { + color: var(--accent-hover); + text-decoration: underline; + text-underline-offset: 2px; +} + +/* ===== Segmented Control ===== */ +.segmented { + display: inline-flex; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + padding: 2px; + gap: 2px; +} + +.seg-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 4px; + font-size: 11.5px; + font-weight: 500; + color: var(--text-tertiary); + background: transparent; + transition: all var(--duration-fast) var(--ease); +} + +.seg-btn:hover { + color: var(--text-secondary); +} + +.seg-btn.active { + background: var(--bg-elevated); + color: var(--text-primary); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 1px var(--border-default); +} + +.seg-btn .icon { + width: 11px; + height: 11px; +} + +/* ===== Switch ===== */ +.switch { + position: relative; + display: inline-block; + width: 34px; + height: 18px; + flex-shrink: 0; +} + +.switch input { + opacity: 0; + position: absolute; + width: 0; + height: 0; +} + +.switch-slider { + position: absolute; + inset: 0; + background: var(--bg-input); + border: 1px solid var(--border-default); + border-radius: 999px; + cursor: pointer; + transition: background var(--duration-fast) var(--ease), + border-color var(--duration-fast) var(--ease); +} + +.switch-slider::after { + content: ''; + position: absolute; + left: 2px; + top: 50%; + transform: translateY(-50%); + width: 12px; + height: 12px; + background: var(--text-secondary); + border-radius: 50%; + transition: left 220ms cubic-bezier(0.34, 1.45, 0.64, 1), + background var(--duration-fast) var(--ease), + width var(--duration-base) var(--ease), + box-shadow var(--duration-fast) var(--ease); +} + +.switch input:checked + .switch-slider { + background: var(--accent); + border-color: var(--accent); +} + +.switch input:checked + .switch-slider::after { + left: 18px; + background: #fff; + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.15); +} + +/* ===== Slider ===== */ +.slider { + -webkit-appearance: none; + appearance: none; + width: 140px; + height: 4px; + background: var(--bg-input); + border-radius: 999px; + outline: none; + padding: 0; + border: none; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + background: #fff; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + transition: transform var(--duration-fast) var(--ease); +} + +.slider::-webkit-slider-thumb:hover { + transform: scale(1.1); +} + +.slider::-moz-range-thumb { + width: 14px; + height: 14px; + background: #fff; + border-radius: 50%; + cursor: pointer; + border: none; +} + +/* ============================================================ + Accessibility: respect user's motion preferences + ============================================================ */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/scripts/start.js b/scripts/start.js new file mode 100644 index 0000000..17156dd --- /dev/null +++ b/scripts/start.js @@ -0,0 +1,48 @@ +/** + * 启动脚本:清除 ELECTRON_RUN_AS_NODE 等会干扰 Electron 的环境变量, + * 然后直接 spawn electron.exe + * + * 解决 Git Bash / npm 环境里 ELECTRON_RUN_AS_NODE=1 导致 Electron 退化为 Node.js 的问题 + */ +const path = require('path'); +const { spawn } = require('child_process'); + +// 关键:清除会让 Electron 退化为普通 Node 的环境变量 +delete process.env.ELECTRON_RUN_AS_NODE; +// 一些 npm/终端 会设置 ELECTRON_NO_ASAR,也清掉 +delete process.env.ELECTRON_NO_ASAR; + +const electronExe = path.join( + __dirname, + '..', + 'node_modules', + 'electron', + 'dist', + 'electron.exe' +); + +const args = [path.join(__dirname, '..'), ...process.argv.slice(2)]; + +console.log('Starting Electron:', electronExe); +console.log('Args:', args); + +const child = spawn(electronExe, args, { + stdio: 'inherit', + windowsHide: false, + env: process.env, +}); + +child.on('error', (err) => { + // spawn 在可执行文件不存在(npm install 没跑 / 路径损坏)时只触发 'error', + // 不会触发 'exit'——不显式监听会让脚本静默挂起,用户只能强退。 + console.error(`启动 Electron 失败:${err.message}`); + console.error(`请确认已运行 npm install,路径:${electronExe}`); + process.exit(1); +}); + +child.on('exit', (code, signal) => { + // Electron 进程被信号杀掉时 code === null、signal 是 'SIGTERM' / 'SIGINT' 等, + // 旧实现会静默吞掉、显示成「成功退出 0」,调试 SIGTERM 路径会很迷惑。 + if (signal) console.log(`Electron exited via signal: ${signal}`); + process.exit(code ?? 0); +}); \ No newline at end of file