import { app, BrowserWindow, desktopCapturer, Menu, session, shell, Tray } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; import { readDesktopSettings } from '../desktop-settings'; let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; let closeToTrayEnabled = true; let appQuitting = false; const WINDOW_STATE_CHANGED_CHANNEL = 'window-state-changed'; function getAssetPath(...segments: string[]): string { const basePath = app.isPackaged ? path.join(process.resourcesPath, 'images') : path.join(__dirname, '..', '..', '..', 'images'); return path.join(basePath, ...segments); } function getExistingAssetPath(...segments: string[]): string | undefined { const assetPath = getAssetPath(...segments); return fs.existsSync(assetPath) ? assetPath : undefined; } function getWindowIconPath(): string | undefined { if (process.platform === 'win32') return getExistingAssetPath('windows', 'icon.ico'); if (process.platform === 'linux') return getExistingAssetPath('icon.png'); return undefined; } export function getDockIconPath(): string | undefined { return getExistingAssetPath('macos', '1024x1024.png'); } function getTrayIconPath(): string | undefined { if (process.platform === 'win32') return getExistingAssetPath('windows', 'icon.ico'); return getExistingAssetPath('icon.png'); } export { getWindowIconPath }; export function getMainWindow(): BrowserWindow | null { return mainWindow; } function destroyTray(): void { if (!tray) { return; } tray.destroy(); tray = null; } function requestAppQuit(): void { prepareWindowForAppQuit(); app.quit(); } function ensureTray(): void { if (tray) { return; } const trayIconPath = getTrayIconPath(); if (!trayIconPath) { return; } tray = new Tray(trayIconPath); tray.setToolTip('MetoYou'); tray.setContextMenu( Menu.buildFromTemplate([ { label: 'Open MetoYou', click: () => { void showMainWindow(); } }, { type: 'separator' }, { label: 'Close MetoYou', click: () => { requestAppQuit(); } } ]) ); tray.on('click', () => { void showMainWindow(); }); } function hideWindowToTray(): void { if (!mainWindow || mainWindow.isDestroyed()) { return; } mainWindow.hide(); emitWindowState(); } export function updateCloseToTraySetting(enabled: boolean): void { closeToTrayEnabled = enabled; } export function prepareWindowForAppQuit(): void { appQuitting = true; destroyTray(); } export async function showMainWindow(): Promise { if (!mainWindow || mainWindow.isDestroyed()) { await createWindow(); return; } if (mainWindow.isMinimized()) { mainWindow.restore(); } if (!mainWindow.isVisible()) { mainWindow.show(); } mainWindow.focus(); emitWindowState(); } function emitWindowState(): void { if (!mainWindow || mainWindow.isDestroyed()) { return; } mainWindow.webContents.send(WINDOW_STATE_CHANGED_CHANNEL, { isFocused: mainWindow.isFocused(), isMinimized: mainWindow.isMinimized() }); } export async function createWindow(): Promise { const windowIconPath = getWindowIconPath(); closeToTrayEnabled = readDesktopSettings().closeToTray; ensureTray(); mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 800, minHeight: 600, frame: false, titleBarStyle: 'hidden', backgroundColor: '#0a0a0f', ...(windowIconPath ? { icon: windowIconPath } : {}), webPreferences: { backgroundThrottling: false, nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, '..', 'preload.js'), webSecurity: true } }); if (process.platform === 'linux') { session.defaultSession.setDisplayMediaRequestHandler( async (_request, respond) => { // On Linux/Wayland the system picker (useSystemPicker: true) handles // the portal. This handler is only reached if the system picker is // unavailable (e.g. X11 without a portal). Fall back to // desktopCapturer so the user still gets something. try { const sources = await desktopCapturer.getSources({ types: ['window', 'screen'], thumbnailSize: { width: 150, height: 150 } }); const firstSource = sources[0]; if (firstSource) { respond({ video: firstSource }); return; } } catch { // desktopCapturer also unavailable } respond({}); }, { useSystemPicker: true } ); } if (process.env['NODE_ENV'] === 'development') { const devUrl = process.env['SSL'] === 'true' ? 'https://localhost:4200' : 'http://localhost:4200'; await mainWindow.loadURL(devUrl); if (process.env['DEBUG_DEVTOOLS'] === '1') { mainWindow.webContents.openDevTools(); } } else { await mainWindow.loadFile(path.join(__dirname, '..', '..', 'client', 'browser', 'index.html')); } mainWindow.on('close', (event) => { if (appQuitting || !closeToTrayEnabled) { return; } event.preventDefault(); hideWindowToTray(); }); mainWindow.on('closed', () => { mainWindow = null; }); mainWindow.on('focus', () => { mainWindow?.flashFrame(false); emitWindowState(); }); mainWindow.on('blur', () => { emitWindowState(); }); mainWindow.on('minimize', () => { emitWindowState(); }); mainWindow.on('restore', () => { emitWindowState(); }); mainWindow.on('show', () => { emitWindowState(); }); mainWindow.on('hide', () => { emitWindowState(); }); emitWindowState(); mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); mainWindow.webContents.on('will-navigate', (event, url) => { const currentUrl = mainWindow?.webContents.getURL(); const isSameOrigin = new URL(url).origin === new URL(currentUrl || '').origin; if (!isSameOrigin) { event.preventDefault(); shell.openExternal(url); } }); }