Files
beStream/electron/main.ts
2025-12-14 18:45:49 +01:00

348 lines
9.2 KiB
TypeScript

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 = '<div style="padding: 20px; font-family: monospace; color: red;">
<h1>Error Loading App</h1>
<p>Failed to load index.html</p>
<p>Path: ${indexPath}</p>
<p>Error: ${error.message}</p>
<p>App Path: ${app.getAppPath()}</p>
</div>';
`);
});
}
// 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<void>((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);
});
}