refactor(electron): major ui refactor into modules
This commit is contained in:
@@ -482,7 +482,7 @@
|
||||
<div class="modal-content info-modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>About 4DSTAR</h3>
|
||||
<button class="modal-close" onclick="hideInfoModal()">×</button>
|
||||
<button id="close-info-modal" class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="info-modal-body">
|
||||
<div class="info-tab-nav">
|
||||
@@ -560,7 +560,7 @@
|
||||
<div id="spinner" class="spinner hidden"></div>
|
||||
|
||||
<script src="opatParser.js"></script>
|
||||
<script src="renderer.js"></script>
|
||||
<script src="renderer-refactored.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
30
electron/main-refactored.js
Normal file
30
electron/main-refactored.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// === REGENERATED CODE START ===
|
||||
// This file was regenerated during refactoring to use modular components
|
||||
// Original functionality preserved in modular structure
|
||||
|
||||
// Import modular components
|
||||
const { setupAppEventHandlers, setupThemeHandlers } = require('./main/app-lifecycle');
|
||||
const { setupFileDialogHandlers } = require('./main/file-dialogs');
|
||||
const { setupBundleIPCHandlers } = require('./main/ipc-handlers');
|
||||
|
||||
// Initialize all modules in the correct order
|
||||
function initializeMainProcess() {
|
||||
// Setup app lifecycle and window management
|
||||
setupAppEventHandlers();
|
||||
|
||||
// Setup theme handling
|
||||
setupThemeHandlers();
|
||||
|
||||
// Setup file dialog handlers
|
||||
setupFileDialogHandlers();
|
||||
|
||||
// Setup bundle operation IPC handlers
|
||||
setupBundleIPCHandlers();
|
||||
|
||||
console.log('[MAIN_PROCESS] All modules initialized successfully');
|
||||
}
|
||||
|
||||
// Start the application
|
||||
initializeMainProcess();
|
||||
|
||||
// === REGENERATED CODE END ===
|
||||
317
electron/main.js
317
electron/main.js
@@ -1,317 +0,0 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, nativeTheme } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const yaml = require('js-yaml');
|
||||
const AdmZip = require('adm-zip');
|
||||
|
||||
// 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,
|
||||
minWidth: 900,
|
||||
minHeight: 600,
|
||||
icon: path.join(__dirname, 'toolkitIcon.png'),
|
||||
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.' };
|
||||
});
|
||||
|
||||
// Handle show save dialog
|
||||
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||||
const result = await dialog.showSaveDialog(BrowserWindow.fromWebContents(event.sender), options);
|
||||
return result;
|
||||
});
|
||||
|
||||
// Handle file copying
|
||||
ipcMain.handle('copy-file', async (event, { source, destination }) => {
|
||||
try {
|
||||
await fs.copy(source, destination);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
78
electron/main/app-lifecycle.js
Normal file
78
electron/main/app-lifecycle.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
// 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,
|
||||
minWidth: 900,
|
||||
minHeight: 600,
|
||||
icon: path.join(__dirname, '..', 'toolkitIcon.png'),
|
||||
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 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setupAppEventHandlers = () => {
|
||||
// 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.
|
||||
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();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setupThemeHandlers = () => {
|
||||
ipcMain.handle('get-dark-mode', () => {
|
||||
return nativeTheme.shouldUseDarkColors;
|
||||
});
|
||||
};
|
||||
|
||||
const getMainWindow = () => {
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setupAppEventHandlers,
|
||||
setupThemeHandlers,
|
||||
getMainWindow,
|
||||
createWindow
|
||||
};
|
||||
98
electron/main/backend-bridge.js
Normal file
98
electron/main/backend-bridge.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
// 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}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runPythonCommand
|
||||
};
|
||||
60
electron/main/file-dialogs.js
Normal file
60
electron/main/file-dialogs.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const { ipcMain, dialog, BrowserWindow } = require('electron');
|
||||
|
||||
const setupFileDialogHandlers = () => {
|
||||
// File selection dialog
|
||||
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;
|
||||
});
|
||||
|
||||
// Directory selection dialog
|
||||
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;
|
||||
});
|
||||
|
||||
// Save file dialog
|
||||
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;
|
||||
});
|
||||
|
||||
// Generic save dialog with options
|
||||
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||||
const result = await dialog.showSaveDialog(BrowserWindow.fromWebContents(event.sender), options);
|
||||
return result;
|
||||
});
|
||||
|
||||
// Error dialog
|
||||
ipcMain.on('show-error-dialog', (event, { title, content }) => {
|
||||
dialog.showErrorBox(title, content);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setupFileDialogHandlers
|
||||
};
|
||||
128
electron/main/ipc-handlers.js
Normal file
128
electron/main/ipc-handlers.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const { ipcMain, dialog } = require('electron');
|
||||
const { runPythonCommand } = require('./backend-bridge');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const setupBundleIPCHandlers = () => {
|
||||
// Create bundle handler
|
||||
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;
|
||||
});
|
||||
|
||||
// Sign bundle handler
|
||||
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);
|
||||
});
|
||||
|
||||
// Validate bundle handler
|
||||
ipcMain.handle('validate-bundle', async (event, bundlePath) => {
|
||||
const kwargs = {
|
||||
bundle_path: bundlePath
|
||||
};
|
||||
return runPythonCommand('validate_bundle', kwargs, event);
|
||||
});
|
||||
|
||||
// Clear bundle handler
|
||||
ipcMain.handle('clear-bundle', async (event, bundlePath) => {
|
||||
const kwargs = { bundle_path: bundlePath };
|
||||
return runPythonCommand('clear_bundle', kwargs, event);
|
||||
});
|
||||
|
||||
// Get fillable targets handler
|
||||
ipcMain.handle('get-fillable-targets', async (event, bundlePath) => {
|
||||
const kwargs = { bundle_path: bundlePath };
|
||||
return runPythonCommand('get_fillable_targets', kwargs, event);
|
||||
});
|
||||
|
||||
// Fill bundle handler
|
||||
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);
|
||||
});
|
||||
|
||||
// Edit bundle metadata handler
|
||||
ipcMain.handle('edit-bundle', async (event, { bundlePath, updatedManifest }) => {
|
||||
const kwargs = {
|
||||
bundle_path: bundlePath,
|
||||
metadata: updatedManifest
|
||||
};
|
||||
return runPythonCommand('edit_bundle_metadata', kwargs, event);
|
||||
});
|
||||
|
||||
// Open bundle handler
|
||||
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.' };
|
||||
});
|
||||
|
||||
// File copying handler
|
||||
ipcMain.handle('copy-file', async (event, { source, destination }) => {
|
||||
try {
|
||||
const fs = require('fs-extra');
|
||||
await fs.copy(source, destination);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setupBundleIPCHandlers
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "fourdst-bundle-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "Electron app for managing fbundle archives",
|
||||
"main": "main.js",
|
||||
"main": "main-refactored.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dev": "electron .",
|
||||
|
||||
88
electron/renderer-refactored.js
Normal file
88
electron/renderer-refactored.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// === REGENERATED CODE START ===
|
||||
// This file was regenerated during refactoring to use modular components
|
||||
// Original functionality preserved in modular structure
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
// Import all modular components
|
||||
const stateManager = require('./renderer/state-manager');
|
||||
const domManager = require('./renderer/dom-manager');
|
||||
const bundleOperations = require('./renderer/bundle-operations');
|
||||
const uiComponents = require('./renderer/ui-components');
|
||||
const eventHandlers = require('./renderer/event-handlers');
|
||||
const opatHandler = require('./renderer/opat-handler');
|
||||
const fillWorkflow = require('./renderer/fill-workflow');
|
||||
|
||||
// Initialize all modules with their dependencies
|
||||
function initializeModules() {
|
||||
// Create dependency object
|
||||
const deps = {
|
||||
stateManager,
|
||||
domManager,
|
||||
bundleOperations,
|
||||
uiComponents,
|
||||
eventHandlers,
|
||||
opatHandler,
|
||||
fillWorkflow
|
||||
};
|
||||
|
||||
// Initialize each module with its dependencies
|
||||
bundleOperations.initializeDependencies(deps);
|
||||
uiComponents.initializeDependencies(deps);
|
||||
eventHandlers.initializeDependencies(deps);
|
||||
opatHandler.initializeDependencies(deps);
|
||||
fillWorkflow.initializeDependencies(deps);
|
||||
|
||||
console.log('[RENDERER] All modules initialized with dependencies');
|
||||
}
|
||||
|
||||
// Main initialization function
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('[RENDERER] Starting modular initialization...');
|
||||
|
||||
// Initialize DOM elements first
|
||||
domManager.initializeDOMElements();
|
||||
|
||||
// Initialize OPAT components
|
||||
opatHandler.initializeOPATElements();
|
||||
|
||||
// Initialize all module dependencies
|
||||
initializeModules();
|
||||
|
||||
// Initialize home screen - set home as default active category
|
||||
const homeCategory = document.querySelector('.category-item[data-category="home"]');
|
||||
const secondarySidebar = document.getElementById('secondary-sidebar');
|
||||
|
||||
if (homeCategory) {
|
||||
homeCategory.classList.add('active');
|
||||
opatHandler.showCategoryHomeScreen('home');
|
||||
|
||||
// Hide secondary sidebar on initial load since we start with home
|
||||
if (secondarySidebar) {
|
||||
secondarySidebar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial view
|
||||
domManager.showView('welcome-screen');
|
||||
|
||||
// Set initial theme
|
||||
const isDarkMode = await ipcRenderer.invoke('get-dark-mode');
|
||||
document.body.classList.toggle('dark-mode', isDarkMode);
|
||||
|
||||
// Setup all event listeners
|
||||
eventHandlers.setupEventListeners();
|
||||
|
||||
console.log('[RENDERER] Modular initialization complete');
|
||||
});
|
||||
|
||||
// Export modules for global access (for compatibility with existing code)
|
||||
window.stateManager = stateManager;
|
||||
window.domManager = domManager;
|
||||
window.bundleOperations = bundleOperations;
|
||||
window.uiComponents = uiComponents;
|
||||
window.eventHandlers = eventHandlers;
|
||||
window.opatHandler = opatHandler;
|
||||
window.fillWorkflow = fillWorkflow;
|
||||
|
||||
// === REGENERATED CODE END ===
|
||||
1390
electron/renderer.js
1390
electron/renderer.js
File diff suppressed because it is too large
Load Diff
258
electron/renderer/bundle-operations.js
Normal file
258
electron/renderer/bundle-operations.js
Normal file
@@ -0,0 +1,258 @@
|
||||
// Bundle operations module for the 4DSTAR Bundle Manager
|
||||
// Extracted from renderer.js to centralize bundle-specific business logic
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
// Import dependencies (these will be injected or imported when integrated)
|
||||
let stateManager, domManager, uiComponents;
|
||||
|
||||
// --- BUNDLE ACTIONS HANDLERS ---
|
||||
async function handleOpenBundle() {
|
||||
const bundlePath = await ipcRenderer.invoke('select-file');
|
||||
if (!bundlePath) return;
|
||||
|
||||
// Small delay to ensure file dialog closes properly
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
domManager.showSpinner();
|
||||
domManager.showModal('Opening...', `Opening bundle: ${path.basename(bundlePath)}`);
|
||||
const result = await ipcRenderer.invoke('open-bundle', bundlePath);
|
||||
domManager.hideSpinner();
|
||||
|
||||
if (result.success) {
|
||||
stateManager.setBundleState(result, bundlePath);
|
||||
displayBundleInfo(result.report);
|
||||
domManager.showView('bundle-view');
|
||||
domManager.hideModal();
|
||||
} else {
|
||||
domManager.showModal('Error Opening Bundle', `Failed to open bundle: ${result ? result.error : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSignBundle() {
|
||||
const currentBundlePath = stateManager.getCurrentBundlePath();
|
||||
if (!currentBundlePath) return;
|
||||
|
||||
domManager.showSpinner();
|
||||
const signResult = await ipcRenderer.invoke('sign-bundle', currentBundlePath);
|
||||
domManager.hideSpinner();
|
||||
|
||||
if (signResult.success) {
|
||||
domManager.showModal('Success', 'Bundle signed successfully.');
|
||||
await reloadCurrentBundle();
|
||||
domManager.hideModal();
|
||||
} else {
|
||||
domManager.showModal('Sign Error', `Failed to sign bundle: ${signResult.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleValidateBundle() {
|
||||
const currentBundlePath = stateManager.getCurrentBundlePath();
|
||||
if (!currentBundlePath) return;
|
||||
|
||||
domManager.showSpinner();
|
||||
const result = await ipcRenderer.invoke('validate-bundle', currentBundlePath);
|
||||
domManager.hideSpinner();
|
||||
|
||||
if (result.success) {
|
||||
// With the new JSON architecture, validation data is directly in result
|
||||
const errors = result.errors || [];
|
||||
const warnings = result.warnings || [];
|
||||
const validationIssues = errors.concat(warnings);
|
||||
|
||||
const elements = domManager.getElements();
|
||||
if (validationIssues.length > 0) {
|
||||
elements.validationResults.textContent = validationIssues.join('\n');
|
||||
elements.validationTabLink.classList.remove('hidden');
|
||||
} else {
|
||||
elements.validationResults.textContent = 'Bundle is valid.';
|
||||
elements.validationTabLink.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Switch to the validation tab to show the results
|
||||
domManager.switchTab('validation-tab');
|
||||
|
||||
// Show summary in modal
|
||||
const summary = result.summary || { errors: errors.length, warnings: warnings.length };
|
||||
const message = `Validation finished with ${summary.errors} errors and ${summary.warnings} warnings.`;
|
||||
domManager.showModal('Validation Complete', message);
|
||||
|
||||
} else {
|
||||
domManager.showModal('Validation Error', `Failed to validate bundle: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClearBundle() {
|
||||
const currentBundlePath = stateManager.getCurrentBundlePath();
|
||||
if (!currentBundlePath) return;
|
||||
|
||||
domManager.showSpinner();
|
||||
const result = await ipcRenderer.invoke('clear-bundle', currentBundlePath);
|
||||
domManager.hideSpinner();
|
||||
|
||||
if (result.success) {
|
||||
domManager.showModal('Success', 'All binaries have been cleared. Reloading...');
|
||||
await reloadCurrentBundle();
|
||||
domManager.hideModal();
|
||||
} else {
|
||||
domManager.showModal('Clear Error', `Failed to clear binaries: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFillBundle() {
|
||||
const currentBundle = stateManager.getCurrentBundle();
|
||||
if (!currentBundle) return domManager.showModal('Action Canceled', 'Please open a bundle first.');
|
||||
|
||||
domManager.showSpinner();
|
||||
domManager.showModal('Filling Bundle...', 'Adding local binaries to bundle.');
|
||||
const result = await ipcRenderer.invoke('fill-bundle', currentBundle.bundlePath);
|
||||
domManager.hideSpinner();
|
||||
|
||||
if (result.success) {
|
||||
domManager.showModal('Success', 'Binaries filled successfully. Reloading...');
|
||||
await reloadCurrentBundle();
|
||||
domManager.hideModal();
|
||||
} else {
|
||||
domManager.showModal('Fill Error', `Failed to fill bundle: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- DATA DISPLAY ---
|
||||
async function reloadCurrentBundle() {
|
||||
const currentBundle = stateManager.getCurrentBundle();
|
||||
if (!currentBundle) return;
|
||||
|
||||
const reloadResult = await ipcRenderer.invoke('open-bundle', currentBundle.bundlePath);
|
||||
if (reloadResult.success) {
|
||||
stateManager.setBundleState(reloadResult, currentBundle.bundlePath);
|
||||
displayBundleInfo(reloadResult.report);
|
||||
} else {
|
||||
domManager.showModal('Reload Error', `Failed to reload bundle details: ${reloadResult.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function displayBundleInfo(report) {
|
||||
if (!report) {
|
||||
domManager.showModal('Display Error', 'Could not load bundle information.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { manifest, signature, validation, plugins } = report;
|
||||
const elements = domManager.getElements();
|
||||
|
||||
// Store original metadata for comparison
|
||||
stateManager.updateOriginalMetadata({
|
||||
bundleVersion: manifest.bundleVersion || '',
|
||||
bundleAuthor: manifest.bundleAuthor || '',
|
||||
bundleComment: manifest.bundleComment || ''
|
||||
});
|
||||
stateManager.markUnsavedChanges(false);
|
||||
updateSaveButtonVisibility();
|
||||
|
||||
// Set bundle title
|
||||
elements.bundleTitle.textContent = manifest.bundleName || 'Untitled Bundle';
|
||||
|
||||
// --- Overview Tab ---
|
||||
const trustStatus = signature.status || 'UNSIGNED';
|
||||
const trustColorClass = {
|
||||
'TRUSTED': 'trusted',
|
||||
'UNTRUSTED': 'untrusted',
|
||||
'INVALID': 'untrusted',
|
||||
'TAMPERED': 'untrusted',
|
||||
'UNSIGNED': 'unsigned',
|
||||
'ERROR': 'untrusted',
|
||||
'UNSUPPORTED': 'warning'
|
||||
}[trustStatus] || 'unsigned';
|
||||
|
||||
elements.manifestDetails.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Trust Status</h3>
|
||||
<div class="trust-indicator-container">
|
||||
<div class="trust-indicator ${trustColorClass}"></div>
|
||||
<span>${trustStatus}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>Manifest Details</h3></div>
|
||||
<div class="card-content">
|
||||
${uiComponents.createEditableField('Version', 'bundleVersion', manifest.bundleVersion || 'N/A')}
|
||||
${uiComponents.createEditableField('Author', 'bundleAuthor', manifest.bundleAuthor || 'N/A')}
|
||||
<p><strong>Bundled On:</strong> ${manifest.bundledOn || 'N/A'}</p>
|
||||
${uiComponents.createEditableField('Comment', 'bundleComment', manifest.bundleComment || 'N/A')}
|
||||
${manifest.bundleAuthorKeyFingerprint ? `<p><strong>Author Key:</strong> ${manifest.bundleAuthorKeyFingerprint}</p>` : ''}
|
||||
${manifest.bundleSignature ? `<p><strong>Signature:</strong> <span class="signature">${manifest.bundleSignature}</span></p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listeners for edit functionality
|
||||
uiComponents.setupEditableFieldListeners();
|
||||
|
||||
// --- Plugins Tab ---
|
||||
elements.pluginsList.innerHTML = '';
|
||||
if (plugins && Object.keys(plugins).length > 0) {
|
||||
Object.entries(plugins).forEach(([pluginName, pluginData]) => {
|
||||
const binariesInfo = pluginData.binaries.map(b => {
|
||||
const compatClass = b.is_compatible ? 'compatible' : 'incompatible';
|
||||
const compatText = b.is_compatible ? 'Compatible' : 'Incompatible';
|
||||
const platformTriplet = b.platform && b.platform.triplet ? `(${b.platform.triplet})` : '';
|
||||
return `<li class="binary-info ${compatClass}"><strong>${b.path}</strong> ${platformTriplet} - ${compatText}</li>`;
|
||||
}).join('');
|
||||
|
||||
const pluginCard = document.createElement('div');
|
||||
pluginCard.className = 'card';
|
||||
pluginCard.innerHTML = `
|
||||
<div class="card-header"><h4>${pluginName}</h4></div>
|
||||
<div class="card-content">
|
||||
<p><strong>Source:</strong> ${pluginData.sdist_path}</p>
|
||||
<p><strong>Binaries:</strong></p>
|
||||
<ul>${binariesInfo.length > 0 ? binariesInfo : '<li>No binaries found.</li>'}</ul>
|
||||
</div>
|
||||
`;
|
||||
elements.pluginsList.appendChild(pluginCard);
|
||||
});
|
||||
} else {
|
||||
elements.pluginsList.innerHTML = '<div class="card"><div class="card-content"><p>No plugins found in this bundle.</p></div></div>';
|
||||
}
|
||||
|
||||
// --- Validation Tab ---
|
||||
const validationIssues = validation.errors.concat(validation.warnings);
|
||||
if (validationIssues.length > 0) {
|
||||
elements.validationResults.textContent = validationIssues.join('\n');
|
||||
elements.validationTabLink.classList.remove('hidden');
|
||||
} else {
|
||||
elements.validationResults.textContent = 'Bundle is valid.';
|
||||
elements.validationTabLink.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Reset to overview tab by default
|
||||
domManager.switchTab('overview-tab');
|
||||
}
|
||||
|
||||
// Helper function that calls ui-components
|
||||
function updateSaveButtonVisibility() {
|
||||
if (uiComponents && uiComponents.updateSaveButtonVisibility) {
|
||||
uiComponents.updateSaveButtonVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dependencies (called when module is loaded)
|
||||
function initializeDependencies(deps) {
|
||||
stateManager = deps.stateManager;
|
||||
domManager = deps.domManager;
|
||||
uiComponents = deps.uiComponents;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDependencies,
|
||||
handleOpenBundle,
|
||||
handleSignBundle,
|
||||
handleValidateBundle,
|
||||
handleClearBundle,
|
||||
handleFillBundle,
|
||||
reloadCurrentBundle,
|
||||
displayBundleInfo
|
||||
};
|
||||
203
electron/renderer/dom-manager.js
Normal file
203
electron/renderer/dom-manager.js
Normal file
@@ -0,0 +1,203 @@
|
||||
// DOM management module for the 4DSTAR Bundle Manager
|
||||
// Extracted from renderer.js to centralize DOM element handling and view management
|
||||
|
||||
// --- DOM ELEMENTS (will be initialized in initializeDOMElements) ---
|
||||
let welcomeScreen, bundleView, createBundleForm;
|
||||
let openBundleBtn, createBundleBtn;
|
||||
let signBundleBtn, validateBundleBtn, clearBundleBtn, saveMetadataBtn;
|
||||
let saveOptionsModal, overwriteBundleBtn, saveAsNewBtn;
|
||||
let signatureWarningModal, signatureWarningCancel, signatureWarningContinue;
|
||||
let fillTabLink, loadFillableTargetsBtn, fillLoading, fillPluginsTables, fillNoTargets, fillTargetsContent;
|
||||
let selectAllTargetsBtn, deselectAllTargetsBtn, startFillProcessBtn, fillProgressContainer, fillProgressContent;
|
||||
let bundleTitle, manifestDetails;
|
||||
|
||||
// Static DOM elements (can be accessed immediately)
|
||||
const pluginsList = document.getElementById('plugins-list');
|
||||
const validationResults = document.getElementById('validation-results');
|
||||
|
||||
// Tabs
|
||||
const tabLinks = document.querySelectorAll('.tab-link');
|
||||
const tabPanes = document.querySelectorAll('.tab-pane');
|
||||
const validationTabLink = document.querySelector('button[data-tab="validation-tab"]');
|
||||
|
||||
// Modal
|
||||
const modal = document.getElementById('modal');
|
||||
const modalTitle = document.getElementById('modal-title');
|
||||
const modalMessage = document.getElementById('modal-message');
|
||||
const modalCloseBtn = document.getElementById('modal-close-btn');
|
||||
|
||||
// Spinner
|
||||
const spinner = document.getElementById('spinner');
|
||||
|
||||
// Fill Modal elements
|
||||
const fillModal = document.getElementById('fill-modal');
|
||||
const closeFillModalButton = document.querySelector('.close-fill-modal-button');
|
||||
const fillModalTitle = document.getElementById('fill-modal-title');
|
||||
const fillModalBody = document.getElementById('fill-modal-body');
|
||||
const fillTargetsList = document.getElementById('fill-targets-list');
|
||||
const startFillButton = document.getElementById('start-fill-button');
|
||||
const fillProgressView = document.getElementById('fill-progress-view');
|
||||
const fillProgressList = document.getElementById('fill-progress-list');
|
||||
|
||||
function initializeDOMElements() {
|
||||
// Views
|
||||
welcomeScreen = document.getElementById('welcome-screen');
|
||||
bundleView = document.getElementById('bundle-view');
|
||||
createBundleForm = document.getElementById('create-bundle-form');
|
||||
|
||||
// Sidebar buttons
|
||||
openBundleBtn = document.getElementById('open-bundle-btn');
|
||||
createBundleBtn = document.getElementById('create-bundle-btn');
|
||||
|
||||
// Bundle action buttons
|
||||
signBundleBtn = document.getElementById('sign-bundle-btn');
|
||||
validateBundleBtn = document.getElementById('validate-bundle-btn');
|
||||
clearBundleBtn = document.getElementById('clear-bundle-btn');
|
||||
saveMetadataBtn = document.getElementById('save-metadata-btn');
|
||||
|
||||
// Save options modal elements
|
||||
saveOptionsModal = document.getElementById('save-options-modal');
|
||||
overwriteBundleBtn = document.getElementById('overwrite-bundle-btn');
|
||||
saveAsNewBtn = document.getElementById('save-as-new-btn');
|
||||
|
||||
// Signature warning modal elements
|
||||
signatureWarningModal = document.getElementById('signature-warning-modal');
|
||||
signatureWarningCancel = document.getElementById('signature-warning-cancel');
|
||||
signatureWarningContinue = document.getElementById('signature-warning-continue');
|
||||
|
||||
// Fill tab elements
|
||||
fillTabLink = document.getElementById('fill-tab-link');
|
||||
loadFillableTargetsBtn = document.getElementById('load-fillable-targets-btn');
|
||||
fillLoading = document.getElementById('fill-loading');
|
||||
fillPluginsTables = document.getElementById('fill-plugins-tables');
|
||||
fillNoTargets = document.getElementById('fill-no-targets');
|
||||
fillTargetsContent = document.getElementById('fill-targets-content');
|
||||
selectAllTargetsBtn = document.getElementById('select-all-targets');
|
||||
deselectAllTargetsBtn = document.getElementById('deselect-all-targets');
|
||||
startFillProcessBtn = document.getElementById('start-fill-process');
|
||||
fillProgressContainer = document.getElementById('fill-progress-container');
|
||||
fillProgressContent = document.getElementById('fill-progress-content');
|
||||
|
||||
// Bundle display
|
||||
bundleTitle = document.getElementById('bundle-title');
|
||||
manifestDetails = document.getElementById('manifest-details');
|
||||
}
|
||||
|
||||
// --- VIEW AND UI LOGIC ---
|
||||
function showView(viewId) {
|
||||
// Hide main content views
|
||||
[welcomeScreen, bundleView, createBundleForm].forEach(view => {
|
||||
view.classList.toggle('hidden', view.id !== viewId);
|
||||
});
|
||||
|
||||
// Also hide all category home screens when showing main content
|
||||
const categoryHomeScreens = [
|
||||
'libplugin-home', 'opat-home', 'libconstants-home', 'serif-home'
|
||||
];
|
||||
|
||||
categoryHomeScreens.forEach(screenId => {
|
||||
const screen = document.getElementById(screenId);
|
||||
if (screen) {
|
||||
screen.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Show the appropriate category view if we're showing bundle-view or other content
|
||||
if (viewId === 'bundle-view') {
|
||||
const libpluginView = document.getElementById('libplugin-view');
|
||||
if (libpluginView) {
|
||||
libpluginView.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(tabId) {
|
||||
tabPanes.forEach(pane => {
|
||||
pane.classList.toggle('active', pane.id === tabId);
|
||||
});
|
||||
tabLinks.forEach(link => {
|
||||
link.classList.toggle('active', link.dataset.tab === tabId);
|
||||
});
|
||||
}
|
||||
|
||||
function showSpinner() {
|
||||
spinner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideSpinner() {
|
||||
spinner.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showModal(title, message, type = 'info') {
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.innerHTML = message; // Use innerHTML to allow for formatted messages
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Export DOM management functions and elements
|
||||
module.exports = {
|
||||
// Initialization
|
||||
initializeDOMElements,
|
||||
|
||||
// View management
|
||||
showView,
|
||||
switchTab,
|
||||
showSpinner,
|
||||
hideSpinner,
|
||||
showModal,
|
||||
hideModal,
|
||||
|
||||
// DOM element getters (for other modules to access)
|
||||
getElements: () => ({
|
||||
welcomeScreen,
|
||||
bundleView,
|
||||
createBundleForm,
|
||||
openBundleBtn,
|
||||
createBundleBtn,
|
||||
signBundleBtn,
|
||||
validateBundleBtn,
|
||||
clearBundleBtn,
|
||||
saveMetadataBtn,
|
||||
saveOptionsModal,
|
||||
overwriteBundleBtn,
|
||||
saveAsNewBtn,
|
||||
signatureWarningModal,
|
||||
signatureWarningCancel,
|
||||
signatureWarningContinue,
|
||||
fillTabLink,
|
||||
loadFillableTargetsBtn,
|
||||
fillLoading,
|
||||
fillPluginsTables,
|
||||
fillNoTargets,
|
||||
fillTargetsContent,
|
||||
selectAllTargetsBtn,
|
||||
deselectAllTargetsBtn,
|
||||
startFillProcessBtn,
|
||||
fillProgressContainer,
|
||||
fillProgressContent,
|
||||
bundleTitle,
|
||||
manifestDetails,
|
||||
pluginsList,
|
||||
validationResults,
|
||||
tabLinks,
|
||||
tabPanes,
|
||||
validationTabLink,
|
||||
modal,
|
||||
modalTitle,
|
||||
modalMessage,
|
||||
modalCloseBtn,
|
||||
spinner,
|
||||
fillModal,
|
||||
closeFillModalButton,
|
||||
fillModalTitle,
|
||||
fillModalBody,
|
||||
fillTargetsList,
|
||||
startFillButton,
|
||||
fillProgressView,
|
||||
fillProgressList
|
||||
})
|
||||
};
|
||||
339
electron/renderer/event-handlers.js
Normal file
339
electron/renderer/event-handlers.js
Normal file
@@ -0,0 +1,339 @@
|
||||
// Event handlers module for the 4DSTAR Bundle Manager
|
||||
// Extracted from renderer.js to centralize event listener setup and management
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
// Import dependencies (these will be injected when integrated)
|
||||
let stateManager, domManager, bundleOperations, fillWorkflow, uiComponents;
|
||||
|
||||
// --- EVENT LISTENERS SETUP ---
|
||||
function setupEventListeners() {
|
||||
const elements = domManager.getElements();
|
||||
|
||||
// Theme updates
|
||||
ipcRenderer.on('theme-updated', (event, { shouldUseDarkColors }) => {
|
||||
document.body.classList.toggle('dark-mode', shouldUseDarkColors);
|
||||
});
|
||||
|
||||
// Sidebar navigation
|
||||
elements.openBundleBtn.addEventListener('click', bundleOperations.handleOpenBundle);
|
||||
elements.createBundleBtn.addEventListener('click', () => {
|
||||
// TODO: Replace with modal
|
||||
domManager.showView('create-bundle-form');
|
||||
domManager.showModal('Not Implemented', 'The create bundle form will be moved to a modal dialog.');
|
||||
});
|
||||
|
||||
// Tab navigation
|
||||
elements.tabLinks.forEach(link => {
|
||||
link.addEventListener('click', () => domManager.switchTab(link.dataset.tab));
|
||||
});
|
||||
|
||||
// Modal close button
|
||||
elements.modalCloseBtn.addEventListener('click', domManager.hideModal);
|
||||
|
||||
// Bundle actions
|
||||
elements.signBundleBtn.addEventListener('click', () => {
|
||||
checkSignatureAndWarn(bundleOperations.handleSignBundle, 'signing');
|
||||
});
|
||||
elements.validateBundleBtn.addEventListener('click', bundleOperations.handleValidateBundle);
|
||||
elements.clearBundleBtn.addEventListener('click', () => {
|
||||
checkSignatureAndWarn(bundleOperations.handleClearBundle, 'clearing binaries');
|
||||
});
|
||||
elements.saveMetadataBtn.addEventListener('click', uiComponents.showSaveOptionsModal);
|
||||
elements.overwriteBundleBtn.addEventListener('click', () => handleSaveMetadata(false));
|
||||
elements.saveAsNewBtn.addEventListener('click', () => handleSaveMetadata(true));
|
||||
|
||||
// Signature warning modal event listeners
|
||||
elements.signatureWarningCancel.addEventListener('click', () => {
|
||||
elements.signatureWarningModal.classList.add('hidden');
|
||||
stateManager.clearPendingOperation();
|
||||
});
|
||||
|
||||
elements.signatureWarningContinue.addEventListener('click', () => {
|
||||
elements.signatureWarningModal.classList.add('hidden');
|
||||
const pendingOperation = stateManager.getPendingOperation();
|
||||
if (pendingOperation) {
|
||||
pendingOperation();
|
||||
stateManager.clearPendingOperation();
|
||||
}
|
||||
});
|
||||
|
||||
// Load fillable targets button
|
||||
elements.loadFillableTargetsBtn.addEventListener('click', async () => {
|
||||
await fillWorkflow.loadFillableTargets();
|
||||
});
|
||||
|
||||
// Category navigation
|
||||
setupCategoryNavigation();
|
||||
|
||||
// Info modal setup
|
||||
setupInfoModal();
|
||||
}
|
||||
|
||||
// Check if bundle is signed and show warning before bundle-modifying operations
|
||||
function checkSignatureAndWarn(operation, operationName = 'operation') {
|
||||
const currentBundle = stateManager.getCurrentBundle();
|
||||
const elements = domManager.getElements();
|
||||
|
||||
if (currentBundle &&
|
||||
currentBundle.report &&
|
||||
currentBundle.report.signature &&
|
||||
currentBundle.report.signature.status &&
|
||||
['TRUSTED', 'UNTRUSTED'].includes(currentBundle.report.signature.status)) {
|
||||
|
||||
// Bundle is signed, show warning
|
||||
stateManager.setPendingOperation(operation);
|
||||
elements.signatureWarningModal.classList.remove('hidden');
|
||||
} else {
|
||||
// Bundle is not signed, proceed directly
|
||||
operation();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup category navigation
|
||||
function setupCategoryNavigation() {
|
||||
const categoryItems = document.querySelectorAll('.category-item');
|
||||
const secondarySidebar = document.getElementById('secondary-sidebar');
|
||||
|
||||
categoryItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const category = item.dataset.category;
|
||||
|
||||
// Update active states
|
||||
categoryItems.forEach(cat => cat.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// Show/hide secondary sidebar based on category
|
||||
if (category === 'home') {
|
||||
if (secondarySidebar) {
|
||||
secondarySidebar.style.display = 'none';
|
||||
}
|
||||
showCategoryHomeScreen('home');
|
||||
} else {
|
||||
if (secondarySidebar) {
|
||||
secondarySidebar.style.display = 'block';
|
||||
}
|
||||
|
||||
// Show appropriate sidebar content
|
||||
const sidebarContents = document.querySelectorAll('.sidebar-content');
|
||||
sidebarContents.forEach(content => {
|
||||
if (content.dataset.category === category) {
|
||||
content.classList.remove('hidden');
|
||||
} else {
|
||||
content.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Show category home screen
|
||||
showCategoryHomeScreen(category);
|
||||
}
|
||||
|
||||
// Update welcome screen
|
||||
updateWelcomeScreen(category);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update welcome screen based on selected category
|
||||
function updateWelcomeScreen(category) {
|
||||
const welcomeTitles = {
|
||||
'home': 'Welcome to 4DSTAR',
|
||||
'libplugin': 'Welcome to libplugin',
|
||||
'libconstants': 'Welcome to libconstants',
|
||||
'opat': 'Welcome to OPAT Core',
|
||||
'serif': 'Welcome to SERiF Libraries'
|
||||
};
|
||||
|
||||
const welcomeMessages = {
|
||||
'home': 'Select a category from the sidebar to get started.',
|
||||
'libplugin': 'Bundle management tools for 4DSTAR plugins.',
|
||||
'libconstants': 'Constants tools coming soon...',
|
||||
'opat': 'OPAT tools coming soon...',
|
||||
'serif': 'SERiF tools coming soon...'
|
||||
};
|
||||
|
||||
const welcomeTitle = document.querySelector('.welcome-title');
|
||||
const welcomeMessage = document.querySelector('.welcome-message');
|
||||
|
||||
if (welcomeTitle) welcomeTitle.textContent = welcomeTitles[category] || welcomeTitles['home'];
|
||||
if (welcomeMessage) welcomeMessage.textContent = welcomeMessages[category] || welcomeMessages['home'];
|
||||
}
|
||||
|
||||
// Show appropriate home screen based on selected category
|
||||
function showCategoryHomeScreen(category) {
|
||||
const views = [
|
||||
'welcome-screen', 'libplugin-home', 'opat-home',
|
||||
'libconstants-home', 'serif-home', 'opat-view', 'libplugin-view',
|
||||
'bundle-view', 'create-bundle-form'
|
||||
];
|
||||
|
||||
// Hide all views
|
||||
views.forEach(viewId => {
|
||||
const view = document.getElementById(viewId);
|
||||
if (view) view.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Show appropriate view
|
||||
const viewMap = {
|
||||
'home': 'welcome-screen',
|
||||
'libplugin': 'libplugin-home',
|
||||
'opat': 'opat-home',
|
||||
'libconstants': 'libconstants-home',
|
||||
'serif': 'serif-home'
|
||||
};
|
||||
|
||||
const viewId = viewMap[category] || 'welcome-screen';
|
||||
const view = document.getElementById(viewId);
|
||||
if (view) view.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Setup info modal
|
||||
function setupInfoModal() {
|
||||
const infoBtn = document.getElementById('info-btn');
|
||||
const infoModal = document.getElementById('info-modal');
|
||||
const closeInfoModalBtn = document.getElementById('close-info-modal');
|
||||
const infoTabLinks = document.querySelectorAll('.info-tab-link');
|
||||
const infoTabPanes = document.querySelectorAll('.info-tab-pane');
|
||||
|
||||
if (infoBtn) {
|
||||
infoBtn.addEventListener('click', () => {
|
||||
if (infoModal) infoModal.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (closeInfoModalBtn) {
|
||||
closeInfoModalBtn.addEventListener('click', hideInfoModal);
|
||||
}
|
||||
|
||||
// Info tab navigation
|
||||
infoTabLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const targetTab = link.dataset.tab;
|
||||
|
||||
// Update active states
|
||||
infoTabLinks.forEach(l => l.classList.remove('active'));
|
||||
infoTabPanes.forEach(p => p.classList.remove('active'));
|
||||
|
||||
link.classList.add('active');
|
||||
const targetPane = document.getElementById(targetTab);
|
||||
if (targetPane) targetPane.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// External link handling
|
||||
const githubLink = document.getElementById('github-link');
|
||||
if (githubLink) {
|
||||
githubLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
ipcRenderer.invoke('open-external-url', 'https://github.com/tboudreaux/4DSTAR');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Hide info modal - make it globally accessible
|
||||
function hideInfoModal() {
|
||||
const infoModal = document.getElementById('info-modal');
|
||||
if (infoModal) infoModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Handle save metadata with option for save as new
|
||||
async function handleSaveMetadata(saveAsNew = false) {
|
||||
const currentBundlePath = stateManager.getCurrentBundlePath();
|
||||
if (!currentBundlePath) return;
|
||||
|
||||
const elements = domManager.getElements();
|
||||
|
||||
// Collect updated metadata from form fields
|
||||
const inputs = document.querySelectorAll('.field-input');
|
||||
const updatedManifest = {};
|
||||
|
||||
inputs.forEach(input => {
|
||||
const fieldName = input.dataset.field;
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
updatedManifest[fieldName] = value;
|
||||
}
|
||||
});
|
||||
|
||||
let targetPath = currentBundlePath;
|
||||
|
||||
if (saveAsNew) {
|
||||
// Show save dialog for new bundle
|
||||
const saveResult = await ipcRenderer.invoke('show-save-dialog', {
|
||||
filters: [{ name: 'Fbundle Archives', extensions: ['fbundle'] }],
|
||||
defaultPath: currentBundlePath.replace(/\.fbundle$/, '_modified.fbundle')
|
||||
});
|
||||
|
||||
if (saveResult.canceled || !saveResult.filePath) {
|
||||
uiComponents.hideSaveOptionsModal();
|
||||
return;
|
||||
}
|
||||
|
||||
targetPath = saveResult.filePath;
|
||||
|
||||
// Copy original bundle to new location first
|
||||
try {
|
||||
const copyResult = await ipcRenderer.invoke('copy-file', {
|
||||
source: currentBundlePath,
|
||||
destination: targetPath
|
||||
});
|
||||
|
||||
if (!copyResult.success) {
|
||||
domManager.showModal('Copy Error', `Failed to copy bundle: ${copyResult.error}`);
|
||||
uiComponents.hideSaveOptionsModal();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
domManager.showModal('Copy Error', `Failed to copy bundle: ${error.message}`);
|
||||
uiComponents.hideSaveOptionsModal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save metadata to target bundle
|
||||
domManager.showSpinner();
|
||||
const result = await ipcRenderer.invoke('edit-bundle', {
|
||||
bundlePath: targetPath,
|
||||
updatedManifest: updatedManifest
|
||||
});
|
||||
domManager.hideSpinner();
|
||||
|
||||
if (result.success) {
|
||||
domManager.showModal('Success', 'Bundle metadata saved successfully. Reloading...');
|
||||
|
||||
// Update current bundle path if we saved as new
|
||||
if (saveAsNew) {
|
||||
stateManager.setBundleState(stateManager.getCurrentBundle(), targetPath);
|
||||
}
|
||||
|
||||
await bundleOperations.reloadCurrentBundle();
|
||||
uiComponents.hideSaveOptionsModal();
|
||||
domManager.hideModal();
|
||||
} else {
|
||||
domManager.showModal('Save Error', `Failed to save metadata: ${result.error}`);
|
||||
}
|
||||
|
||||
uiComponents.hideSaveOptionsModal();
|
||||
}
|
||||
|
||||
// Initialize dependencies (called when module is loaded)
|
||||
function initializeDependencies(deps) {
|
||||
stateManager = deps.stateManager;
|
||||
domManager = deps.domManager;
|
||||
bundleOperations = deps.bundleOperations;
|
||||
fillWorkflow = deps.fillWorkflow;
|
||||
uiComponents = deps.uiComponents;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDependencies,
|
||||
setupEventListeners,
|
||||
checkSignatureAndWarn,
|
||||
setupCategoryNavigation,
|
||||
updateWelcomeScreen,
|
||||
showCategoryHomeScreen,
|
||||
setupInfoModal,
|
||||
hideInfoModal,
|
||||
handleSaveMetadata
|
||||
};
|
||||
317
electron/renderer/fill-workflow.js
Normal file
317
electron/renderer/fill-workflow.js
Normal file
@@ -0,0 +1,317 @@
|
||||
// Fill workflow module for the 4DSTAR Bundle Manager
|
||||
// Extracted from renderer.js to centralize fill process management
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
// Import dependencies (these will be injected when integrated)
|
||||
let stateManager, domManager;
|
||||
|
||||
// Load fillable targets for the Fill tab
|
||||
async function loadFillableTargets() {
|
||||
const currentBundlePath = stateManager.getCurrentBundlePath();
|
||||
const elements = domManager.getElements();
|
||||
|
||||
console.log('loadFillableTargets called, currentBundlePath:', currentBundlePath);
|
||||
|
||||
// Check if required DOM elements exist
|
||||
if (!elements.fillNoTargets || !elements.fillTargetsContent || !elements.fillLoading) {
|
||||
console.error('Fill tab DOM elements not found');
|
||||
domManager.showModal('Error', 'Fill tab interface not properly initialized.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentBundlePath) {
|
||||
console.log('No bundle path, showing no targets message');
|
||||
hideAllFillStates();
|
||||
elements.fillNoTargets.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
hideAllFillStates();
|
||||
elements.fillLoading.classList.remove('hidden');
|
||||
elements.loadFillableTargetsBtn.disabled = true;
|
||||
|
||||
console.log('Requesting fillable targets for:', currentBundlePath);
|
||||
const result = await ipcRenderer.invoke('get-fillable-targets', currentBundlePath);
|
||||
console.log('Fillable targets result:', result);
|
||||
|
||||
if (result && result.success && result.data) {
|
||||
const hasTargets = Object.values(result.data).some(targets => targets.length > 0);
|
||||
|
||||
if (hasTargets) {
|
||||
console.log('Found fillable targets, populating table');
|
||||
hideAllFillStates();
|
||||
elements.fillTargetsContent.classList.remove('hidden');
|
||||
populateFillTargetsTable(result.data);
|
||||
} else {
|
||||
console.log('No fillable targets found');
|
||||
hideAllFillStates();
|
||||
elements.fillNoTargets.classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to get fillable targets:', result.error);
|
||||
hideAllFillStates();
|
||||
elements.fillNoTargets.classList.remove('hidden');
|
||||
domManager.showModal('Error', `Failed to load fillable targets: ${result.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Exception in loadFillableTargets:', error);
|
||||
hideAllFillStates();
|
||||
elements.fillNoTargets.classList.remove('hidden');
|
||||
domManager.showModal('Error', `Failed to load fillable targets: ${error.message}`);
|
||||
} finally {
|
||||
elements.loadFillableTargetsBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to hide all fill tab states
|
||||
function hideAllFillStates() {
|
||||
const elements = domManager.getElements();
|
||||
elements.fillLoading.classList.add('hidden');
|
||||
elements.fillNoTargets.classList.add('hidden');
|
||||
elements.fillTargetsContent.classList.add('hidden');
|
||||
elements.fillProgressContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Create modern table-based interface for fillable targets
|
||||
function populateFillTargetsTable(plugins) {
|
||||
const elements = domManager.getElements();
|
||||
elements.fillPluginsTables.innerHTML = '';
|
||||
|
||||
for (const [pluginName, targets] of Object.entries(plugins)) {
|
||||
if (targets.length > 0) {
|
||||
// Create plugin table container
|
||||
const pluginTable = document.createElement('div');
|
||||
pluginTable.className = 'fill-plugin-table';
|
||||
|
||||
// Plugin header
|
||||
const pluginHeader = document.createElement('div');
|
||||
pluginHeader.className = 'fill-plugin-header';
|
||||
pluginHeader.textContent = `${pluginName} (${targets.length} target${targets.length > 1 ? 's' : ''})`;
|
||||
pluginTable.appendChild(pluginHeader);
|
||||
|
||||
// Create table
|
||||
const table = document.createElement('table');
|
||||
table.className = 'fill-targets-table';
|
||||
|
||||
// Table header
|
||||
const thead = document.createElement('thead');
|
||||
thead.innerHTML = `
|
||||
<tr>
|
||||
<th style="width: 50px;">
|
||||
<input type="checkbox" class="plugin-select-all" data-plugin="${pluginName}" checked>
|
||||
</th>
|
||||
<th>Target Platform</th>
|
||||
<th>Architecture</th>
|
||||
<th>Type</th>
|
||||
<th>Compiler</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
|
||||
// Table body
|
||||
const tbody = document.createElement('tbody');
|
||||
targets.forEach(target => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<input type="checkbox" class="fill-target-checkbox"
|
||||
data-plugin="${pluginName}"
|
||||
data-target='${JSON.stringify(target)}'
|
||||
checked>
|
||||
</td>
|
||||
<td><strong>${target.triplet}</strong></td>
|
||||
<td>${target.arch}</td>
|
||||
<td><span class="target-type ${target.type}">${target.type}</span></td>
|
||||
<td>${target.type === 'docker' ? 'GCC' : (target.details?.compiler || 'N/A')} ${target.details?.compiler_version || ''}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
|
||||
pluginTable.appendChild(table);
|
||||
elements.fillPluginsTables.appendChild(pluginTable);
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listeners for select all functionality
|
||||
setupFillTargetEventListeners();
|
||||
}
|
||||
|
||||
// Setup event listeners for Fill tab functionality
|
||||
function setupFillTargetEventListeners() {
|
||||
const elements = domManager.getElements();
|
||||
|
||||
// Plugin-level select all checkboxes
|
||||
document.querySelectorAll('.plugin-select-all').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const pluginName = e.target.dataset.plugin;
|
||||
const pluginCheckboxes = document.querySelectorAll(`.fill-target-checkbox[data-plugin="${pluginName}"]`);
|
||||
pluginCheckboxes.forEach(cb => cb.checked = e.target.checked);
|
||||
});
|
||||
});
|
||||
|
||||
// Global select/deselect all buttons
|
||||
elements.selectAllTargetsBtn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.fill-target-checkbox, .plugin-select-all').forEach(cb => cb.checked = true);
|
||||
});
|
||||
|
||||
elements.deselectAllTargetsBtn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.fill-target-checkbox, .plugin-select-all').forEach(cb => cb.checked = false);
|
||||
});
|
||||
|
||||
// Start fill process button
|
||||
elements.startFillProcessBtn.addEventListener('click', async () => {
|
||||
const selectedTargetsByPlugin = {};
|
||||
|
||||
document.querySelectorAll('.fill-target-checkbox:checked').forEach(checkbox => {
|
||||
try {
|
||||
const target = JSON.parse(checkbox.dataset.target);
|
||||
const pluginName = checkbox.dataset.plugin;
|
||||
|
||||
if (!selectedTargetsByPlugin[pluginName]) {
|
||||
selectedTargetsByPlugin[pluginName] = [];
|
||||
}
|
||||
selectedTargetsByPlugin[pluginName].push(target);
|
||||
} catch (error) {
|
||||
console.error('Error parsing target data:', error);
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(selectedTargetsByPlugin).length === 0) {
|
||||
domManager.showModal('No Targets Selected', 'Please select at least one target to build.');
|
||||
return;
|
||||
}
|
||||
|
||||
await startFillProcess(selectedTargetsByPlugin);
|
||||
});
|
||||
}
|
||||
|
||||
// Create progress display for fill process
|
||||
function populateFillProgress(selectedTargetsByPlugin) {
|
||||
const elements = domManager.getElements();
|
||||
elements.fillProgressContent.innerHTML = '';
|
||||
|
||||
Object.entries(selectedTargetsByPlugin).forEach(([pluginName, targets]) => {
|
||||
targets.forEach(target => {
|
||||
const progressItem = document.createElement('div');
|
||||
progressItem.className = 'fill-progress-item';
|
||||
progressItem.id = `progress-${target.triplet}`;
|
||||
progressItem.innerHTML = `
|
||||
<div class="progress-target">${pluginName}: ${target.triplet}</div>
|
||||
<div class="progress-status">Waiting...</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
`;
|
||||
elements.fillProgressContent.appendChild(progressItem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Start the fill process
|
||||
async function startFillProcess(selectedTargetsByPlugin) {
|
||||
const currentBundlePath = stateManager.getCurrentBundlePath();
|
||||
const elements = domManager.getElements();
|
||||
|
||||
if (!currentBundlePath) {
|
||||
domManager.showModal('Error', 'No bundle is currently open.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show progress view
|
||||
hideAllFillStates();
|
||||
elements.fillProgressContainer.classList.remove('hidden');
|
||||
populateFillProgress(selectedTargetsByPlugin);
|
||||
|
||||
// Set up progress listener
|
||||
const progressHandler = (event, progressData) => {
|
||||
updateFillProgress(progressData);
|
||||
};
|
||||
|
||||
ipcRenderer.on('fill-bundle-progress', progressHandler);
|
||||
|
||||
// Start the fill process
|
||||
const result = await ipcRenderer.invoke('fill-bundle', {
|
||||
bundlePath: currentBundlePath,
|
||||
targetsToBuild: selectedTargetsByPlugin
|
||||
});
|
||||
|
||||
// Clean up progress listener
|
||||
ipcRenderer.removeListener('fill-bundle-progress', progressHandler);
|
||||
|
||||
if (result.success) {
|
||||
domManager.showModal('Fill Complete', 'Bundle fill process completed successfully. Reloading bundle...');
|
||||
// Reload the bundle to show updated information
|
||||
// Note: This will be handled by the parent module that has access to bundleOperations
|
||||
if (window.bundleOperations) {
|
||||
await window.bundleOperations.reloadCurrentBundle();
|
||||
}
|
||||
domManager.hideModal();
|
||||
} else {
|
||||
domManager.showModal('Fill Error', `Fill process failed: ${result.error}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fill process error:', error);
|
||||
domManager.showModal('Fill Error', `Fill process failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress display during fill process
|
||||
function updateFillProgress(progressData) {
|
||||
console.log('Fill progress update:', progressData);
|
||||
|
||||
if (progressData.target) {
|
||||
const progressItem = document.getElementById(`progress-${progressData.target}`);
|
||||
if (progressItem) {
|
||||
const statusElement = progressItem.querySelector('.progress-status');
|
||||
const progressBar = progressItem.querySelector('.progress-fill');
|
||||
|
||||
if (progressData.status) {
|
||||
statusElement.textContent = progressData.status;
|
||||
|
||||
// Update progress bar based on status
|
||||
let percentage = 0;
|
||||
switch (progressData.status) {
|
||||
case 'Building':
|
||||
percentage = 50;
|
||||
progressItem.className = 'fill-progress-item building';
|
||||
break;
|
||||
case 'Success':
|
||||
percentage = 100;
|
||||
progressItem.className = 'fill-progress-item success';
|
||||
break;
|
||||
case 'Failed':
|
||||
percentage = 100;
|
||||
progressItem.className = 'fill-progress-item failed';
|
||||
break;
|
||||
}
|
||||
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dependencies (called when module is loaded)
|
||||
function initializeDependencies(deps) {
|
||||
stateManager = deps.stateManager;
|
||||
domManager = deps.domManager;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDependencies,
|
||||
loadFillableTargets,
|
||||
hideAllFillStates,
|
||||
populateFillTargetsTable,
|
||||
setupFillTargetEventListeners,
|
||||
populateFillProgress,
|
||||
startFillProcess,
|
||||
updateFillProgress
|
||||
};
|
||||
355
electron/renderer/opat-handler.js
Normal file
355
electron/renderer/opat-handler.js
Normal file
@@ -0,0 +1,355 @@
|
||||
// OPAT file handler module for the 4DSTAR Bundle Manager
|
||||
// Extracted from renderer.js to centralize OPAT file parsing and display logic
|
||||
|
||||
// Import dependencies (these will be injected when integrated)
|
||||
let stateManager, domManager;
|
||||
|
||||
// OPAT File Inspector variables
|
||||
let opatFileInput, opatBrowseBtn, opatView, opatCloseBtn;
|
||||
let opatHeaderInfo, opatAllTagsList, opatIndexSelector, opatTablesDisplay, opatTableDataContent;
|
||||
|
||||
// Initialize OPAT UI elements
|
||||
function initializeOPATElements() {
|
||||
opatFileInput = document.getElementById('opat-file-input');
|
||||
opatBrowseBtn = document.getElementById('opat-browse-btn');
|
||||
opatView = document.getElementById('opat-view');
|
||||
opatCloseBtn = document.getElementById('opat-close-btn');
|
||||
opatHeaderInfo = document.getElementById('opat-header-info');
|
||||
opatAllTagsList = document.getElementById('opat-all-tags-list');
|
||||
opatIndexSelector = document.getElementById('opat-index-selector');
|
||||
opatTablesDisplay = document.getElementById('opat-tables-display');
|
||||
opatTableDataContent = document.getElementById('opat-table-data-content');
|
||||
|
||||
// Event listeners
|
||||
opatBrowseBtn.addEventListener('click', () => opatFileInput.click());
|
||||
opatFileInput.addEventListener('change', handleOPATFileSelection);
|
||||
opatIndexSelector.addEventListener('change', handleIndexVectorChange);
|
||||
opatCloseBtn.addEventListener('click', closeOPATFile);
|
||||
|
||||
// Initialize OPAT tab navigation
|
||||
initializeOPATTabs();
|
||||
|
||||
// Add window resize listener to update table heights
|
||||
window.updateTableHeights = function() {
|
||||
const newHeight = Math.max(300, window.innerHeight - 450);
|
||||
|
||||
// Target the main table containers
|
||||
const containers = document.querySelectorAll('.opat-table-container');
|
||||
containers.forEach((container, index) => {
|
||||
container.style.setProperty('height', newHeight + 'px', 'important');
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', window.updateTableHeights);
|
||||
}
|
||||
|
||||
// Initialize OPAT tab navigation
|
||||
function initializeOPATTabs() {
|
||||
const opatTabLinks = document.querySelectorAll('.opat-tab-link');
|
||||
const opatTabPanes = document.querySelectorAll('.opat-tab-pane');
|
||||
|
||||
opatTabLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const targetTab = link.dataset.tab;
|
||||
|
||||
// Update active states
|
||||
opatTabLinks.forEach(l => l.classList.remove('active'));
|
||||
opatTabPanes.forEach(p => p.classList.remove('active'));
|
||||
|
||||
link.classList.add('active');
|
||||
document.getElementById(targetTab).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Reset OPAT viewer state
|
||||
function resetOPATViewerState() {
|
||||
if (opatHeaderInfo) opatHeaderInfo.innerHTML = '';
|
||||
if (opatAllTagsList) opatAllTagsList.innerHTML = '';
|
||||
if (opatIndexSelector) opatIndexSelector.innerHTML = '<option value="">-- Select an index vector --</option>';
|
||||
if (opatTablesDisplay) opatTablesDisplay.innerHTML = '';
|
||||
if (opatTableDataContent) opatTableDataContent.innerHTML = '';
|
||||
}
|
||||
|
||||
// Handle OPAT file selection
|
||||
async function handleOPATFileSelection(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
console.log('Loading OPAT file:', file.name);
|
||||
domManager.showSpinner();
|
||||
|
||||
resetOPATViewerState();
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const currentOPATFile = parseOPAT(arrayBuffer);
|
||||
stateManager.setOPATFile(currentOPATFile);
|
||||
|
||||
displayOPATFileInfo();
|
||||
populateIndexSelector();
|
||||
|
||||
// Show OPAT view and hide other views
|
||||
hideAllViews();
|
||||
opatView.classList.remove('hidden');
|
||||
|
||||
// Update title with filename
|
||||
document.getElementById('opat-title').textContent = `OPAT File Inspector - ${file.name}`;
|
||||
|
||||
domManager.hideSpinner();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error parsing OPAT file:', error);
|
||||
domManager.hideSpinner();
|
||||
domManager.showModal('Error', `Failed to parse OPAT file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Display OPAT file information
|
||||
function displayOPATFileInfo() {
|
||||
const currentOPATFile = stateManager.getOPATFile();
|
||||
if (!currentOPATFile) return;
|
||||
|
||||
const header = currentOPATFile.header;
|
||||
opatHeaderInfo.innerHTML = `
|
||||
<div class="opat-info-section">
|
||||
<h4 class="opat-section-title">Header Information</h4>
|
||||
<div class="info-grid">
|
||||
<p><strong>Magic:</strong> ${header.magic}</p>
|
||||
<p><strong>Version:</strong> ${header.version}</p>
|
||||
<p><strong>Number of Tables:</strong> ${header.numTables}</p>
|
||||
<p><strong>Header Size:</strong> ${header.headerSize} bytes</p>
|
||||
<p><strong>Index Offset:</strong> ${header.indexOffset}</p>
|
||||
<p><strong>Creation Date:</strong> ${header.creationDate}</p>
|
||||
<p><strong>Source Info:</strong> ${header.sourceInfo}</p>
|
||||
<p><strong>Comment:</strong> ${header.comment || 'None'}</p>
|
||||
<p><strong>Number of Indices:</strong> ${header.numIndex}</p>
|
||||
<p><strong>Hash Precision:</strong> ${header.hashPrecision}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Display all unique table tags
|
||||
displayAllTableTags();
|
||||
}
|
||||
|
||||
// Display all table tags
|
||||
function displayAllTableTags() {
|
||||
const currentOPATFile = stateManager.getOPATFile();
|
||||
if (!currentOPATFile) return;
|
||||
|
||||
const allTags = new Set();
|
||||
for (const card of currentOPATFile.cards.values()) {
|
||||
for (const tag of card.tableIndex.keys()) {
|
||||
allTags.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
opatAllTagsList.innerHTML = '';
|
||||
Array.from(allTags).sort().forEach(tag => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = tag;
|
||||
opatAllTagsList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Populate index selector
|
||||
function populateIndexSelector() {
|
||||
const currentOPATFile = stateManager.getOPATFile();
|
||||
if (!currentOPATFile) return;
|
||||
|
||||
opatIndexSelector.innerHTML = '<option value="">-- Select an index vector --</option>';
|
||||
|
||||
for (const [key, entry] of currentOPATFile.cardCatalog.entries()) {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = `[${entry.index.join(', ')}]`;
|
||||
opatIndexSelector.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle index vector change
|
||||
function handleIndexVectorChange() {
|
||||
const selectedKey = opatIndexSelector.value;
|
||||
const currentOPATFile = stateManager.getOPATFile();
|
||||
|
||||
if (!selectedKey || !currentOPATFile) {
|
||||
opatTablesDisplay.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const card = currentOPATFile.cards.get(selectedKey);
|
||||
if (!card) return;
|
||||
|
||||
opatTablesDisplay.innerHTML = '';
|
||||
|
||||
for (const [tag, tableEntry] of card.tableIndex.entries()) {
|
||||
const tableInfo = document.createElement('div');
|
||||
tableInfo.className = 'opat-table-info';
|
||||
tableInfo.innerHTML = `
|
||||
<div class="opat-table-tag">${tag}</div>
|
||||
<div class="opat-table-details">
|
||||
Rows: ${tableEntry.numRows}, Columns: ${tableEntry.numColumns}<br>
|
||||
Row Name: ${tableEntry.rowName}, Column Name: ${tableEntry.columnName}
|
||||
</div>
|
||||
`;
|
||||
|
||||
tableInfo.addEventListener('click', () => {
|
||||
const table = card.tableData.get(tag);
|
||||
displayTableData(table, tag);
|
||||
});
|
||||
|
||||
opatTablesDisplay.appendChild(tableInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Display table data
|
||||
function displayTableData(table, tag, showAll = false) {
|
||||
if (!table) {
|
||||
opatTableDataContent.innerHTML = '<p class="opat-placeholder">Table not found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<div class="opat-table-title"><span class="opat-table-tag-highlight">${tag}</span> Table Data</div>`;
|
||||
html += `<p><strong>Dimensions:</strong> ${table.N_R} rows × ${table.N_C} columns × ${table.m_vsize} values per cell</p>`;
|
||||
|
||||
if (table.N_R > 0 && table.N_C > 0) {
|
||||
if (table.m_vsize === 0 || table.data.length === 0) {
|
||||
html += '<p><strong>Note:</strong> This table has no data values (m_vsize = 0 or empty data array).</p>';
|
||||
html += '<p>The table structure exists but contains no numerical data to display.</p>';
|
||||
} else {
|
||||
// Add show all/show less toggle buttons
|
||||
if (table.N_R > 50) {
|
||||
html += '<div class="table-controls">';
|
||||
if (!showAll) {
|
||||
html += `<button class="show-all-btn" onclick="displayTableData(stateManager.getOPATFile().cards.get('${opatIndexSelector.value}').tableData.get('${tag}'), '${tag}', true)">Show All ${table.N_R} Rows</button>`;
|
||||
} else {
|
||||
html += `<button class="show-less-btn" onclick="displayTableData(stateManager.getOPATFile().cards.get('${opatIndexSelector.value}').tableData.get('${tag}'), '${tag}', false)">Show First 50 Rows</button>`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '<div class="opat-table-container">';
|
||||
html += '<div class="table-scroll-wrapper">';
|
||||
html += '<table class="opat-data-table">';
|
||||
|
||||
// Header row
|
||||
html += '<thead><tr><th class="corner-cell"></th>';
|
||||
for (let c = 0; c < table.N_C; c++) {
|
||||
html += `<th>${table.columnValues[c].toFixed(3)}</th>`;
|
||||
}
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Data rows
|
||||
html += '<tbody>';
|
||||
const rowsToShow = showAll ? table.N_R : Math.min(table.N_R, 50);
|
||||
for (let r = 0; r < rowsToShow; r++) {
|
||||
html += '<tr>';
|
||||
html += `<th class="row-header">${table.rowValues[r].toFixed(3)}</th>`;
|
||||
for (let c = 0; c < table.N_C; c++) {
|
||||
try {
|
||||
const value = table.getValue(r, c, 0); // Get first value in cell
|
||||
html += `<td>${value.toFixed(6)}</td>`;
|
||||
} catch (error) {
|
||||
html += `<td>N/A</td>`;
|
||||
}
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody>';
|
||||
html += '</table>';
|
||||
html += '</div></div>';
|
||||
|
||||
if (table.N_R > 50 && !showAll) {
|
||||
html += `<p><em>Showing first 50 rows of ${table.N_R} total rows.</em></p>`;
|
||||
} else if (showAll && table.N_R > 50) {
|
||||
html += `<p><em>Showing all ${table.N_R} rows.</em></p>`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html += '<p>No data to display.</p>';
|
||||
}
|
||||
|
||||
opatTableDataContent.innerHTML = html;
|
||||
|
||||
// Auto-switch to Data Explorer tab when displaying data
|
||||
const explorerTab = document.querySelector('[data-tab="opat-explorer-tab"]');
|
||||
if (explorerTab) {
|
||||
explorerTab.click();
|
||||
}
|
||||
|
||||
// Update table heights after table is rendered
|
||||
setTimeout(() => {
|
||||
if (window.updateTableHeights) {
|
||||
window.updateTableHeights();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Close OPAT file
|
||||
function closeOPATFile() {
|
||||
stateManager.clearOPATFile();
|
||||
resetOPATViewerState();
|
||||
|
||||
// Reset file input
|
||||
if (opatFileInput) {
|
||||
opatFileInput.value = '';
|
||||
}
|
||||
|
||||
// Hide OPAT view and show appropriate home screen
|
||||
hideAllViews();
|
||||
showCategoryHomeScreen('opat');
|
||||
}
|
||||
|
||||
// Helper function to hide all views
|
||||
function hideAllViews() {
|
||||
const views = [
|
||||
'welcome-screen', 'libplugin-home', 'opat-home',
|
||||
'libconstants-home', 'serif-home', 'opat-view', 'libplugin-view'
|
||||
];
|
||||
|
||||
views.forEach(viewId => {
|
||||
const view = document.getElementById(viewId);
|
||||
if (view) view.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Show appropriate home screen based on selected category
|
||||
function showCategoryHomeScreen(category) {
|
||||
hideAllViews();
|
||||
|
||||
const viewMap = {
|
||||
'home': 'welcome-screen',
|
||||
'libplugin': 'libplugin-home',
|
||||
'opat': 'opat-home',
|
||||
'libconstants': 'libconstants-home',
|
||||
'serif': 'serif-home'
|
||||
};
|
||||
|
||||
const viewId = viewMap[category] || 'welcome-screen';
|
||||
const view = document.getElementById(viewId);
|
||||
if (view) view.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Initialize dependencies (called when module is loaded)
|
||||
function initializeDependencies(deps) {
|
||||
stateManager = deps.stateManager;
|
||||
domManager = deps.domManager;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDependencies,
|
||||
initializeOPATElements,
|
||||
initializeOPATTabs,
|
||||
resetOPATViewerState,
|
||||
handleOPATFileSelection,
|
||||
displayOPATFileInfo,
|
||||
displayAllTableTags,
|
||||
populateIndexSelector,
|
||||
handleIndexVectorChange,
|
||||
displayTableData,
|
||||
closeOPATFile,
|
||||
hideAllViews,
|
||||
showCategoryHomeScreen
|
||||
};
|
||||
94
electron/renderer/state-manager.js
Normal file
94
electron/renderer/state-manager.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// State management module for the 4DSTAR Bundle Manager
|
||||
// Extracted from renderer.js to centralize state handling
|
||||
|
||||
// --- GLOBAL STATE ---
|
||||
let currentBundle = null;
|
||||
let currentBundlePath = null;
|
||||
let hasUnsavedChanges = false;
|
||||
let originalMetadata = {};
|
||||
let pendingOperation = null; // Store the operation to execute after warning confirmation
|
||||
|
||||
// Current OPAT file state
|
||||
let currentOPATFile = null;
|
||||
|
||||
// Bundle state management
|
||||
const getBundleState = () => ({
|
||||
currentBundle,
|
||||
currentBundlePath,
|
||||
hasUnsavedChanges,
|
||||
originalMetadata
|
||||
});
|
||||
|
||||
const setBundleState = (bundle, bundlePath) => {
|
||||
currentBundle = bundle;
|
||||
currentBundlePath = bundlePath;
|
||||
hasUnsavedChanges = false;
|
||||
originalMetadata = bundle ? { ...bundle.manifest } : {};
|
||||
};
|
||||
|
||||
const clearBundleState = () => {
|
||||
currentBundle = null;
|
||||
currentBundlePath = null;
|
||||
hasUnsavedChanges = false;
|
||||
originalMetadata = {};
|
||||
};
|
||||
|
||||
const markUnsavedChanges = (hasChanges = true) => {
|
||||
hasUnsavedChanges = hasChanges;
|
||||
};
|
||||
|
||||
const updateOriginalMetadata = (metadata) => {
|
||||
originalMetadata = { ...metadata };
|
||||
};
|
||||
|
||||
// Pending operation management
|
||||
const setPendingOperation = (operation) => {
|
||||
pendingOperation = operation;
|
||||
};
|
||||
|
||||
const getPendingOperation = () => {
|
||||
return pendingOperation;
|
||||
};
|
||||
|
||||
const clearPendingOperation = () => {
|
||||
pendingOperation = null;
|
||||
};
|
||||
|
||||
// OPAT file state management
|
||||
const setOPATFile = (opatFile) => {
|
||||
currentOPATFile = opatFile;
|
||||
};
|
||||
|
||||
const getOPATFile = () => {
|
||||
return currentOPATFile;
|
||||
};
|
||||
|
||||
const clearOPATFile = () => {
|
||||
currentOPATFile = null;
|
||||
};
|
||||
|
||||
// Export state management functions
|
||||
module.exports = {
|
||||
// Bundle state
|
||||
getBundleState,
|
||||
setBundleState,
|
||||
clearBundleState,
|
||||
markUnsavedChanges,
|
||||
updateOriginalMetadata,
|
||||
|
||||
// Pending operations
|
||||
setPendingOperation,
|
||||
getPendingOperation,
|
||||
clearPendingOperation,
|
||||
|
||||
// OPAT file state
|
||||
setOPATFile,
|
||||
getOPATFile,
|
||||
clearOPATFile,
|
||||
|
||||
// Direct state access (for compatibility)
|
||||
getCurrentBundle: () => currentBundle,
|
||||
getCurrentBundlePath: () => currentBundlePath,
|
||||
getHasUnsavedChanges: () => hasUnsavedChanges,
|
||||
getOriginalMetadata: () => originalMetadata
|
||||
};
|
||||
183
electron/renderer/ui-components.js
Normal file
183
electron/renderer/ui-components.js
Normal file
@@ -0,0 +1,183 @@
|
||||
// UI components module for the 4DSTAR Bundle Manager
|
||||
// Extracted from renderer.js to centralize reusable UI component logic
|
||||
|
||||
// Import dependencies (these will be injected when integrated)
|
||||
let stateManager, domManager;
|
||||
|
||||
// Helper function to create editable fields with pencil icons
|
||||
function createEditableField(label, fieldName, value) {
|
||||
const displayValue = value === 'N/A' ? '' : value;
|
||||
return `
|
||||
<p class="editable-field">
|
||||
<strong>${label}:</strong>
|
||||
<span class="field-display" data-field="${fieldName}">${value}</span>
|
||||
<span class="field-edit hidden" data-field="${fieldName}">
|
||||
<input type="text" class="field-input" data-field="${fieldName}" value="${displayValue}">
|
||||
</span>
|
||||
<button class="edit-icon" data-field="${fieldName}" title="Edit ${label}">✏️</button>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
// Setup event listeners for editable fields
|
||||
function setupEditableFieldListeners() {
|
||||
const editIcons = document.querySelectorAll('.edit-icon');
|
||||
const fieldInputs = document.querySelectorAll('.field-input');
|
||||
|
||||
editIcons.forEach(icon => {
|
||||
icon.addEventListener('click', (e) => {
|
||||
const fieldName = e.target.dataset.field;
|
||||
toggleFieldEdit(fieldName, true);
|
||||
});
|
||||
});
|
||||
|
||||
fieldInputs.forEach(input => {
|
||||
input.addEventListener('blur', (e) => {
|
||||
const fieldName = e.target.dataset.field;
|
||||
saveFieldEdit(fieldName);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const fieldName = e.target.dataset.field;
|
||||
saveFieldEdit(fieldName);
|
||||
} else if (e.key === 'Escape') {
|
||||
const fieldName = e.target.dataset.field;
|
||||
cancelFieldEdit(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
checkForChanges();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle between display and edit mode for a field
|
||||
function toggleFieldEdit(fieldName, editMode) {
|
||||
const displaySpan = document.querySelector(`.field-display[data-field="${fieldName}"]`);
|
||||
const editSpan = document.querySelector(`.field-edit[data-field="${fieldName}"]`);
|
||||
const input = document.querySelector(`.field-input[data-field="${fieldName}"]`);
|
||||
const icon = document.querySelector(`.edit-icon[data-field="${fieldName}"]`);
|
||||
|
||||
if (editMode) {
|
||||
displaySpan.classList.add('hidden');
|
||||
editSpan.classList.remove('hidden');
|
||||
icon.textContent = '✅';
|
||||
icon.title = 'Save';
|
||||
input.focus();
|
||||
input.select();
|
||||
} else {
|
||||
displaySpan.classList.remove('hidden');
|
||||
editSpan.classList.add('hidden');
|
||||
icon.textContent = '✏️';
|
||||
icon.title = `Edit ${fieldName}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Save field edit and update display
|
||||
function saveFieldEdit(fieldName) {
|
||||
const input = document.querySelector(`.field-input[data-field="${fieldName}"]`);
|
||||
const displaySpan = document.querySelector(`.field-display[data-field="${fieldName}"]`);
|
||||
|
||||
const newValue = input.value.trim();
|
||||
const displayValue = newValue || 'N/A';
|
||||
|
||||
displaySpan.textContent = displayValue;
|
||||
toggleFieldEdit(fieldName, false);
|
||||
checkForChanges();
|
||||
}
|
||||
|
||||
// Cancel field edit and restore original value
|
||||
function cancelFieldEdit(fieldName) {
|
||||
const input = document.querySelector(`.field-input[data-field="${fieldName}"]`);
|
||||
const originalMetadata = stateManager.getOriginalMetadata();
|
||||
const originalValue = originalMetadata[fieldName] || '';
|
||||
|
||||
input.value = originalValue;
|
||||
toggleFieldEdit(fieldName, false);
|
||||
}
|
||||
|
||||
// Check if any fields have been modified
|
||||
function checkForChanges() {
|
||||
const inputs = document.querySelectorAll('.field-input');
|
||||
const originalMetadata = stateManager.getOriginalMetadata();
|
||||
let hasChanges = false;
|
||||
|
||||
inputs.forEach(input => {
|
||||
const fieldName = input.dataset.field;
|
||||
const currentValue = input.value.trim();
|
||||
const originalValue = originalMetadata[fieldName] || '';
|
||||
|
||||
if (currentValue !== originalValue) {
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
stateManager.markUnsavedChanges(hasChanges);
|
||||
updateSaveButtonVisibility();
|
||||
}
|
||||
|
||||
// Show/hide save button based on changes
|
||||
function updateSaveButtonVisibility() {
|
||||
const elements = domManager.getElements();
|
||||
const hasUnsavedChanges = stateManager.getHasUnsavedChanges();
|
||||
|
||||
if (hasUnsavedChanges) {
|
||||
elements.saveMetadataBtn.classList.remove('hidden');
|
||||
} else {
|
||||
elements.saveMetadataBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Show save options modal
|
||||
function showSaveOptionsModal() {
|
||||
const hasUnsavedChanges = stateManager.getHasUnsavedChanges();
|
||||
if (!hasUnsavedChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = domManager.getElements();
|
||||
const currentBundle = stateManager.getCurrentBundle();
|
||||
|
||||
// Check if bundle is signed and show warning banner
|
||||
const signatureWarningSection = document.getElementById('signature-warning-section');
|
||||
const isSigned = currentBundle &&
|
||||
currentBundle.report &&
|
||||
currentBundle.report.signature &&
|
||||
currentBundle.report.signature.status &&
|
||||
['TRUSTED', 'UNTRUSTED'].includes(currentBundle.report.signature.status);
|
||||
|
||||
if (isSigned) {
|
||||
signatureWarningSection.classList.remove('hidden');
|
||||
} else {
|
||||
signatureWarningSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
elements.saveOptionsModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Hide save options modal
|
||||
function hideSaveOptionsModal() {
|
||||
const elements = domManager.getElements();
|
||||
elements.saveOptionsModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Initialize dependencies (called when module is loaded)
|
||||
function initializeDependencies(deps) {
|
||||
stateManager = deps.stateManager;
|
||||
domManager = deps.domManager;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDependencies,
|
||||
createEditableField,
|
||||
setupEditableFieldListeners,
|
||||
toggleFieldEdit,
|
||||
saveFieldEdit,
|
||||
cancelFieldEdit,
|
||||
checkForChanges,
|
||||
updateSaveButtonVisibility,
|
||||
showSaveOptionsModal,
|
||||
hideSaveOptionsModal
|
||||
};
|
||||
Reference in New Issue
Block a user