Files
fourdst/electron/main.js

300 lines
9.6 KiB
JavaScript

const { app, BrowserWindow, ipcMain, dialog, nativeTheme } = require('electron');
const path = require('path');
const fs = require('fs-extra');
const yaml = require('js-yaml');
const AdmZip = require('adm-zip');
const { spawn } = require('child_process');
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
app.quit();
}
let mainWindow;
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
},
});
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'index.html'));
// Open the DevTools for debugging
// mainWindow.webContents.openDevTools();
nativeTheme.on('updated', () => {
if (mainWindow) {
mainWindow.webContents.send('theme-updated', { shouldUseDarkColors: nativeTheme.shouldUseDarkColors });
}
});
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
ipcMain.handle('get-dark-mode', () => {
return nativeTheme.shouldUseDarkColors;
});
ipcMain.on('show-error-dialog', (event, { title, content }) => {
dialog.showErrorBox(title, content);
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// IPC handlers
ipcMain.handle('select-file', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'Fbundle Archives', extensions: ['fbundle'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('select-directory', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory']
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('select-save-file', async () => {
const result = await dialog.showSaveDialog({
filters: [
{ name: 'Fbundle Archives', extensions: ['fbundle'] }
]
});
if (!result.canceled) {
return result.filePath;
}
return null;
});
// Helper function to run python commands via the bundled backend
function runPythonCommand(command, kwargs, event) {
const buildDir = path.resolve(__dirname, '..', 'build');
let backendPath;
if (app.isPackaged) {
backendPath = path.join(process.resourcesPath, 'fourdst-backend');
} else {
backendPath = path.join(buildDir, 'electron', 'dist', 'fourdst-backend', 'fourdst-backend');
}
console.log(`[MAIN_PROCESS] Spawning backend: ${backendPath}`);
const args = [command, JSON.stringify(kwargs)];
console.log(`[MAIN_PROCESS] With args: [${args.join(', ')}]`);
return new Promise((resolve) => {
const process = spawn(backendPath, args);
let stdoutBuffer = '';
let errorOutput = '';
process.stderr.on('data', (data) => {
errorOutput += data.toString();
console.error('Backend STDERR:', data.toString().trim());
});
const isStreaming = command === 'fill_bundle';
process.stdout.on('data', (data) => {
const chunk = data.toString();
stdoutBuffer += chunk;
if (isStreaming && event) {
// Process buffer line by line for streaming commands
let newlineIndex;
while ((newlineIndex = stdoutBuffer.indexOf('\n')) >= 0) {
const line = stdoutBuffer.substring(0, newlineIndex).trim();
stdoutBuffer = stdoutBuffer.substring(newlineIndex + 1);
if (line) {
try {
const parsed = JSON.parse(line);
if (parsed.type === 'progress') {
event.sender.send('fill-bundle-progress', parsed.data);
} else {
// Not a progress update, put it back in the buffer for final processing
stdoutBuffer = line + '\n' + stdoutBuffer;
break; // Stop processing lines
}
} catch (e) {
// Ignore parsing errors for intermediate lines in a stream
}
}
}
}
});
process.on('close', (code) => {
console.log(`[MAIN_PROCESS] Backend process exited with code ${code}`);
let resultData = null;
try {
// Core functions now return clean JSON directly
const finalJson = JSON.parse(stdoutBuffer.trim());
resultData = finalJson; // Use the JSON response directly
} catch (e) {
console.error(`[MAIN_PROCESS] Could not parse backend output as JSON: ${e}`);
console.error(`[MAIN_PROCESS] Raw output: "${stdoutBuffer}"`);
// If parsing fails, return a structured error response
resultData = {
success: false,
error: `JSON parsing failed: ${e.message}`,
raw_output: stdoutBuffer
};
}
const finalError = errorOutput.trim();
if (finalError && !resultData) {
resolve({ success: false, error: finalError });
} else if (resultData) {
resolve(resultData);
} else {
const errorMessage = finalError || `The script finished without returning a result (exit code: ${code})`;
resolve({ success: false, error: errorMessage });
}
});
process.on('error', (err) => {
resolve({ success: false, error: `Failed to start backend process: ${err.message}` });
});
});
}
ipcMain.handle('create-bundle', async (event, bundleData) => {
const kwargs = {
plugin_dirs: bundleData.pluginDirs,
output_path: bundleData.outputPath,
bundle_name: bundleData.bundleName,
bundle_version: bundleData.bundleVersion,
bundle_author: bundleData.bundleAuthor,
bundle_comment: bundleData.bundleComment,
};
const result = await runPythonCommand('create_bundle', kwargs, event);
// The renderer expects a 'path' property on success
if (result.success) {
result.path = bundleData.outputPath;
}
return result;
});
ipcMain.handle('sign-bundle', async (event, bundlePath) => {
// Prompt for private key
const result = await dialog.showOpenDialog({
properties: ['openFile'],
title: 'Select Private Key',
filters: [{ name: 'PEM Private Key', extensions: ['pem'] }],
});
if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
return { success: false, error: 'Private key selection was canceled.' };
}
const privateKeyPath = result.filePaths[0];
const kwargs = {
bundle_path: bundlePath,
private_key: privateKeyPath,
};
return runPythonCommand('sign_bundle', kwargs, event);
});
ipcMain.handle('validate-bundle', async (event, bundlePath) => {
const kwargs = {
bundle_path: bundlePath
};
return runPythonCommand('validate_bundle', kwargs, event);
});
ipcMain.handle('clear-bundle', async (event, bundlePath) => {
const kwargs = { bundle_path: bundlePath };
return runPythonCommand('clear_bundle', kwargs, event);
});
ipcMain.handle('get-fillable-targets', async (event, bundlePath) => {
const kwargs = { bundle_path: bundlePath };
return runPythonCommand('get_fillable_targets', kwargs, event);
});
ipcMain.handle('fill-bundle', async (event, { bundlePath, targetsToBuild }) => {
const kwargs = {
bundle_path: bundlePath,
targets_to_build: targetsToBuild
};
// Pass event to stream progress
return runPythonCommand('fill_bundle', kwargs, event);
});
ipcMain.handle('edit-bundle', async (event, { bundlePath, updatedManifest }) => {
const kwargs = {
bundle_path: bundlePath,
metadata: updatedManifest
};
return runPythonCommand('edit_bundle_metadata', kwargs, event);
});
ipcMain.handle('open-bundle', async (event, bundlePath) => {
console.log(`[IPC_HANDLER] Opening bundle: ${bundlePath}`);
const kwargs = { bundle_path: bundlePath };
const result = await runPythonCommand('inspect_bundle', kwargs, event);
console.log(`[IPC_HANDLER] inspect_bundle result:`, result);
// Core functions now return consistent JSON structure directly
if (result && result.success) {
// The core inspect_bundle function returns the data directly
// We just need to add the bundlePath for the renderer
return {
success: true,
manifest: result.manifest,
report: result, // The entire result is the report
bundlePath: bundlePath
};
}
// Return error as-is since it's already in the correct format
return result || { success: false, error: 'An unknown error occurred while opening the bundle.' };
});