import { app, BrowserWindow, ipcMain, screen, powerSaveBlocker } from 'electron'; import * as path from 'path'; import * as fs from 'fs'; import { ScreenManager } from './services/ScreenManager'; import { PresenceDetector } from './services/PresenceDetector'; import { FrigateStreamer } from './services/FrigateStreamer'; import { MotionDetector } from './services/MotionDetector'; let mainWindow: BrowserWindow | null = null; let screenManager: ScreenManager | null = null; let presenceDetector: PresenceDetector | null = null; let frigateStreamer: FrigateStreamer | null = null; let motionDetector: MotionDetector | null = null; let powerSaveBlockerId: number | null = null; // Check if we're in dev mode: explicitly set NODE_ENV or running from source with vite dev server // When running from unpacked folder, app.isPackaged is false but we still want production behavior const isDev = process.env.NODE_ENV === 'development'; function createWindow(): void { const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.workAreaSize; mainWindow = new BrowserWindow({ width, height, x: 0, y: 0, fullscreen: true, kiosk: !isDev, // Kiosk mode in production frame: false, autoHideMenuBar: true, backgroundColor: '#0a0a0a', webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, sandbox: false, // Required for some electron features }, }); // Load the app if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools({ mode: 'detach' }); } else { // __dirname is dist-electron, so go up one level to reach dist mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); } // Prevent power save mode powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep'); // Initialize services screenManager = new ScreenManager(mainWindow); // Initialize motion detector (runs in main process, not throttled by browser) // Uses file size comparison which is more reliable for JPEG streams motionDetector = new MotionDetector({ go2rtcUrl: 'http://192.168.1.241:1985', cameraName: 'Kitchen_Panel', sensitivityThreshold: 5, // % file size change to trigger (5% = significant motion) checkIntervalMs: 2000, // Check every 2 seconds for responsiveness }); motionDetector.on('motion', () => { console.log('MotionDetector: Motion detected, waking screen'); screenManager?.wakeScreen(); mainWindow?.webContents.send('motion:detected'); }); motionDetector.start(); // Handle window close mainWindow.on('closed', () => { mainWindow = null; }); // Prevent accidental navigation mainWindow.webContents.on('will-navigate', (event) => { event.preventDefault(); }); // Handle external links mainWindow.webContents.setWindowOpenHandler(() => { return { action: 'deny' }; }); } // IPC handlers function setupIpcHandlers(): void { // Screen management ipcMain.handle('screen:wake', async () => { await screenManager?.wakeScreen(); return true; }); ipcMain.handle('screen:sleep', async () => { await screenManager?.sleepScreen(); return true; }); ipcMain.handle('screen:setIdleTimeout', async (_event, timeout: number) => { screenManager?.setIdleTimeout(timeout); return true; }); // Handle touch/click activity from renderer (for Wayland touch support) ipcMain.handle('screen:activity', async () => { screenManager?.handleUserActivity(); return true; }); // Presence detection control ipcMain.handle('presence:start', async () => { if (!presenceDetector) { presenceDetector = new PresenceDetector(); presenceDetector.on('personDetected', () => { mainWindow?.webContents.send('presence:detected'); screenManager?.wakeScreen(); }); presenceDetector.on('noPersonDetected', () => { mainWindow?.webContents.send('presence:cleared'); }); } await presenceDetector.start(); return true; }); ipcMain.handle('presence:stop', async () => { await presenceDetector?.stop(); return true; }); // Frigate streaming control ipcMain.handle('frigate:startStream', async (_event, rtspUrl: string) => { if (!frigateStreamer) { frigateStreamer = new FrigateStreamer(); } await frigateStreamer.start(rtspUrl); return true; }); ipcMain.handle('frigate:stopStream', async () => { await frigateStreamer?.stop(); return true; }); // Config - read stored token from file ipcMain.handle('config:getStoredToken', async () => { try { const tokenPath = path.join(app.getPath('userData'), 'ha_token.txt'); if (fs.existsSync(tokenPath)) { const token = fs.readFileSync(tokenPath, 'utf-8').trim(); return token || null; } return null; } catch { return null; } }); // Config - read Jellyfin API key from file ipcMain.handle('config:getJellyfinApiKey', async () => { try { const keyPath = path.join(app.getPath('userData'), 'jellyfin_api_key.txt'); if (fs.existsSync(keyPath)) { const key = fs.readFileSync(keyPath, 'utf-8').trim(); return key || null; } return null; } catch { return null; } }); // App control ipcMain.handle('app:quit', () => { app.quit(); }); ipcMain.handle('app:toggleFullscreen', () => { if (mainWindow) { const isFullScreen = mainWindow.isFullScreen(); mainWindow.setFullScreen(!isFullScreen); } }); ipcMain.handle('app:toggleDevTools', () => { if (isDev && mainWindow) { mainWindow.webContents.toggleDevTools(); } }); } // App lifecycle app.whenReady().then(() => { setupIpcHandlers(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { // Clean up if (powerSaveBlockerId !== null) { powerSaveBlocker.stop(powerSaveBlockerId); } presenceDetector?.stop(); frigateStreamer?.stop(); motionDetector?.stop(); if (process.platform !== 'darwin') { app.quit(); } }); // Handle uncaught exceptions process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled rejection:', reason); });