refactor(electron): major ui refactor into modules

This commit is contained in:
2025-08-10 11:41:47 -04:00
parent a1752aaf37
commit 875be6a43c
17 changed files with 2234 additions and 1710 deletions

View 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
};

View 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
})
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};