import { app, BrowserWindow, globalShortcut, ipcMain, Menu, nativeTheme, Tray, nativeImage, } from 'electron'; import {URL} from 'node:url'; import {PyShell} from '/@/pyshell'; import { dpiScaling, webuiTheme, webuiArgs, webuiPath, installerPath, installerArgs, } from '/@/config'; import {isMacintosh} from '@common/utils/env'; import relaunchApp from '/@/relaunchApp'; import {ALAS_LOG, UPDATE_APP} from '@common/constant/eventNames'; const path = require('path'); /** * Load deploy settings and start Alas web server. */ let installer: PyShell | null = null; let alas: PyShell | null = null; let browserWindow: BrowserWindow | null = null; nativeTheme.themeSource = webuiTheme; export async function createWindow() { browserWindow = new BrowserWindow({ width: 1280, height: 880, show: false, // Use 'ready-to-show' event to show window frame: false, icon: path.join(__dirname, './icon.png'), webPreferences: { nodeIntegration: true, contextIsolation: true, sandbox: false, // Sandbox disabled because the demo of preload script depend on the Node.js api webviewTag: false, // The webview tag is not recommended. Consider alternatives like an iframe or Electron's BrowserView. @see https://www.electronjs.org/docs/latest/api/webview-tag#warning preload: path.join(app.getAppPath(), 'packages/preload/dist/index.cjs'), }, }); /** * If the 'show' property of the BrowserWindow's constructor is omitted from the initialization options, * it then defaults to 'true'. This can cause flickering as the window loads the html content, * and it also has show problematic behaviour with the closing of the window. * Use `show: false` and listen to the `ready-to-show` event to show the window. * * @see https://github.com/electron/electron/issues/25012 for the afford mentioned issue. */ browserWindow.on('ready-to-show', () => { browserWindow?.show(); // Hide menu const {Menu} = require('electron'); Menu.setApplicationMenu(null); if (import.meta.env.DEV) { browserWindow?.webContents.openDevTools(); } }); browserWindow.on('focus', function () { // Dev tools globalShortcut.register('Ctrl+Shift+I', function () { if (browserWindow?.webContents.isDevToolsOpened()) { browserWindow?.webContents.closeDevTools(); } else { browserWindow?.webContents.openDevTools(); } }); // Refresh globalShortcut.register('Ctrl+R', function () { browserWindow?.reload(); }); globalShortcut.register('Ctrl+Shift+R', function () { browserWindow?.reload(); }); }); browserWindow.on('blur', function () { globalShortcut.unregisterAll(); }); ipcMain.on('window-ready', async function (_, args) { args && (await initWindowEvents()); }); /* * Fix oversize icon on bar in macOS */ const icon = nativeImage.createFromPath(path.join(__dirname, './icon.png')); const dockerIcon = icon.resize({width: 16, height: 16}); // Tray const tray = new Tray(isMacintosh ? dockerIcon : icon); const contextMenu = Menu.buildFromTemplate([ { label: 'Show', click: function () { browserWindow?.show(); }, }, { label: 'Hide', click: function () { browserWindow?.hide(); }, }, { label: 'Exit', click: function () { alas?.kill(function () { browserWindow?.close(); }); }, }, ]); tray.setToolTip('AAS'); tray.setContextMenu(contextMenu); tray.on('click', () => { if (browserWindow?.isVisible()) { if (browserWindow?.isMinimized()) { browserWindow?.show(); } else { browserWindow?.hide(); } } else { browserWindow?.show(); } }); tray.on('right-click', () => { tray.popUpContextMenu(contextMenu); }); return browserWindow; } // No DPI scaling if (!dpiScaling) { app.commandLine.appendSwitch('high-dpi-support', '1'); app.commandLine.appendSwitch('force-device-scale-factor', '1'); } export function loadURL() { /** * URL for main window. * Vite dev server for development. * `file://../renderer/index.html` for production and test */ const pageUrl = import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined ? import.meta.env.VITE_DEV_SERVER_URL : new URL('../renderer/dist/index.html', 'file://' + __dirname).toString(); browserWindow?.loadURL(pageUrl); } // Minimize, maximize, close window. ipcMain.on('window-tray', function () { browserWindow?.hide(); }); ipcMain.on('window-min', function () { browserWindow?.minimize(); }); ipcMain.on('window-max', function () { browserWindow?.isMaximized() ? browserWindow?.restore() : browserWindow?.maximize(); }); ipcMain.on('window-close', function () { if (installer) { installer?.removeAllListeners('stderr'); installer?.removeAllListeners('message'); installer?.removeAllListeners('stdout'); installer?.kill(function () { browserWindow?.close(); browserWindow = null; installer = null; }); return; } alas?.removeAllListeners('stderr'); alas?.removeAllListeners('message'); alas?.removeAllListeners('stdout'); alas?.kill(function () { browserWindow?.close(); browserWindow = null; }); browserWindow?.close(); }); async function initWindowEvents() { // Start installer and wait for it to finish. await runInstaller(); ipcMain.on('install-success', async function () { installer = null; // Start Alas web server. runAlas(); }); } async function runInstaller() { installer = new PyShell(installerPath, installerArgs); installer?.end(function (err: string) { sendLaunchLog(err); if (err) throw err; }); installer?.on('stdout', function (message) { sendLaunchLog(message); }); installer?.on('message', function (message) { sendLaunchLog(message); }); installer?.on('stderr', function (message: string) { sendLaunchLog(message); }); } function runAlas() { alas = new PyShell(webuiPath, webuiArgs); alas?.end(function (err: string) { sendLaunchLog(err); if (err) throw err; }); alas?.on('stdout', function (message) { sendLaunchLog(message); }); alas?.on('message', function (message) { sendLaunchLog(message); }); alas?.on('stderr', function (message: string) { sendLaunchLog(message); /** * Receive logs, judge if Alas is ready * For starlette backend, there will have: * `INFO: Uvicorn running on http://0.0.0.0:22267 (Press CTRL+C to quit)` * Or backend has started already * `[Errno 10048] error while attempting to bind on address ('0.0.0.0', 22267): ` */ if (message.includes('Application startup complete') || message.includes('bind on address')) { alas?.removeAllListeners('stderr'); alas?.removeAllListeners('message'); alas?.removeAllListeners('stdout'); // loadURL(); } }); } function sendLaunchLog(message: string) { message?.includes(UPDATE_APP) && relaunchApp(); browserWindow?.webContents.send(ALAS_LOG, message); } export async function restoreWindow() { // Someone tried to run a second instance, we should focus our window. if (browserWindow) { if (browserWindow.isMinimized()) browserWindow.restore(); if (!browserWindow.isVisible()) browserWindow.show(); browserWindow.focus(); } }