1. 信息面板 UI 重做:设计 token 统一、lucide 图标、分区小节、星标/标签/关联 chip 化
2. 关联文档升级:新增 doc:link-batch IPC,A↔B 双向写入 links;面板支持快速双向关联 + 批量勾选关联/解除
3. 代码块内全选复制:Ctrl+A 在代码块内仅全选该代码块(TyporaKeys selectInsideCodeBlock),Ctrl+C 复制纯代码;代码块内 Ctrl+V 按纯文本粘贴
4. 修复 React error #62:btn 改为函数后遗漏 style={btn} 裸用,导致 render 期间更新
96 lines
4.8 KiB
TypeScript
96 lines
4.8 KiB
TypeScript
// 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<string, unknown>; 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));
|
||
});
|
||
} |