import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' import fs from 'node:fs/promises' import { createHudOverlayWindow, createEditorWindow, createSourceSelectorWindow } from './windows' import { registerIpcHandlers } from './ipc/handlers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) export const RECORDINGS_DIR = path.join(app.getPath('userData'), 'recordings') async function ensureRecordingsDir() { try { await fs.mkdir(RECORDINGS_DIR, { recursive: true }) console.log('RECORDINGS_DIR:', RECORDINGS_DIR) console.log('User Data Path:', app.getPath('userData')) } catch (error) { console.error('Failed to create recordings directory:', error) } } // The built directory structure // // ├─┬─┬ dist // │ │ └── index.html // │ │ // │ ├─┬ dist-electron // │ │ ├── main.js // │ │ └── preload.mjs // │ process.env.APP_ROOT = path.join(__dirname, '..') // Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST // Window references let mainWindow: BrowserWindow | null = null let sourceSelectorWindow: BrowserWindow | null = null let tray: Tray | null = null let selectedSourceName = '' // Tray Icons const defaultTrayIcon = getTrayIcon('openscreen.png'); const recordingTrayIcon = getTrayIcon('rec-button.png'); function createWindow() { mainWindow = createHudOverlayWindow() } function isEditorWindow(window: BrowserWindow) { return window.webContents.getURL().includes('windowType=editor') } function sendEditorMenuAction(channel: 'menu-load-project' | 'menu-save-project' | 'menu-save-project-as') { let targetWindow = BrowserWindow.getFocusedWindow() ?? mainWindow if (!targetWindow || targetWindow.isDestroyed() || !isEditorWindow(targetWindow)) { createEditorWindowWrapper() targetWindow = mainWindow if (!targetWindow || targetWindow.isDestroyed()) return targetWindow.webContents.once('did-finish-load', () => { if (!targetWindow || targetWindow.isDestroyed()) return targetWindow.webContents.send(channel) }) return } targetWindow.webContents.send(channel) } function setupApplicationMenu() { const isMac = process.platform === 'darwin' const template: Electron.MenuItemConstructorOptions[] = [] if (isMac) { template.push({ label: app.name, submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' }, ], }) } template.push( { label: 'File', submenu: [ { label: 'Load Project…', accelerator: 'CmdOrCtrl+O', click: () => sendEditorMenuAction('menu-load-project'), }, { label: 'Save Project…', accelerator: 'CmdOrCtrl+S', click: () => sendEditorMenuAction('menu-save-project'), }, { label: 'Save Project As…', accelerator: 'CmdOrCtrl+Shift+S', click: () => sendEditorMenuAction('menu-save-project-as'), }, ...(isMac ? [] : [{ type: 'separator' as const }, { role: 'quit' as const }]), ], }, { label: 'Edit', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }, ], }, { label: 'View', submenu: [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, ], }, { label: 'Window', submenu: isMac ? [ { role: 'minimize' }, { role: 'zoom' }, { type: 'separator' }, { role: 'front' }, ] : [ { role: 'minimize' }, { role: 'close' }, ], }, ) const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) } function createTray() { tray = new Tray(defaultTrayIcon); } function getTrayIcon(filename: string) { return nativeImage.createFromPath(path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename)).resize({ width: 24, height: 24, quality: 'best' }); } function updateTrayMenu(recording: boolean = false) { if (!tray) return; const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon; const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen"; const menuTemplate = recording ? [ { label: "Stop Recording", click: () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send("stop-recording-from-tray"); } }, }, ] : [ { label: "Open", click: () => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.isMinimized() && mainWindow.restore(); } else { createWindow(); } }, }, { label: "Quit", click: () => { app.quit(); }, }, ]; tray.setImage(trayIcon); tray.setToolTip(trayToolTip); tray.setContextMenu(Menu.buildFromTemplate(menuTemplate)); } function createEditorWindowWrapper() { if (mainWindow) { mainWindow.close() mainWindow = null } mainWindow = createEditorWindow() } function createSourceSelectorWindowWrapper() { sourceSelectorWindow = createSourceSelectorWindow() sourceSelectorWindow.on('closed', () => { sourceSelectorWindow = null }) return sourceSelectorWindow } // On macOS, applications and their menu bar stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { // Keep app running (macOS behavior) }) app.on('activate', () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) // Register all IPC handlers when app is ready app.whenReady().then(async () => { // Listen for HUD overlay quit event (macOS only) const { ipcMain } = await import('electron'); ipcMain.on('hud-overlay-close', () => { app.quit(); }); createTray() updateTrayMenu() setupApplicationMenu() // Ensure recordings directory exists await ensureRecordingsDir() registerIpcHandlers( createEditorWindowWrapper, createSourceSelectorWindowWrapper, () => mainWindow, () => sourceSelectorWindow, (recording: boolean, sourceName: string) => { selectedSourceName = sourceName if (!tray) createTray(); updateTrayMenu(recording); if (!recording) { if (mainWindow) mainWindow.restore(); } } ) createWindow() })