Files
MyNote/apps/desktop/src/main/index.ts
T

116 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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(); });