Files
metona-ai-desktop/electron/services/window-manager.service.ts
T
thzxx 1d185db6b3 feat: MetonaAI Desktop 初始项目
- Electron + React + TypeScript 架构
- 三栏布局: Sidebar | ChatPanel | DetailPanel
- 9 个内置工具 (文件系统/网络/记忆/命令)
- SQLite 持久化 (better-sqlite3)
- MUI 暗色/亮色主题系统
- Agent Loop ReAct 状态机引擎
- DeepSeek / Agnes AI / Ollama Provider 适配器
- MCP 协议集成
- 系统托盘 + 全局快捷键
- Tailwind CSS v4 + Tailwind Merge
- 修复: Sidebar 缺失 TextField 导入导致黑屏
2026-06-27 21:33:27 +08:00

215 lines
4.9 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.
/**
* Window Manager — 多窗口管理
*
* 职责:
* 1. 创建和管理多个窗口(每个工作空间可独立开窗口)
* 2. 窗口状态持久化(位置、大小)
* 3. 全局快捷键注册(Cmd+Shift+M 切换到 Metona
* 4. 关闭到托盘行为
*
* @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理
*/
import { BrowserWindow, globalShortcut, app, shell } from 'electron';
import { join } from 'path';
import { existsSync } from 'fs';
import { is } from '@electron-toolkit/utils';
import log from 'electron-log';
export interface WindowState {
x?: number;
y?: number;
width: number;
height: number;
isMaximized?: boolean;
}
export class WindowManager {
private windows = new Map<string, BrowserWindow>();
private activeWindowId: string | null = null;
/**
* 创建新窗口
*/
createWindow(options: {
id?: string;
workspacePath?: string;
title?: string;
state?: WindowState;
}): BrowserWindow {
const id = options.id ?? `window_${Date.now()}`;
const state = options.state ?? { width: 1440, height: 900 };
const win = new BrowserWindow({
width: state.width,
height: state.height,
x: state.x,
y: state.y,
minWidth: 960,
minHeight: 600,
icon: this.getIconPath(),
show: false,
titleBarStyle: 'hiddenInset',
title: options.title ?? 'MetonaAI Desktop',
webPreferences: {
preload: join(__dirname, '../preload/preload.mjs'),
sandbox: false,
contextIsolation: true,
nodeIntegration: false,
},
});
// 加载页面
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
win.loadURL(process.env['ELECTRON_RENDERER_URL']);
} else {
win.loadFile(join(__dirname, '../../dist/index.html'));
}
// 窗口事件
win.on('ready-to-show', () => {
if (state.isMaximized) {
win.maximize();
}
win.show();
});
win.on('closed', () => {
this.windows.delete(id);
if (this.activeWindowId === id) {
this.activeWindowId = this.windows.size > 0 ? this.windows.keys().next().value ?? null : null;
}
});
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
this.windows.set(id, win);
this.activeWindowId = id;
log.info(`Window created: ${id} (${options.title ?? 'MetonaAI Desktop'})`);
return win;
}
/**
* 获取窗口
*/
getWindow(id: string): BrowserWindow | undefined {
return this.windows.get(id);
}
/**
* 获取活动窗口
*/
getActiveWindow(): BrowserWindow | null {
if (this.activeWindowId) {
return this.windows.get(this.activeWindowId) ?? null;
}
return null;
}
/**
* 获取所有窗口
*/
getAllWindows(): BrowserWindow[] {
return Array.from(this.windows.values());
}
/**
* 聚焦到窗口(从任意应用切换)
*/
focusWindow(): void {
const win = this.getActiveWindow();
if (win) {
if (win.isMinimized()) {
win.restore();
}
win.show();
win.focus();
}
}
/**
* 注册全局快捷键
*/
registerGlobalShortcuts(): void {
// Cmd/Ctrl+Shift+M — 从任意应用切换到 Metona
const registered = globalShortcut.register('CommandOrControl+Shift+M', () => {
this.focusWindow();
log.info('Global shortcut: Switch to Metona');
});
if (!registered) {
log.warn('Failed to register global shortcut: Cmd+Shift+M');
} else {
log.info('Global shortcut registered: Cmd+Shift+M');
}
}
/**
* 注销全局快捷键
*/
unregisterGlobalShortcuts(): void {
globalShortcut.unregisterAll();
log.info('Global shortcuts unregistered');
}
/**
* 获取窗口状态(用于持久化)
*/
getWindowState(id: string): WindowState | null {
const win = this.windows.get(id);
if (!win) return null;
const bounds = win.getBounds();
return {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
isMaximized: win.isMaximized(),
};
}
/**
* 关闭所有窗口
*/
closeAll(): void {
for (const [id, win] of this.windows) {
try {
win.destroy();
} catch {
log.warn(`Failed to close window: ${id}`);
}
}
this.windows.clear();
this.activeWindowId = null;
}
/**
* 获取窗口数量
*/
get count(): number {
return this.windows.size;
}
/**
* 获取应用图标路径
*/
private getIconPath(): string | undefined {
const candidates = [
join(__dirname, '../../assets/logo.ico'),
join(__dirname, '../../assets/logo.png'),
join(process.resourcesPath || '', 'assets/logo.ico'),
join(process.resourcesPath || '', 'assets/logo.png'),
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return undefined;
}
}