Files
metona-ollama-desktop/src/main/main.ts
T
thzxx 0e1f216aa4 feat: 添加工作空间面板 - 右侧终端/文件浏览器
- 新增主进程 workspace.ts:spawn 进程管理,流式输出,安全检查
- IPC 双向通信:workspace:exec(on/send 模式,无超时限制)
- preload 暴露 workspace API
- 渲染进程 workspace-panel.ts:多终端 Tab、ANSI 颜色渲染、文件浏览器
- 设置面板添加工作空间目录配置
- 启动时自动创建 workspace 目录
- 窗口关闭时自动清理所有子进程
- 帮助文档更新
2026-04-07 18:30:45 +08:00

175 lines
4.8 KiB
TypeScript

/**
* Metona Ollama Desktop - 主进程入口
*/
import { app, BrowserWindow } from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import { setupIPC } from './ipc.js';
import { createTray } from './tray.js';
import { createMenu } from './menu.js';
import { showNotification } from './utils.js';
import { ensureWorkspaceDir, killAllProcesses } from './workspace.js';
const APP_NAME = 'Metona Ollama';
const ICON_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.png');
const ICO_PATH = path.join(__dirname, '..', '..', 'assets', 'icons', 'llama.ico');
const IS_DEV = !app.isPackaged;
export let mainWindow: BrowserWindow | null = null;
export let isQuitting = false;
export function setQuitting(): void {
isQuitting = true;
}
export function getIconPath(): string {
return process.platform === 'win32' ? ICO_PATH : ICON_PATH;
}
function createMainWindow(): BrowserWindow {
const userDataPath = app.getPath('userData');
const configPath = path.join(userDataPath, 'window-state.json');
let windowState = { width: 1200, height: 800, x: undefined as number | undefined, y: undefined as number | undefined, maximized: false };
try {
if (fs.existsSync(configPath)) {
windowState = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
} catch { /* use defaults */ }
mainWindow = new BrowserWindow({
width: windowState.width || 1200,
height: windowState.height || 800,
x: windowState.x,
y: windowState.y,
minWidth: 800,
minHeight: 600,
icon: process.platform === 'win32' ? ICO_PATH : ICON_PATH,
title: APP_NAME,
backgroundColor: '#202020',
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
webSecurity: false,
allowRunningInsecureContent: false
}
});
// 开发模式:从 src/renderer/ 加载;生产模式:从 dist/renderer/ 加载
const isDev = !app.isPackaged;
if (isDev) {
mainWindow.loadFile(path.join(__dirname, '..', '..', 'src', 'renderer', 'index.html'));
} else {
mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'));
}
mainWindow.setMenuBarVisibility(false);
mainWindow.once('ready-to-show', () => {
if (windowState.maximized) {
mainWindow!.maximize();
}
mainWindow!.show();
});
const saveWindowState = (): void => {
if (!mainWindow || mainWindow.isMaximized() || mainWindow.isMinimized()) return;
const bounds = mainWindow.getBounds();
try {
fs.writeFileSync(configPath, JSON.stringify(bounds));
} catch { /* ignore */ }
};
mainWindow.on('resize', saveWindowState);
mainWindow.on('move', saveWindowState);
mainWindow.on('maximize', () => {
try {
const state = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
state.maximized = true;
fs.writeFileSync(configPath, JSON.stringify(state));
} catch { /* ignore */ }
});
mainWindow.on('unmaximize', () => {
try {
const state = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
state.maximized = false;
fs.writeFileSync(configPath, JSON.stringify(state));
} catch { /* ignore */ }
});
mainWindow.on('close', (e) => {
if (!isQuitting) {
e.preventDefault();
mainWindow!.hide();
if (!(global as Record<string, unknown>)._trayHintShown) {
(global as Record<string, unknown>)._trayHintShown = true;
showNotification('Metona 已最小化到系统托盘', '点击托盘图标可重新打开');
}
}
});
mainWindow.on('closed', () => {
mainWindow = null;
});
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('http://') || url.startsWith('https://')) {
const { shell } = require('electron');
shell.openExternal(url);
return { action: 'deny' as const };
}
return { action: 'allow' as const };
});
return mainWindow;
}
// ── 单实例锁 ──
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
app.whenReady().then(() => {
ensureWorkspaceDir();
setupIPC();
createMainWindow();
createTray();
createMenu();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
} else if (mainWindow) {
mainWindow.show();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
// keep running with tray
}
});
app.on('before-quit', () => {
isQuitting = true;
// 清理所有工作空间进程
killAllProcesses();
// 通知渲染进程释放显存
mainWindow?.webContents.send('app-quit');
});