/** * OpenHamClock Electron Main Process * * Creates a native desktop application wrapper for OpenHamClock * Supports Windows, macOS, Linux, and Raspberry Pi */ const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron'); const path = require('path'); // Keep a global reference to prevent garbage collection let mainWindow; // Check if running in development const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; // Start the Express server in production let server; if (!isDev) { // In production, start the built-in server const express = require('express'); const serverApp = express(); const PORT = 3847; // Use a unique port for embedded server serverApp.use(express.static(path.join(__dirname, '..', 'public'))); serverApp.get('*', (req, res) => { res.sendFile(path.join(__dirname, '..', 'public', 'index.html')); }); server = serverApp.listen(PORT, () => { console.log(`Embedded server running on port ${PORT}`); }); } function createWindow() { // Determine the URL to load const loadURL = isDev ? 'http://localhost:3000' : `http://localhost:3847`; // Create the browser window mainWindow = new BrowserWindow({ width: 1600, height: 900, minWidth: 1024, minHeight: 600, title: 'OpenHamClock', icon: path.join(__dirname, '..', 'public', 'icons', 'icon.png'), backgroundColor: '#0a0e14', webPreferences: { nodeIntegration: false, contextIsolation: true, enableRemoteModule: false, // Preload script for any IPC communication // preload: path.join(__dirname, 'preload.js') }, // Frame options frame: true, autoHideMenuBar: false, }); // Load the app mainWindow.loadURL(loadURL); // Open DevTools in development if (isDev) { mainWindow.webContents.openDevTools(); } // Handle external links mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); // Cleanup on close mainWindow.on('closed', () => { mainWindow = null; }); // Handle fullscreen toggle with F11 mainWindow.webContents.on('before-input-event', (event, input) => { if (input.key === 'F11') { mainWindow.setFullScreen(!mainWindow.isFullScreen()); event.preventDefault(); } }); } // Create application menu function createMenu() { const template = [ { label: 'File', submenu: [ { label: 'Refresh Data', accelerator: 'F5', click: () => { if (mainWindow) { mainWindow.webContents.reload(); } } }, { type: 'separator' }, { label: 'Kiosk Mode', accelerator: 'F11', click: () => { if (mainWindow) { mainWindow.setFullScreen(!mainWindow.isFullScreen()); } } }, { type: 'separator' }, { role: 'quit' } ] }, { label: 'View', submenu: [ { role: 'reload' }, { role: 'forceReload' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' } ] }, { label: 'Map', submenu: [ { label: 'Dark Theme', accelerator: '1', click: () => sendMapStyle('dark') }, { label: 'Satellite', accelerator: '2', click: () => sendMapStyle('satellite') }, { label: 'Terrain', accelerator: '3', click: () => sendMapStyle('terrain') }, { label: 'Streets', accelerator: '4', click: () => sendMapStyle('streets') }, { label: 'Topographic', accelerator: '5', click: () => sendMapStyle('topo') }, { label: 'Ocean', accelerator: '6', click: () => sendMapStyle('ocean') }, { label: 'National Geographic', accelerator: '7', click: () => sendMapStyle('natgeo') }, { label: 'Gray', accelerator: '8', click: () => sendMapStyle('gray') } ] }, { label: 'Help', submenu: [ { label: 'About OpenHamClock', click: () => { const { dialog } = require('electron'); dialog.showMessageBox(mainWindow, { type: 'info', title: 'About OpenHamClock', message: 'OpenHamClock v3.0.0', detail: 'An open-source amateur radio dashboard.\n\nIn memory of Elwood Downey, WB0OEW, creator of the original HamClock.\n\n73 de the OpenHamClock community!' }); } }, { label: 'GitHub Repository', click: () => { shell.openExternal('https://github.com/accius/openhamclock'); } }, { type: 'separator' }, { label: 'Report Issue', click: () => { shell.openExternal('https://github.com/accius/openhamclock/issues/new'); } }, { type: 'separator' }, { label: 'Toggle Developer Tools', accelerator: 'F12', click: () => { if (mainWindow) { mainWindow.webContents.toggleDevTools(); } } } ] } ]; // macOS specific menu adjustments if (process.platform === 'darwin') { template.unshift({ label: app.getName(), submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } ] }); } const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); } // Send map style change to renderer function sendMapStyle(style) { if (mainWindow) { mainWindow.webContents.executeJavaScript(` window.postMessage({ type: 'SET_MAP_STYLE', style: '${style}' }, '*'); `); } } // App ready app.whenReady().then(() => { createWindow(); createMenu(); // macOS: recreate window when dock icon is clicked app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); // Quit when all windows are closed app.on('window-all-closed', () => { // On macOS, apps typically stay open until Cmd+Q if (process.platform !== 'darwin') { app.quit(); } }); // Cleanup on quit app.on('before-quit', () => { if (server) { server.close(); } }); // Security: Prevent navigation to external URLs app.on('web-contents-created', (event, contents) => { contents.on('will-navigate', (event, navigationUrl) => { const parsedUrl = new URL(navigationUrl); if (parsedUrl.origin !== 'http://localhost:3000' && parsedUrl.origin !== 'http://localhost:3847') { event.preventDefault(); shell.openExternal(navigationUrl); } }); });