@mytec: iter2.1 ready for testing
This commit is contained in:
314
desktop/main.js
Normal file
314
desktop/main.js
Normal file
@@ -0,0 +1,314 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const Store = require('electron-store');
|
||||
|
||||
const store = new Store();
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
let backendProcess;
|
||||
|
||||
// Paths
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
const getResourcePath = (relativePath) => {
|
||||
if (isDev) {
|
||||
return path.join(__dirname, '..', relativePath);
|
||||
}
|
||||
return path.join(process.resourcesPath, relativePath);
|
||||
};
|
||||
|
||||
const getDataPath = () => {
|
||||
return path.join(app.getPath('userData'), 'data');
|
||||
};
|
||||
|
||||
// Get backend executable name based on platform
|
||||
const getBackendExeName = () => {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return 'rfcp-server.exe';
|
||||
case 'darwin':
|
||||
return 'rfcp-server';
|
||||
case 'linux':
|
||||
return 'rfcp-server';
|
||||
default:
|
||||
return 'rfcp-server';
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure data directories exist
|
||||
function ensureDataDirs() {
|
||||
const dirs = ['terrain', 'osm', 'projects', 'cache'];
|
||||
const dataPath = getDataPath();
|
||||
|
||||
dirs.forEach(dir => {
|
||||
const fullPath = path.join(dataPath, dir);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
fs.mkdirSync(fullPath, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
// Create splash window
|
||||
function createSplashWindow() {
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 300,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
|
||||
splashWindow.loadFile(path.join(__dirname, 'splash.html'));
|
||||
splashWindow.center();
|
||||
}
|
||||
|
||||
// Start Python backend
|
||||
async function startBackend() {
|
||||
const dataPath = ensureDataDirs();
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
RFCP_DATA_PATH: dataPath,
|
||||
RFCP_DATABASE_URL: `sqlite:///${path.join(dataPath, 'rfcp.db')}`,
|
||||
RFCP_HOST: '127.0.0.1',
|
||||
RFCP_PORT: '8888',
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
// Development: run uvicorn
|
||||
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
||||
backendProcess = spawn(pythonCmd, [
|
||||
'-m', 'uvicorn',
|
||||
'app.main:app',
|
||||
'--host', '127.0.0.1',
|
||||
'--port', '8888',
|
||||
'--reload'
|
||||
], {
|
||||
cwd: path.join(__dirname, '..', 'backend'),
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
} else {
|
||||
// Production: run PyInstaller bundle
|
||||
const backendExe = path.join(
|
||||
getResourcePath('backend'),
|
||||
getBackendExeName()
|
||||
);
|
||||
|
||||
// Make executable on Unix
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
fs.chmodSync(backendExe, '755');
|
||||
} catch (e) {
|
||||
console.log('chmod failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
backendProcess = spawn(backendExe, [], {
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
}
|
||||
|
||||
// Log backend output
|
||||
backendProcess.stdout?.on('data', (data) => {
|
||||
console.log(`[Backend] ${data}`);
|
||||
});
|
||||
|
||||
backendProcess.stderr?.on('data', (data) => {
|
||||
console.error(`[Backend Error] ${data}`);
|
||||
});
|
||||
|
||||
backendProcess.on('error', (err) => {
|
||||
console.error('Failed to start backend:', err);
|
||||
dialog.showErrorBox('Backend Error', `Failed to start backend: ${err.message}`);
|
||||
});
|
||||
|
||||
// Wait for backend to be ready
|
||||
return new Promise((resolve) => {
|
||||
const maxAttempts = 60; // 30 seconds
|
||||
let attempts = 0;
|
||||
|
||||
const checkBackend = setInterval(async () => {
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:8888/api/health/');
|
||||
if (response.ok) {
|
||||
clearInterval(checkBackend);
|
||||
console.log('Backend ready!');
|
||||
resolve(true);
|
||||
}
|
||||
} catch (_e) {
|
||||
// Not ready yet
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
clearInterval(checkBackend);
|
||||
console.error('Backend failed to start in time');
|
||||
resolve(false);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Create main window
|
||||
function createMainWindow() {
|
||||
const windowState = store.get('windowState', {
|
||||
width: 1400,
|
||||
height: 900
|
||||
});
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: windowState.width,
|
||||
height: windowState.height,
|
||||
x: windowState.x,
|
||||
y: windowState.y,
|
||||
minWidth: 1024,
|
||||
minHeight: 768,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
},
|
||||
icon: path.join(__dirname, 'assets',
|
||||
process.platform === 'win32' ? 'icon.ico' : 'icon.png'
|
||||
),
|
||||
title: 'RFCP - RF Coverage Planner',
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
||||
});
|
||||
|
||||
// Save window state on close
|
||||
mainWindow.on('close', () => {
|
||||
const bounds = mainWindow.getBounds();
|
||||
store.set('windowState', bounds);
|
||||
});
|
||||
|
||||
// Load frontend
|
||||
if (isDev) {
|
||||
mainWindow.loadURL('http://localhost:5173');
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(getResourcePath('frontend'), 'index.html'));
|
||||
}
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
if (splashWindow) {
|
||||
splashWindow.close();
|
||||
splashWindow = null;
|
||||
}
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Handle external links
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
}
|
||||
|
||||
// App lifecycle
|
||||
app.whenReady().then(async () => {
|
||||
createSplashWindow();
|
||||
|
||||
const backendStarted = await startBackend();
|
||||
|
||||
if (!backendStarted) {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Startup Error',
|
||||
message: 'Failed to start backend server.',
|
||||
detail: 'Please check logs and try again. Click "Show Logs" to open the log folder.',
|
||||
buttons: ['Quit', 'Show Logs']
|
||||
});
|
||||
|
||||
if (result.response === 1) {
|
||||
shell.openPath(app.getPath('logs'));
|
||||
}
|
||||
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
createMainWindow();
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (backendProcess) {
|
||||
backendProcess.kill();
|
||||
backendProcess = null;
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (backendProcess) {
|
||||
backendProcess.kill();
|
||||
}
|
||||
});
|
||||
|
||||
// IPC Handlers
|
||||
ipcMain.handle('get-data-path', () => getDataPath());
|
||||
ipcMain.handle('get-app-version', () => app.getVersion());
|
||||
ipcMain.handle('get-platform', () => process.platform);
|
||||
|
||||
ipcMain.handle('get-gpu-info', async () => {
|
||||
// TODO: Implement GPU detection
|
||||
return {
|
||||
available: false,
|
||||
name: null,
|
||||
cudaVersion: null
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('select-directory', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
return result.filePaths[0] || null;
|
||||
});
|
||||
|
||||
ipcMain.handle('select-file', async (_event, options) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
filters: options?.filters || []
|
||||
});
|
||||
return result.filePaths[0] || null;
|
||||
});
|
||||
|
||||
ipcMain.handle('save-file', async (_event, options) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow, {
|
||||
defaultPath: options?.defaultPath,
|
||||
filters: options?.filters || []
|
||||
});
|
||||
return result.filePath || null;
|
||||
});
|
||||
|
||||
ipcMain.handle('get-setting', (_event, key) => store.get(key));
|
||||
ipcMain.handle('set-setting', (_event, key, value) => store.set(key, value));
|
||||
|
||||
ipcMain.handle('open-external', (_event, url) => shell.openExternal(url));
|
||||
ipcMain.handle('open-path', (_event, filePath) => shell.openPath(filePath));
|
||||
Reference in New Issue
Block a user