// IPC handlers — 主进程侧通道注册(类型链约束于 @hunter/shared) import nodePath from 'node:path'; import { ipcMain, shell, dialog, clipboard } from 'electron'; import { IPC } from '@hunter/shared'; import type { VaultService } from '../services/vault-service.js'; import { buildGraphData } from '../services/graph-builder.js'; export function registerIpc(vault: VaultService) { const fs2 = () => { if (!vault.fileService) throw new Error('vault not initialized'); return vault.fileService; }; ipcMain.handle(IPC.vaultList, async () => vault.getMeta()); ipcMain.handle(IPC.vaultSwitch, async (_e, p: { root: string }) => vault.switch(p.root)); ipcMain.handle(IPC.vaultSetDefault, async (_e, p: { root: string }) => { await vault.setDefaultRoot(p.root); }); ipcMain.handle(IPC.vaultPick, async () => vault.pickAndSwitch()); ipcMain.handle(IPC.vaultBrowse, async () => vault.pickDirectory()); ipcMain.handle(IPC.treeList, async () => fs2().listTree()); ipcMain.handle(IPC.treeRefresh, async () => fs2().listTree()); ipcMain.handle(IPC.fileCreate, async (_e, p: { basePath: string; name: string; isFolder: boolean }) => fs2().create(p.basePath, p.name, p.isFolder)); ipcMain.handle(IPC.fileRename, async (_e, p: { from: string; toName: string }) => fs2().rename(p.from, p.toName)); ipcMain.handle(IPC.fileMove, async (_e, p: { src: string; destDir: string }) => fs2().move(p.src, p.destDir)); // FR-F8:删除优先移入回收站(shell.trashItem),失败回退永久删除 ipcMain.handle(IPC.fileDelete, async (_e, p: { path: string; recursive: boolean }) => { const abs = nodePath.join(fs2().getRoot(), p.path); try { await shell.trashItem(abs); } catch (e) { // 回退:回收站不可用时永久删除 console.error('trashItem failed, fallback to permanent delete:', e); await fs2().delete(p.path, p.recursive); } }); // FR-F11:导入——sources 为空时弹系统对话框(支持文件夹导入) ipcMain.handle(IPC.fileImport, async (_e, p: { sources: string[]; destDir: string }) => { let sources = p.sources; if (sources.length === 0) { const r = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'], filters: [{ name: 'Markdown', extensions: ['md'] }] }); if (r.canceled) return { ok: [], failed: [] }; sources = r.filePaths; } return fs2().importFiles(sources, p.destDir); }); // FR-F11:导入文件夹——选择外部文件夹递归复制进库 ipcMain.handle(IPC.fileImportFolder, async (_e, p: { destDir: string }) => { const r = await dialog.showOpenDialog({ properties: ['openDirectory'] }); if (r.canceled || r.filePaths.length === 0) return { ok: [], failed: [] }; return fs2().importFiles(r.filePaths, p.destDir); }); // FR-E6:图片入库 ipcMain.handle(IPC.assetPickImage, async () => vault.pickImage()); ipcMain.handle(IPC.assetWrite, async (_e, p: { name: string; dataBase64: string }) => fs2().writeAsset(p.name, p.dataBase64)); ipcMain.handle(IPC.docRead, async (_e, p: { path: string }) => fs2().readDoc(p.path)); ipcMain.handle(IPC.docWrite, async (_e, p: { path: string; meta: any; body: string }) => fs2().writeDoc({ path: p.path, meta: p.meta, body: p.body })); // 双向批量关联:当前文档与多个目标文档互相写入 links ipcMain.handle(IPC.docLinkBatch, async (_e, p: { path: string; targets: string[]; remove: boolean }) => fs2().linkBatch(p.path, p.targets, p.remove)); ipcMain.handle(IPC.search, async (_e, p: { query: string }) => vault.searchService?.search(p.query) ?? []); ipcMain.handle(IPC.tagStats, async () => vault.searchService?.tagStats() ?? []); ipcMain.handle(IPC.tagPaths, async () => vault.searchService?.tagPaths() ?? {}); ipcMain.handle(IPC.graph, async () => { const fs2 = vault.fileService; if (!fs2) return { nodes: [], edges: [] }; const tree = await fs2.listTree(); const gd = await buildGraphData(fs2.getRoot(), tree); return gd; }); ipcMain.handle(IPC.appearanceLoad, async () => vault.getAppearance()); ipcMain.handle(IPC.appearanceSave, async (_e, p: { themeId: string; params: Record; effect: string; bgImage: string | null }) => { await vault.saveAppearance(p); }); ipcMain.handle(IPC.keymapLoad, async () => vault.getKeymaps()); ipcMain.handle(IPC.keymapSave, async (_e, p: { id: string; combo: string }[]) => { await vault.saveKeymaps(p); }); ipcMain.handle(IPC.copyMd, async (_e, p: { path: string }) => { const fs2 = vault.fileService; if (!fs2) return ''; const { body } = await fs2.readDoc(p.path); clipboard.writeText(body); return body; }); ipcMain.handle(IPC.docReveal, async (_e, p: { path: string }) => { const fs2 = vault.fileService; if (!fs2) return; shell.showItemInFolder(nodePath.join(fs2.getRoot(), p.path)); }); }