// main 进程入口:创建窗口 + 初始化库 + 注册 IPC + 迁移检测 import { app, BrowserWindow, ipcMain, protocol, net, dialog, shell } from 'electron'; import nodePath from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { promises as fs } from 'node:fs'; import { VaultService } from './services/vault-service.js'; import { registerIpc } from './ipc/handlers.js'; import { IPC } from '@hunter/shared'; const __dirname = nodePath.dirname(fileURLToPath(import.meta.url)); // FR-E6:注册 vaultasset:// 协议——渲染层用相对路径展示库内本地图片(须在 app ready 前注册) protocol.registerSchemesAsPrivileged([ { scheme: 'vaultasset', privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true } }, ]); let win: BrowserWindow | null = null; // 多窗口支持:窗口控制作用于当前聚焦窗口,其次主窗口 const focusedWin = () => BrowserWindow.getFocusedWindow() ?? win; function createWindow() { const w = new BrowserWindow({ width: 1200, height: 760, minWidth: 900, minHeight: 580, center: true, frame: false, show: false, transparent: false, backgroundColor: '#f6f7f9', backgroundMaterial: 'none', // UI 重新设计:窗口不透明,backgroundColor 用 L0 空间底色,稳定呈现玻璃材质(玻璃透过的是窗口内的空间层) webPreferences: { preload: nodePath.join(__dirname, '../preload/index.mjs'), contextIsolation: true, nodeIntegration: false, sandbox: false } }); if (!win) win = w; w.on('ready-to-show', () => w.show()); // 安全:外部链接一律交给系统默认浏览器,不在应用窗口内导航(避免丢失编辑器状态) w.webContents.setWindowOpenHandler(({ url }) => { if (/^https?:\/\//i.test(url)) void shell.openExternal(url); return { action: 'deny' }; }); w.webContents.on('will-navigate', (e, url) => { const current = w.webContents.getURL(); // 仅拦截跳转到其它 http(s) 站点的导航;应用自身 (vaultasset/file/app) 不拦 if (/^https?:\/\//i.test(url) && url !== current) { e.preventDefault(); void shell.openExternal(url); } }); // 转发渲染进程控制台消息到主进程终端(便于开发调试) w.webContents.on('console-message', (e, level, message, line, sourceId) => { const lv = ['log', 'info', 'warn', 'error'][level] || 'log'; console.log('[renderer:' + lv + '] ' + message + (sourceId ? ' (' + sourceId + ':' + line + ')' : '')); }); w.on('closed', () => { if (win === w) win = null; }); if (process.env.ELECTRON_RENDERER_URL) w.loadURL(process.env.ELECTRON_RENDERER_URL); else w.loadFile(nodePath.join(__dirname, '../renderer/index.html')); return w; } // 窗口控制(FR-W2 + 多窗口安全 + Typora 快捷键:全屏 F11 / 新窗口 Ctrl+Shift+N) ipcMain.on(IPC.winMinimize, () => focusedWin()?.minimize()); ipcMain.on(IPC.winMaximize, () => { const w = focusedWin(); if (w?.isMaximized()) w.unmaximize(); else w?.maximize(); }); ipcMain.handle(IPC.winIsMaximized, () => focusedWin()?.isMaximized() ?? false); ipcMain.on(IPC.winClose, () => focusedWin()?.close()); ipcMain.on(IPC.winFullscreen, () => { const w = focusedWin(); if (w) w.setFullScreen(!w.isFullScreen()); }); ipcMain.on(IPC.winNew, () => { createWindow(); }); // Ctrl+O 打开文件(选 .md → 返回绝对路径,渲染层换算为库内相对路径) ipcMain.handle(IPC.fileOpen, async () => { const w = focusedWin(); const r = await dialog.showOpenDialog(w ?? undefined as never, { properties: ['openFile'], filters: [{ name: 'Markdown', extensions: ['md'] }] }); if (r.canceled || r.filePaths.length === 0) return null; return r.filePaths[0]; }); // Ctrl+Shift+S 另存为:弹保存对话框写入正文副本 ipcMain.handle(IPC.docSaveAs, async (_e, payload: { body: string; title: string }) => { const w = focusedWin(); const r = await dialog.showSaveDialog(w ?? undefined as never, { defaultPath: (payload.title || 'untitled') + '.md', filters: [{ name: 'Markdown', extensions: ['md'] }] }); if (r.canceled || !r.filePath) return null; await fs.writeFile(r.filePath, payload.body, 'utf8'); return r.filePath; }); app.whenReady().then(async () => { const vault = new VaultService(); await vault.init(); // FR-E6:vaultasset:// 协议处理器——安全提供库内文件给渲染层(限定在库根内) protocol.handle('vaultasset', async (req) => { const root = vault.fileService?.getRoot(); if (!root) return new Response('no vault', { status: 503 }); const rel = decodeURIComponent(new URL(req.url).pathname).replace(/^\/+/, ''); const abs = nodePath.resolve(root, rel); const relFromRoot = nodePath.relative(root, abs); if (relFromRoot.startsWith('..') || nodePath.isAbsolute(relFromRoot)) return new Response('forbidden', { status: 403 }); try { return await net.fetch(pathToFileURL(abs).toString()); } catch { return new Response('not found', { status: 404 }); } }); // FR-A5:GPU 探测——软渲染时自动禁用实时模糊 let gpuStatus = { softwareRendering: false, vendor: '', renderer: '' }; try { const gi = await (app as any).getGPUInfo('full'); gpuStatus = { softwareRendering: gi.softwareRendering === true || /SwiftShader|llvmpipe|Software/.test(gi.gpuDevice?.[0]?.renderer ?? ''), vendor: gi.gpuDevice?.[0]?.vendorString ?? '', renderer: gi.gpuDevice?.[0]?.renderer ?? '', }; } catch { /* getGPUInfo 在某些环境不可用 */ } ipcMain.handle(IPC.gpuInfo, () => gpuStatus); registerIpc(vault); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });