348 lines
9.2 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
|