import type { WindowNames } from '@runtime/lib/types' import { CONFIG_KEYS, IPC_EVENTS, WINDOW_NAMES } from '@runtime/lib/constants' import { app, BrowserWindow, BrowserWindowConstructorOptions, ipcMain, IpcMainInvokeEvent, type IpcMainEvent } from 'electron' import { createLogo } from '@electron/utils' import logManager from '@electron/service/logger' import configManager from '@electron/service/config-service' import themeManager from '@electron/service/theme-service' import path from 'node:path'; declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined; interface WindowState { instance: BrowserWindow | void; isHidden: boolean; onCreate: ((window: BrowserWindow) => void)[]; onClosed: ((window: BrowserWindow) => void)[]; } interface SizeOptions { width: number; // 窗口宽度 height: number; // 窗口高度 maxWidth?: number; // 窗口最大宽度,可选 maxHeight?: number; // 窗口最大高度,可选 minWidth?: number; // 窗口最小宽度,可选 minHeight?: number; // 窗口最小高度,可选 } const isMac = process.platform === 'darwin'; const isWindows = process.platform === 'win32'; const useCustomTitleBar = isWindows; const preloadEntryPath = MAIN_WINDOW_VITE_DEV_SERVER_URL ? path.join(process.cwd(), 'dist-electron', 'preload', 'preload.js') : path.join(__dirname, '..', 'preload', 'preload.js'); function getSharedWindowOptions(): BrowserWindowConstructorOptions { return { frame: isMac || !useCustomTitleBar, titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default', trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined, show: false, title: 'NIANXX', darkTheme: themeManager.isDark, backgroundColor: themeManager.isDark ? '#2C2C2C' : '#FFFFFF', webPreferences: { nodeIntegration: false, // 禁用 Node.js 集成,提高安全性 contextIsolation: true, // 启用上下文隔离,防止渲染进程访问主进程 API sandbox: true, // 启用沙箱模式,进一步增强安全性 backgroundThrottling: false, preload: preloadEntryPath, }, }; } class WindowService { private static _instance: WindowService; private _logo = createLogo(); private readonly isDev = !!MAIN_WINDOW_VITE_DEV_SERVER_URL private _winStates: Record = { main: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, setting: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, dialog: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, login: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, loading: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }, } private constructor() { this._setupIpcEvents(); logManager.info('WindowService initialized successfully.'); } private _isReallyClose(windowName: WindowNames | void) { if (windowName === WINDOW_NAMES.MAIN) return configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY) === false; if (windowName === WINDOW_NAMES.SETTING) return false; return true; } private _setupIpcEvents() { ipcMain.handle(IPC_EVENTS.APP_LOAD_PAGE, (e: IpcMainInvokeEvent, page: string) => { const win = BrowserWindow.fromWebContents(e.sender); if (win) this._loadPage(win, page); }); } public static getInstance(): WindowService { if (!this._instance) { this._instance = new WindowService(); } return this._instance; } public create(name: WindowNames, size: SizeOptions, moreOpts?: BrowserWindowConstructorOptions) { if (this.get(name)) return; const isHiddenWin = this._isHiddenWin(name); let window = this._createWinInstance(name, { ...size, ...moreOpts }); this._setupDevtools(window, name); !isHiddenWin && this ._setupWinLifecycle(window, name) ._loadWindowTemplate(window, name) this._listenWinReady({ win: window, isHiddenWin, size }) if (!isHiddenWin) { this._winStates[name].instance = window; this._winStates[name].onCreate.forEach(callback => callback(window)); } if (isHiddenWin) { this._winStates[name].isHidden = false; logManager.info(`Hidden window show: ${name}`) } return window; } private _setupDevtools(window: BrowserWindow, name: WindowNames) { if (!this.isDev) return; let opened = false; const openDevtools = () => { if (opened || window.isDestroyed()) return; opened = true; try { window.webContents.openDevTools({ mode: 'detach', activate: true }); logManager.info(`DevTools opened for window: ${name}`); } catch (error) { opened = false; logManager.warn(`Failed to open DevTools for window: ${name}`, error); } }; window.webContents.once('did-finish-load', () => { setTimeout(openDevtools, 150); }); window.webContents.on('before-input-event', (_event, input) => { const isMacDevtoolsShortcut = process.platform === 'darwin' && input.meta && input.alt && input.key.toLowerCase() === 'i'; const isF12 = input.key === 'F12'; if (!isF12 && !isMacDevtoolsShortcut) return; if (window.webContents.isDevToolsOpened()) { window.webContents.closeDevTools(); return; } openDevtools(); }); } private _setupWinLifecycle(window: BrowserWindow, name: WindowNames) { window.once('closed', () => { this._winStates[name].onClosed.forEach(callback => callback(window)); window?.destroy(); this._winStates[name].instance = void 0; this._winStates[name].isHidden = false; logManager.info(`Window closed: ${name}`); }); return this; } private _listenWinReady(params: { win: BrowserWindow, isHiddenWin: boolean, size: SizeOptions, }) { const onReady = () => { params.win?.once('show', () => setTimeout(() => this._applySizeConstraints(params.win, params.size), 2)); params.win?.show(); } if (!params.isHiddenWin) { const loadingHandler = this._addLoadingView(params.win, params.size); loadingHandler?.(onReady) } else { onReady(); } } private _addLoadingView(window: BrowserWindow, size: SizeOptions) { let rendererIsReady = false; const onRendererIsReady = (e: IpcMainEvent) => { if ((e.sender !== window?.webContents) || rendererIsReady) return; rendererIsReady = true; ipcMain.removeListener(IPC_EVENTS.RENDERER_IS_READY, onRendererIsReady); } ipcMain.on(IPC_EVENTS.RENDERER_IS_READY, onRendererIsReady); return (cb: () => void) => { // Immediately call callback since we don't have a loading view cb(); } } private _applySizeConstraints(win: BrowserWindow, size: SizeOptions) { if (size.maxHeight && size.maxWidth) { win.setMaximumSize(size.maxWidth, size.maxHeight); } if (size.minHeight && size.minWidth) { win.setMinimumSize(size.minWidth, size.minHeight); } } private _loadPage(window: BrowserWindow, pageName: string) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { return window.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}/${pageName}.html`); } window.loadFile(path.join(app.getAppPath(), 'dist', `${pageName}.html`)); } private _loadWindowTemplate(window: BrowserWindow, name: WindowNames) { // Always load index.html, loading.html has been removed const page = 'index'; this._loadPage(window, page); } private _handleCloseWindowState(target: BrowserWindow, really: boolean) { const name = this.getName(target) as WindowNames; if (name) { if (!really) this._winStates[name].isHidden = true; else this._winStates[name].instance = void 0; } setTimeout(() => { target[really ? 'close' : 'hide']?.(); this._checkAndCloseAllWinodws(); }, 210) } private _checkAndCloseAllWinodws() { if (!this._winStates[WINDOW_NAMES.MAIN].instance || this._winStates[WINDOW_NAMES.MAIN].instance?.isDestroyed()) return Object.values(this._winStates).forEach(win => win?.instance?.close()); const minimizeToTray = configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY); if (!minimizeToTray && !this.get(WINDOW_NAMES.MAIN)?.isVisible()) return Object.values(this._winStates).forEach(win => !win?.instance?.isVisible() && win?.instance?.close()); } private _isHiddenWin(name: WindowNames) { return this._winStates[name] && this._winStates[name].isHidden; } private _createWinInstance(name: WindowNames, opts?: BrowserWindowConstructorOptions) { return this._isHiddenWin(name) ? this._winStates[name].instance as BrowserWindow : new BrowserWindow({ ...getSharedWindowOptions(), icon: this._logo, ...opts, }); } public focus(target: BrowserWindow | void | null) { if (!target) return; const name = this.getName(target); if (target?.isMaximized()) { target?.restore(); logManager.debug(`Window ${name} restored and focused`); } else { logManager.debug(`Window ${name} focused`); } target?.focus(); } public close(target: BrowserWindow | void | null, really: boolean = true) { if (!target) return; const name = this.getName(target); logManager.info(`Close window: ${name}, really: ${really}`); this._handleCloseWindowState(target, really); } public toggleMax(target: BrowserWindow | void | null) { if (!target) return; target.isMaximized() ? target.unmaximize() : target.maximize(); } public getName(target: BrowserWindow | null | void): WindowNames | void { if (!target) return; for (const [name, win] of Object.entries(this._winStates) as [WindowNames, { instance: BrowserWindow | void } | void][]) { if (win?.instance === target) return name; } } public get(name: WindowNames) { if (this._winStates[name].isHidden) return void 0; return this._winStates[name].instance; } public onWindowCreate(name: WindowNames, callback: (window: BrowserWindow) => void) { this._winStates[name].onCreate.push(callback); } public onWindowClosed(name: WindowNames, callback: (window: BrowserWindow) => void) { this._winStates[name].onClosed.push(callback); } } export const windowManager = WindowService.getInstance(); export default windowManager;