import { app, BrowserWindow, ipcMain, shell, Menu, Tray, nativeImage } from 'electron'; import * as path from 'path'; import * as url from 'url'; import { fileURLToPath } from 'url'; import { fork, ChildProcess } from 'child_process'; const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; let serverProcess: ChildProcess | null = null; // ES module compatible __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 800, minHeight: 600, backgroundColor: '#141414', titleBarStyle: 'hiddenInset', frame: process.platform === 'darwin' ? true : false, webPreferences: { nodeIntegration: true, contextIsolation: false, webSecurity: false, }, icon: path.join(__dirname, '../public/icon.png'), }); // Load the app if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { const indexPath = path.join(app.getAppPath(), 'dist', 'index.html'); console.log('Loading index.html from:', indexPath); mainWindow.loadFile(indexPath).catch((error) => { console.error('Failed to load index.html:', error); // Show error in window mainWindow?.webContents.executeJavaScript(` document.body.innerHTML = '

Error Loading App

Failed to load index.html

Path: ${indexPath}

Error: ${error.message}

App Path: ${app.getAppPath()}

'; `); }); } // Log all console messages from renderer mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => { console.log(`[Renderer ${level}] ${message} (${sourceId}:${line})`); }); // Log page load events mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => { console.error('Page failed to load:', { errorCode, errorDescription, url: validatedURL, }); }); mainWindow.webContents.on('did-finish-load', () => { console.log('Page finished loading'); }); // Handle external links mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); mainWindow.on('closed', () => { mainWindow = null; }); // Create application menu createMenu(); } function createMenu() { const template: Electron.MenuItemConstructorOptions[] = [ { label: 'beStream', submenu: [ { label: 'About beStream', role: 'about' }, { type: 'separator' }, { label: 'Preferences', accelerator: 'CmdOrCtrl+,', click: () => { mainWindow?.webContents.send('navigate', '/settings'); }}, { type: 'separator' }, { label: 'Hide beStream', role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { label: 'Quit beStream', role: 'quit' }, ], }, { label: 'File', submenu: [ { label: 'New Window', accelerator: 'CmdOrCtrl+N', click: createWindow }, { type: 'separator' }, { label: 'Close Window', role: 'close' }, ], }, { 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: 'Navigate', submenu: [ { label: 'Home', accelerator: 'CmdOrCtrl+H', click: () => { mainWindow?.webContents.send('navigate', '/'); }}, { label: 'Browse', accelerator: 'CmdOrCtrl+B', click: () => { mainWindow?.webContents.send('navigate', '/browse'); }}, { label: 'Watchlist', accelerator: 'CmdOrCtrl+L', click: () => { mainWindow?.webContents.send('navigate', '/watchlist'); }}, { label: 'Downloads', accelerator: 'CmdOrCtrl+D', click: () => { mainWindow?.webContents.send('navigate', '/downloads'); }}, { type: 'separator' }, { label: 'Back', accelerator: 'CmdOrCtrl+[', click: () => { mainWindow?.webContents.goBack(); }}, { label: 'Forward', accelerator: 'CmdOrCtrl+]', click: () => { mainWindow?.webContents.goForward(); }}, ], }, { label: 'Window', submenu: [ { role: 'minimize' }, { role: 'zoom' }, { type: 'separator' }, { role: 'front' }, ], }, { label: 'Help', submenu: [ { label: 'Learn More', click: () => shell.openExternal('https://github.com') }, ], }, ]; const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); } function createTray() { const iconPath = path.join(__dirname, '../public/icon.png'); const icon = nativeImage.createFromPath(iconPath); tray = new Tray(icon.resize({ width: 16, height: 16 })); const contextMenu = Menu.buildFromTemplate([ { label: 'Open beStream', click: () => mainWindow?.show() }, { type: 'separator' }, { label: 'Downloads', click: () => { mainWindow?.show(); mainWindow?.webContents.send('navigate', '/downloads'); }}, { type: 'separator' }, { label: 'Quit', role: 'quit' }, ]); tray.setToolTip('beStream'); tray.setContextMenu(contextMenu); tray.on('click', () => { mainWindow?.show(); }); } /** * Start the backend server using Electron's bundled Node.js */ function startServer() { const serverDir = path.join(app.getAppPath(), 'server'); const serverPath = path.join(serverDir, 'src', 'index.js'); console.log('Starting server from:', serverPath); console.log('Server directory:', serverDir); // Use fork() which automatically uses Electron's bundled Node.js runtime // This works in both development and packaged apps serverProcess = fork(serverPath, [], { cwd: serverDir, env: { ...process.env, PORT: '3001', NODE_ENV: isDev ? 'development' : 'production', }, stdio: ['ignore', 'pipe', 'pipe', 'ipc'], // 'ipc' channel for fork communication silent: false, // Allow server to output to console }); serverProcess.stdout?.on('data', (data) => { console.log(`[Server] ${data.toString()}`); }); serverProcess.stderr?.on('data', (data) => { console.error(`[Server Error] ${data.toString()}`); }); serverProcess.on('exit', (code, signal) => { console.log(`Server process exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`); serverProcess = null; }); serverProcess.on('error', (error) => { console.error('Failed to start server:', error); serverProcess = null; }); // Wait a bit for server to start before creating window return new Promise((resolve) => { // Give server time to start setTimeout(() => { // Check if server is still running if (serverProcess && !serverProcess.killed) { console.log('Server started successfully'); } else { console.warn('Server may not have started properly'); } resolve(); }, 2000); }); } /** * Stop the backend server */ function stopServer() { if (serverProcess) { serverProcess.kill(); serverProcess = null; } } // App event handlers app.whenReady().then(async () => { // Start server first await startServer(); createWindow(); createTray(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { stopServer(); if (process.platform !== 'darwin') { app.quit(); } }); app.on('before-quit', () => { stopServer(); }); // IPC handlers for renderer communication ipcMain.handle('get-app-path', () => { return app.getPath('userData'); }); ipcMain.handle('get-downloads-path', () => { return app.getPath('downloads'); }); ipcMain.handle('show-item-in-folder', (_event, filePath: string) => { shell.showItemInFolder(filePath); }); ipcMain.handle('open-external', (_event, url: string) => { shell.openExternal(url); }); // Window control handlers ipcMain.handle('window-minimize', () => { mainWindow?.minimize(); }); ipcMain.handle('window-maximize', () => { if (mainWindow?.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow?.maximize(); } }); ipcMain.handle('window-close', () => { mainWindow?.close(); }); ipcMain.handle('window-is-maximized', () => { return mainWindow?.isMaximized() ?? false; }); // Handle certificate errors for development if (isDev) { app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { event.preventDefault(); callback(true); }); }