const { ipcRenderer } = require('electron'); const path = require('path'); // --- STATE --- let currentBundle = null; // --- DOM ELEMENTS --- // Views const welcomeScreen = document.getElementById('welcome-screen'); const bundleView = document.getElementById('bundle-view'); const createBundleForm = document.getElementById('create-bundle-form'); // This will be a modal later // Sidebar buttons const openBundleBtn = document.getElementById('open-bundle-btn'); const createBundleBtn = document.getElementById('create-bundle-btn'); // Bundle action buttons const signBundleBtn = document.getElementById('sign-bundle-btn'); const validateBundleBtn = document.getElementById('validate-bundle-btn'); // Fill button removed - Fill tab is now always visible const clearBundleBtn = document.getElementById('clear-bundle-btn'); const saveMetadataBtn = document.getElementById('save-metadata-btn'); // Save options modal elements const saveOptionsModal = document.getElementById('save-options-modal'); const overwriteBundleBtn = document.getElementById('overwrite-bundle-btn'); const saveAsNewBtn = document.getElementById('save-as-new-btn'); // Fill tab elements const fillTabLink = document.getElementById('fill-tab-link'); const loadFillableTargetsBtn = document.getElementById('load-fillable-targets-btn'); const fillLoading = document.getElementById('fill-loading'); const fillPluginsTables = document.getElementById('fill-plugins-tables'); const fillNoTargets = document.getElementById('fill-no-targets'); const fillTargetsContent = document.getElementById('fill-targets-content'); const selectAllTargetsBtn = document.getElementById('select-all-targets'); const deselectAllTargetsBtn = document.getElementById('deselect-all-targets'); const startFillProcessBtn = document.getElementById('start-fill-process'); const fillProgressContainer = document.getElementById('fill-progress-container'); const fillProgressContent = document.getElementById('fill-progress-content'); // Bundle display const bundleTitle = document.getElementById('bundle-title'); const manifestDetails = document.getElementById('manifest-details'); 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'); let currentBundlePath = null; let hasUnsavedChanges = false; let originalMetadata = {}; // --- INITIALIZATION --- document.addEventListener('DOMContentLoaded', async () => { // Set initial view showView('welcome-screen'); // Set initial theme const isDarkMode = await ipcRenderer.invoke('get-dark-mode'); document.body.classList.toggle('dark-mode', isDarkMode); // Setup event listeners setupEventListeners(); }); // --- EVENT LISTENERS --- function setupEventListeners() { // Theme updates ipcRenderer.on('theme-updated', (event, { shouldUseDarkColors }) => { document.body.classList.toggle('dark-mode', shouldUseDarkColors); }); // Sidebar navigation openBundleBtn.addEventListener('click', handleOpenBundle); createBundleBtn.addEventListener('click', () => { // TODO: Replace with modal showView('create-bundle-form'); showModal('Not Implemented', 'The create bundle form will be moved to a modal dialog.'); }); // Tab navigation tabLinks.forEach(link => { link.addEventListener('click', () => switchTab(link.dataset.tab)); }); // Modal close button modalCloseBtn.addEventListener('click', hideModal); // Bundle actions signBundleBtn.addEventListener('click', handleSignBundle); validateBundleBtn.addEventListener('click', handleValidateBundle); clearBundleBtn.addEventListener('click', handleClearBundle); saveMetadataBtn.addEventListener('click', showSaveOptionsModal); overwriteBundleBtn.addEventListener('click', () => handleSaveMetadata(false)); saveAsNewBtn.addEventListener('click', () => handleSaveMetadata(true)); // Load fillable targets button loadFillableTargetsBtn.addEventListener('click', async () => { await loadFillableTargets(); }); // Load fillable targets for the Fill tab async function loadFillableTargets() { console.log('loadFillableTargets called, currentBundlePath:', currentBundlePath); // Check if required DOM elements exist if (!fillNoTargets || !fillTargetsContent || !fillLoading) { console.error('Fill tab DOM elements not found'); showModal('Error', 'Fill tab interface not properly initialized.'); return; } if (!currentBundlePath) { console.log('No bundle path, showing no targets message'); hideAllFillStates(); fillNoTargets.classList.remove('hidden'); return; } try { // Show loading state hideAllFillStates(); fillLoading.classList.remove('hidden'); loadFillableTargetsBtn.disabled = true; console.log('Calling get-fillable-targets...'); const result = await ipcRenderer.invoke('get-fillable-targets', currentBundlePath); console.log('get-fillable-targets result:', result); if (!result.success) { console.log('get-fillable-targets failed:', result.error); hideAllFillStates(); showModal('Error', `Failed to load fillable targets: ${result.error}`); return; } const targets = result.data; console.log('Fillable targets:', targets); hideAllFillStates(); if (!targets || Object.keys(targets).length === 0) { console.log('No fillable targets found'); fillNoTargets.classList.remove('hidden'); } else { console.log('Populating fillable targets table'); fillTargetsContent.classList.remove('hidden'); populateFillTargetsTable(targets); } } catch (error) { console.error('Error in loadFillableTargets:', error); hideAllFillStates(); showModal('Error', `Error loading fillable targets: ${error.message}`); } finally { loadFillableTargetsBtn.disabled = false; } } // Helper function to hide all fill tab states function hideAllFillStates() { fillLoading.classList.add('hidden'); fillNoTargets.classList.add('hidden'); fillTargetsContent.classList.add('hidden'); } // Old modal code removed - now using tab-based interface // Create modern table-based interface for fillable targets function populateFillTargetsTable(plugins) { 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 = `
Bundled On: ${manifest.bundledOn || 'N/A'}
${createEditableField('Comment', 'bundleComment', manifest.bundleComment || 'N/A')} ${manifest.bundleAuthorKeyFingerprint ? `Author Key: ${manifest.bundleAuthorKeyFingerprint}
` : ''} ${manifest.bundleSignature ? `Signature: ${manifest.bundleSignature}
` : ''}Source: ${pluginData.sdist_path}
Binaries:
No plugins found in this bundle.
${label}: ${value}
`; } // 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 originalValue = originalMetadata[fieldName] || ''; input.value = originalValue; toggleFieldEdit(fieldName, false); } // Check if any fields have been modified function checkForChanges() { const inputs = document.querySelectorAll('.field-input'); 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; } }); hasUnsavedChanges = hasChanges; updateSaveButtonVisibility(); } // Show/hide save button based on changes function updateSaveButtonVisibility() { if (hasUnsavedChanges) { saveMetadataBtn.classList.remove('hidden'); } else { saveMetadataBtn.classList.add('hidden'); } } // Show save options modal function showSaveOptionsModal() { if (!currentBundlePath || !hasUnsavedChanges) return; saveOptionsModal.classList.remove('hidden'); } // Hide save options modal function hideSaveOptionsModal() { saveOptionsModal.classList.add('hidden'); } // Handle save metadata with option for save as new async function handleSaveMetadata(saveAsNew = false) { if (!currentBundlePath || !hasUnsavedChanges) return; // Hide the modal first hideSaveOptionsModal(); const inputs = document.querySelectorAll('.field-input'); const updatedMetadata = {}; inputs.forEach(input => { const fieldName = input.dataset.field; const value = input.value.trim(); if (value !== originalMetadata[fieldName]) { // Convert camelCase to snake_case for backend const backendFieldName = fieldName.replace(/([A-Z])/g, '_$1').toLowerCase(); updatedMetadata[backendFieldName] = value; } }); if (Object.keys(updatedMetadata).length === 0) { hasUnsavedChanges = false; updateSaveButtonVisibility(); return; } let targetPath = currentBundlePath; if (saveAsNew) { // Show file save dialog for new bundle const result = await ipcRenderer.invoke('show-save-dialog', { defaultPath: currentBundlePath.replace('.fbundle', '_edited.fbundle'), filters: [{ name: 'Bundle Files', extensions: ['fbundle'] }] }); if (result.canceled) { return; // User canceled the save dialog } targetPath = result.filePath; // Copy the original bundle to the new location first try { await ipcRenderer.invoke('copy-file', { source: currentBundlePath, destination: targetPath }); } catch (error) { showModal('Copy Error', `Failed to copy bundle: ${error.message}`); return; } } showSpinner(); const result = await ipcRenderer.invoke('edit-bundle', { bundlePath: targetPath, updatedManifest: updatedMetadata }); hideSpinner(); if (result.success) { // Update original metadata to reflect saved changes Object.keys(updatedMetadata).forEach(backendKey => { const frontendKey = backendKey.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase()); originalMetadata[frontendKey] = updatedMetadata[backendKey]; }); hasUnsavedChanges = false; updateSaveButtonVisibility(); if (saveAsNew) { showModal('Success', `New bundle created successfully at: ${targetPath}`); // Open the new bundle currentBundlePath = targetPath; await reloadCurrentBundle(); } else { showModal('Success', 'Metadata updated successfully!'); // Reload current bundle to reflect changes await reloadCurrentBundle(); } } else { showModal('Save Error', `Failed to save metadata: ${result.error}`); } } // Helper function to reload current bundle async function reloadCurrentBundle() { if (!currentBundlePath) return; const result = await ipcRenderer.invoke('open-bundle', currentBundlePath); if (result.success) { displayBundleInfo(result.report); } }